2025-10-13 01:54:03 -07:00
|
|
|
use crate::context::SproutContext;
|
2025-10-24 16:32:48 -07:00
|
|
|
use crate::entries::{BootableEntry, EntryDeclaration};
|
2025-10-13 01:54:03 -07:00
|
|
|
use crate::generators::bls::entry::BlsEntry;
|
|
|
|
|
use crate::utils;
|
2025-11-01 21:34:55 -04:00
|
|
|
use crate::utils::vercmp;
|
2025-10-13 01:54:03 -07:00
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
2025-11-01 20:41:30 -04:00
|
|
|
use std::cmp::Ordering;
|
2025-10-13 01:54:03 -07:00
|
|
|
use std::rc::Rc;
|
|
|
|
|
use std::str::FromStr;
|
2025-10-27 00:50:17 -04:00
|
|
|
use uefi::cstr16;
|
|
|
|
|
use uefi::fs::{FileSystem, PathBuf};
|
2025-10-13 01:54:03 -07:00
|
|
|
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
|
|
|
|
use uefi::proto::media::fs::SimpleFileSystem;
|
|
|
|
|
|
2025-10-20 00:06:46 -07:00
|
|
|
/// BLS entry parser.
|
2025-10-14 12:47:33 -07:00
|
|
|
mod entry;
|
|
|
|
|
|
2025-10-27 00:31:07 -04:00
|
|
|
/// The default path to the BLS directory.
|
|
|
|
|
const BLS_TEMPLATE_PATH: &str = "\\loader";
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
/// The configuration of the BLS generator.
|
|
|
|
|
/// The BLS uses the Bootloader Specification to produce
|
|
|
|
|
/// entries from an input template.
|
2025-10-27 03:37:09 -04:00
|
|
|
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
2025-10-13 01:54:03 -07:00
|
|
|
pub struct BlsConfiguration {
|
2025-10-20 00:06:46 -07:00
|
|
|
/// The entry to use for as a template.
|
2025-10-13 01:54:03 -07:00
|
|
|
pub entry: EntryDeclaration,
|
2025-10-27 00:31:07 -04:00
|
|
|
/// The path to the BLS directory.
|
2025-10-13 01:54:03 -07:00
|
|
|
#[serde(default = "default_bls_path")]
|
|
|
|
|
pub path: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_bls_path() -> String {
|
2025-10-20 00:06:46 -07:00
|
|
|
BLS_TEMPLATE_PATH.to_string()
|
2025-10-13 01:54:03 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-20 11:33:33 -07:00
|
|
|
// TODO(azenla): remove this once variable substitution is implemented.
|
|
|
|
|
/// This function is used to remove the `tuned_initrd` variable from entry values.
|
|
|
|
|
/// Fedora uses tuned which adds an initrd that shouldn't be used.
|
|
|
|
|
fn quirk_initrd_remove_tuned(input: String) -> String {
|
|
|
|
|
input.replace("$tuned_initrd", "").trim().to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 20:41:30 -04:00
|
|
|
/// Sorts two entries according to the BLS sort system.
|
|
|
|
|
/// Reference: https://uapi-group.org/specifications/specs/boot_loader_specification/#sorting
|
|
|
|
|
fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering {
|
|
|
|
|
// Grab the components of both entries.
|
|
|
|
|
let (a_bls, a_boot) = a;
|
|
|
|
|
let (b_bls, b_boot) = b;
|
|
|
|
|
|
|
|
|
|
// Grab the sort keys from both entries.
|
|
|
|
|
let a_sort_key = a_bls.sort_key();
|
|
|
|
|
let b_sort_key = b_bls.sort_key();
|
|
|
|
|
|
|
|
|
|
// Compare the sort keys of both entries.
|
|
|
|
|
match a_sort_key.cmp(&b_sort_key) {
|
|
|
|
|
// If A and B sort keys are equal, sort by machine-id.
|
|
|
|
|
Ordering::Equal => {
|
|
|
|
|
// Grab the machine-id from both entries.
|
|
|
|
|
let a_machine_id = a_bls.machine_id();
|
|
|
|
|
let b_machine_id = b_bls.machine_id();
|
|
|
|
|
|
|
|
|
|
// Compare the machine-id of both entries.
|
|
|
|
|
match a_machine_id.cmp(&b_machine_id) {
|
|
|
|
|
// If both machine-id values are equal, sort by version.
|
|
|
|
|
Ordering::Equal => {
|
|
|
|
|
// Grab the version from both entries.
|
|
|
|
|
let a_version = a_bls.version();
|
|
|
|
|
let b_version = b_bls.version();
|
|
|
|
|
|
|
|
|
|
// Compare the version of both entries, sorting newer versions first.
|
2025-11-01 21:34:55 -04:00
|
|
|
match vercmp::compare_versions_optional(
|
|
|
|
|
a_version.as_deref(),
|
|
|
|
|
b_version.as_deref(),
|
|
|
|
|
)
|
|
|
|
|
.reverse()
|
|
|
|
|
{
|
2025-11-01 20:41:30 -04:00
|
|
|
// If both versions are equal, sort by file name in reverse order.
|
|
|
|
|
Ordering::Equal => {
|
|
|
|
|
// Grab the file name from both entries.
|
|
|
|
|
let a_name = a_boot.name();
|
|
|
|
|
let b_name = b_boot.name();
|
|
|
|
|
|
|
|
|
|
// Compare the file names of both entries, sorting newer entries first.
|
2025-11-01 21:34:55 -04:00
|
|
|
vercmp::compare_versions(a_name, b_name).reverse()
|
2025-11-01 20:41:30 -04:00
|
|
|
}
|
|
|
|
|
other => other,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
other => other,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
other => other,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 00:06:46 -07:00
|
|
|
/// Generates entries from the BLS entries directory using the specified `bls` configuration and
|
|
|
|
|
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
|
2025-10-24 16:32:48 -07:00
|
|
|
pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> {
|
2025-10-13 01:54:03 -07:00
|
|
|
let mut entries = Vec::new();
|
|
|
|
|
|
2025-10-27 00:31:07 -04:00
|
|
|
// Stamp the path to the BLS directory.
|
2025-10-13 01:54:03 -07:00
|
|
|
let path = context.stamp(&bls.path);
|
2025-10-20 00:06:46 -07:00
|
|
|
|
2025-10-27 00:50:17 -04:00
|
|
|
// Resolve the path to the BLS directory.
|
2025-10-30 22:56:01 -04:00
|
|
|
let bls_resolved = utils::resolve_path(Some(context.root().loaded_image_path()?), &path)
|
2025-10-27 00:50:17 -04:00
|
|
|
.context("unable to resolve bls path")?;
|
2025-10-27 00:31:07 -04:00
|
|
|
|
2025-10-27 00:50:17 -04:00
|
|
|
// Construct a filesystem path to the BLS entries directory.
|
|
|
|
|
let mut entries_path = PathBuf::from(
|
|
|
|
|
bls_resolved
|
|
|
|
|
.sub_path
|
|
|
|
|
.to_string(DisplayOnly(false), AllowShortcuts(false))
|
|
|
|
|
.context("unable to convert bls path to string")?,
|
|
|
|
|
);
|
2025-10-27 00:31:07 -04:00
|
|
|
entries_path.push(cstr16!("entries"));
|
|
|
|
|
|
2025-10-20 00:06:46 -07:00
|
|
|
// Open exclusive access to the BLS filesystem.
|
2025-10-27 00:31:07 -04:00
|
|
|
let fs =
|
2025-10-27 00:50:17 -04:00
|
|
|
uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(bls_resolved.filesystem_handle)
|
2025-10-27 00:31:07 -04:00
|
|
|
.context("unable to open bls filesystem")?;
|
2025-10-13 01:54:03 -07:00
|
|
|
let mut fs = FileSystem::new(fs);
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
// Read the BLS entries directory.
|
2025-10-13 01:54:03 -07:00
|
|
|
let entries_iter = fs
|
2025-10-27 00:50:17 -04:00
|
|
|
.read_dir(&entries_path)
|
2025-10-14 12:47:33 -07:00
|
|
|
.context("unable to read bls entries")?;
|
2025-10-13 01:54:03 -07:00
|
|
|
|
2025-10-20 00:06:46 -07:00
|
|
|
// For each entry in the BLS entries directory, parse the entry and add it to the list.
|
2025-10-13 01:54:03 -07:00
|
|
|
for entry in entries_iter {
|
2025-10-20 00:06:46 -07:00
|
|
|
// Unwrap the entry file info.
|
|
|
|
|
let entry = entry.context("unable to read bls item entry")?;
|
|
|
|
|
|
|
|
|
|
// Skip items that are not regular files.
|
2025-10-13 01:54:03 -07:00
|
|
|
if !entry.is_regular_file() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
// Get the file name of the filesystem item.
|
2025-10-28 23:23:12 -04:00
|
|
|
let mut name = entry.file_name().to_string();
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
// Ignore files that are not .conf files.
|
2025-10-27 03:37:09 -04:00
|
|
|
if !name.to_lowercase().ends_with(".conf") {
|
2025-10-13 01:54:03 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 23:23:12 -04:00
|
|
|
// Remove the .conf extension.
|
|
|
|
|
name.truncate(name.len() - 5);
|
|
|
|
|
|
2025-11-01 18:59:41 -04:00
|
|
|
// Skip over files that are named just ".conf" as they are not valid entry files.
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 18:56:11 -07:00
|
|
|
// Create a mutable path so we can append the file name to produce the full path.
|
|
|
|
|
let mut full_entry_path = entries_path.to_path_buf();
|
|
|
|
|
full_entry_path.push(entry.file_name());
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
// Read the entry file.
|
2025-10-13 01:54:03 -07:00
|
|
|
let content = fs
|
|
|
|
|
.read(full_entry_path)
|
2025-10-14 12:47:33 -07:00
|
|
|
.context("unable to read bls file")?;
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
// Parse the entry file as a UTF-8 string.
|
2025-10-14 12:47:33 -07:00
|
|
|
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?;
|
2025-10-20 00:06:46 -07:00
|
|
|
|
|
|
|
|
// Parse the entry file as a BLS entry.
|
2025-10-14 12:47:33 -07:00
|
|
|
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?;
|
2025-10-13 01:54:03 -07:00
|
|
|
|
2025-10-20 00:06:46 -07:00
|
|
|
// Ignore entries that are not valid for Sprout.
|
2025-10-13 01:54:03 -07:00
|
|
|
if !entry.is_valid() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 00:06:46 -07:00
|
|
|
// Produce a new sprout context for the entry with the extracted values.
|
2025-10-13 01:54:03 -07:00
|
|
|
let mut context = context.fork();
|
2025-10-20 11:33:33 -07:00
|
|
|
|
2025-11-01 21:03:02 -04:00
|
|
|
let title_base = entry.title().unwrap_or_else(|| name.clone());
|
2025-10-20 11:33:33 -07:00
|
|
|
let chainload = entry.chainload_path().unwrap_or_default();
|
|
|
|
|
let options = entry.options().unwrap_or_default();
|
2025-11-01 21:03:02 -04:00
|
|
|
let version = entry.version().unwrap_or_default();
|
|
|
|
|
let machine_id = entry.machine_id().unwrap_or_default();
|
2025-10-20 11:33:33 -07:00
|
|
|
|
|
|
|
|
// Put the initrd through a quirk modifier to support Fedora.
|
|
|
|
|
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
|
|
|
|
|
|
2025-11-01 21:03:02 -04:00
|
|
|
// Combine the title with the version if a version is present.
|
|
|
|
|
let title_full = if !version.is_empty() {
|
|
|
|
|
format!("{} {}", title_base, version)
|
|
|
|
|
} else {
|
|
|
|
|
title_base.clone()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
context.set("title-base", title_base);
|
|
|
|
|
context.set("title", title_full);
|
2025-10-20 11:33:33 -07:00
|
|
|
context.set("chainload", chainload);
|
|
|
|
|
context.set("options", options);
|
|
|
|
|
context.set("initrd", initrd);
|
2025-11-01 21:03:02 -04:00
|
|
|
context.set("version", version);
|
|
|
|
|
context.set("machine-id", machine_id);
|
2025-10-13 01:54:03 -07:00
|
|
|
|
2025-10-28 23:23:12 -04:00
|
|
|
// Produce a new bootable entry.
|
2025-11-01 20:41:30 -04:00
|
|
|
let mut boot = BootableEntry::new(
|
2025-10-24 16:32:48 -07:00
|
|
|
name,
|
|
|
|
|
bls.entry.title.clone(),
|
|
|
|
|
context.freeze(),
|
|
|
|
|
bls.entry.clone(),
|
2025-10-28 23:23:12 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Pin the entry name to prevent prefixing.
|
|
|
|
|
// This is needed as the bootloader interface requires the name to be
|
|
|
|
|
// the same as the entry file name, minus the .conf extension.
|
2025-11-01 20:41:30 -04:00
|
|
|
boot.mark_pin_name();
|
2025-10-28 23:23:12 -04:00
|
|
|
|
2025-11-01 20:41:30 -04:00
|
|
|
// Add the BLS entry to the list, along with the bootable entry.
|
|
|
|
|
entries.push((entry, boot));
|
2025-10-13 01:54:03 -07:00
|
|
|
}
|
|
|
|
|
|
2025-11-01 20:41:30 -04:00
|
|
|
// Sort all the entries according to the BLS sort system.
|
|
|
|
|
entries.sort_by(sort_entries);
|
|
|
|
|
|
|
|
|
|
// Collect all the bootable entries and return them.
|
|
|
|
|
Ok(entries.into_iter().map(|(_, boot)| boot).collect())
|
2025-10-13 01:54:03 -07:00
|
|
|
}
|