diff --git a/hack/dev/boot.sh b/hack/dev/boot.sh index e52c513..b5f3a21 100755 --- a/hack/dev/boot.sh +++ b/hack/dev/boot.sh @@ -65,13 +65,8 @@ set -- "${@}" \ -drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \ -device nvme,drive=disk1,serial=cafebabe -if [ "${DISK_BOOT}" = "1" ]; then - set -- "${@}" \ - -drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on" -else - set -- "${@}" \ - -drive "if=none,file=fat:rw:${FINAL_DIR}/efi,format=raw,id=disk1" -fi +set -- "${@}" \ + -drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on" set -- "${@}" -name "sprout ${TARGET_ARCH}" diff --git a/hack/dev/boot/Dockerfile b/hack/dev/boot/Dockerfile index 0c0b355..45010f5 100644 --- a/hack/dev/boot/Dockerfile +++ b/hack/dev/boot/Dockerfile @@ -13,6 +13,7 @@ COPY xen.efi /work/XEN.EFI COPY xen.cfg /work/XEN.CFG COPY initramfs /work/INITRAMFS COPY edera-splash.png /work/EDERA-SPLASH.PNG +COPY bls.conf /work/BLS.CONF RUN truncate -s128MiB sprout.img && \ parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \ parted --script sprout.img mkpart primary fat32 1MiB 100% > /dev/null 2>&1 && \ @@ -20,6 +21,8 @@ RUN truncate -s128MiB sprout.img && \ mkfs.vfat -F32 -n EFI sprout.img && \ mmd -i sprout.img ::/EFI && \ mmd -i sprout.img ::/EFI/BOOT && \ + mmd -i sprout.img ::/LOADER && \ + mmd -i sprout.img ::/LOADER/ENTRIES && \ mcopy -i sprout.img ${EFI_NAME}.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img SHELL.EFI ::/EFI/BOOT/ && \ @@ -28,6 +31,7 @@ RUN truncate -s128MiB sprout.img && \ mcopy -i sprout.img SPROUT.TOML ::/ && \ mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \ mcopy -i sprout.img INITRAMFS ::/ && \ + mcopy -i sprout.img BLS.CONF ::/LOADER/ENTRIES/ && \ mv sprout.img /sprout.img FROM scratch AS final diff --git a/hack/dev/build.sh b/hack/dev/build.sh index d4acd4e..6506ce5 100755 --- a/hack/dev/build.sh +++ b/hack/dev/build.sh @@ -108,6 +108,7 @@ if [ "${SKIP_SPROUT_BUILD}" != "1" ]; then cp "hack/dev/configs/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml" cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg" cp "hack/dev/assets/edera-splash.png" "${FINAL_DIR}/edera-splash.png" + cp "hack/dev/configs/bls.conf" "${FINAL_DIR}/bls.conf" mkdir -p "${FINAL_DIR}/efi/EFI/BOOT" cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI" diff --git a/hack/dev/configs/autoconfigure.sprout.toml b/hack/dev/configs/autoconfigure.sprout.toml new file mode 100644 index 0000000..9b7d56f --- /dev/null +++ b/hack/dev/configs/autoconfigure.sprout.toml @@ -0,0 +1,4 @@ +version = 1 + +[defaults] +autoconfigure = true diff --git a/hack/dev/configs/bls.conf b/hack/dev/configs/bls.conf new file mode 100644 index 0000000..ff0017e --- /dev/null +++ b/hack/dev/configs/bls.conf @@ -0,0 +1,4 @@ +title Boot Linux +linux /efi/boot/kernel.efi +options console=hvc0 +initrd /initramfs diff --git a/src/actions.rs b/src/actions.rs index 8bd1031..e20c951 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -19,7 +19,7 @@ pub mod splash; /// that you can specify via other concepts. /// /// Actions are the main work that Sprout gets done, like booting Linux. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ActionDeclaration { /// Chainload to another EFI application. /// This allows you to load any EFI application, either to boot an operating system diff --git a/src/actions/chainload.rs b/src/actions/chainload.rs index 891ca27..96bff81 100644 --- a/src/actions/chainload.rs +++ b/src/actions/chainload.rs @@ -10,7 +10,7 @@ use uefi::CString16; use uefi::proto::loaded_image::LoadedImage; /// The configuration of the chainload action. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ChainloadConfiguration { /// The path to the image to chainload. /// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable. @@ -99,10 +99,6 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat initrd_handle = Some(handle); } - // Retrieve the base and size of the loaded image to display. - let (base, size) = loaded_image_protocol.info(); - info!("loaded image: base={:#x} size={:#x}", base.addr(), size); - // Start the loaded image. // This call might return, or it may pass full control to another image that will never return. // Capture the result to ensure we can return an error if the image fails to start, but only diff --git a/src/actions/edera.rs b/src/actions/edera.rs index a88b9e1..e9cf1d5 100644 --- a/src/actions/edera.rs +++ b/src/actions/edera.rs @@ -22,7 +22,7 @@ use crate::{ /// The configuration of the edera action which boots the Edera hypervisor. /// Edera is based on Xen but modified significantly with a Rust stack. /// Sprout is a component of the Edera stack and provides the boot functionality of Xen. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct EderaConfiguration { /// The path to the Xen hypervisor EFI image. pub xen: String, diff --git a/src/actions/print.rs b/src/actions/print.rs index 9b1bbe6..1013d46 100644 --- a/src/actions/print.rs +++ b/src/actions/print.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::rc::Rc; /// The configuration of the print action. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct PrintConfiguration { /// The text to print to the console. #[serde(default)] diff --git a/src/actions/splash.rs b/src/actions/splash.rs index 991aecf..173ed43 100644 --- a/src/actions/splash.rs +++ b/src/actions/splash.rs @@ -15,7 +15,7 @@ use uefi::proto::console::gop::GraphicsOutput; const DEFAULT_SPLASH_TIME: u32 = 0; /// The configuration of the splash action. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct SplashConfiguration { /// The path to the image to display. /// Currently, only PNG images are supported. diff --git a/src/autoconfigure.rs b/src/autoconfigure.rs index 3cd5c2a..71daabf 100644 --- a/src/autoconfigure.rs +++ b/src/autoconfigure.rs @@ -27,24 +27,30 @@ fn scan_for_bls( let bls_entries_path = Path::new(cstr16!("\\loader\\entries")); // Convert the device path root to a string we can use in the configuration. - let root = root + let mut root = root .to_string(DisplayOnly(false), AllowShortcuts(false)) .context("unable to convert device root to string")? .to_string(); + // Add a trailing slash to the root to ensure the path is valid. + root.push('/'); // Whether we have a loader.conf file. let has_loader_conf = filesystem .try_exists(bls_loader_conf_path) - .context("unable to check for BLS entries directory")?; + .context("unable to check for BLS loader.conf file")?; // Whether we have an entries directory. + // We actually iterate the entries to see if there are any. let has_entries_dir = filesystem - .try_exists(bls_entries_path) - .context("unable to check for BLS loader.conf file")?; + .read_dir(bls_entries_path) + .ok() + .and_then(|mut iterator| iterator.next()) + .map(|entry| entry.is_ok()) + .unwrap_or(false); // Detect if a BLS supported configuration is on this filesystem. // We check both loader.conf and entries directory as only one of them is required. - if !has_loader_conf && !has_entries_dir { + if !(has_loader_conf || has_entries_dir) { return Ok(false); } @@ -55,7 +61,7 @@ fn scan_for_bls( actions: vec![BLS_CHAINLOAD_ACTION.to_string()], ..Default::default() }, - path: format!("{}\\loader", root,), + path: format!("{}\\loader", root), }; // Generate a unique name for the BLS generator and insert the generator into the configuration. @@ -79,7 +85,7 @@ fn add_bls_actions(config: &mut RootConfiguration) -> Result<()> { let chainload = ChainloadConfiguration { path: "$chainload".to_string(), options: vec!["$options".to_string()], - linux_initrd: Some("initrd".to_string()), + linux_initrd: Some("$initrd".to_string()), }; // Insert the chainload action into the configuration. @@ -124,8 +130,9 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> { // Scan the filesystem for BLS supported configurations. // If we find any, we will add a BLS generator to the configuration, and then // at the end we will add the BLS actions to the configuration. - has_any_bls = has_any_bls - || scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; + let bls_found = + scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; + has_any_bls = has_any_bls || bls_found; } // If we had any BLS-supported filesystems, add the BLS actions to the configuration. diff --git a/src/config.rs b/src/config.rs index b6a92ad..a6affc3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,7 +18,7 @@ pub const LATEST_VERSION: u32 = 1; pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10; /// The Sprout configuration format. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct RootConfiguration { /// The version of the configuration. This should always be declared /// and be the latest version that is supported. If not specified, it is assumed @@ -66,7 +66,7 @@ pub struct RootConfiguration { } /// Default configuration for Sprout, used when the corresponding options are not specified. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct DefaultsConfiguration { /// The entry to boot without showing the boot menu. /// If not specified, a boot menu is shown. diff --git a/src/context.rs b/src/context.rs index e299325..6afa693 100644 --- a/src/context.rs +++ b/src/context.rs @@ -83,6 +83,11 @@ impl SproutContext { self.root.as_ref() } + /// Access the root context to modify it, if possible. + pub fn root_mut(&mut self) -> Option<&mut RootContext> { + Rc::get_mut(&mut self.root) + } + /// Retrieve the value specified by `key` from this context or its parents. /// Returns `None` if the value is not found. pub fn get(&self, key: impl AsRef) -> Option<&String> { @@ -235,4 +240,10 @@ impl SproutContext { pub fn stamp(&self, text: impl AsRef) -> String { Self::stamp_values(&self.all_values(), text.as_ref()).1 } + + /// Unloads a [SproutContext] back into an owned context. This + /// may not succeed if something else is holding onto the value. + pub fn unload(self: Rc) -> Option { + Rc::into_inner(self) + } } diff --git a/src/drivers.rs b/src/drivers.rs index f2e0daa..cf02fd1 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -12,7 +12,7 @@ use uefi::proto::device_path::LoadedImageDevicePath; /// Drivers allow extending the functionality of Sprout. /// Drivers are loaded at runtime and can provide extra functionality like filesystem support. /// Drivers are loaded by their name, which is used to reference them in other concepts. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct DriverDeclaration { /// The filesystem path to the driver. /// This file should be an EFI executable that can be located and executed. diff --git a/src/entries.rs b/src/entries.rs index f32f781..d624c77 100644 --- a/src/entries.rs +++ b/src/entries.rs @@ -7,7 +7,7 @@ use std::rc::Rc; /// /// Entries are the user-facing concept of Sprout, making it possible /// to run a set of actions with a specific context. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct EntryDeclaration { /// The title of the entry which will be display in the boot menu. /// This is the pre-stamped value. diff --git a/src/extractors.rs b/src/extractors.rs index 7320958..ae4f325 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -10,7 +10,7 @@ pub mod filesystem_device_match; /// Declares an extractor configuration. /// Extractors allow calculating values at runtime /// using built-in sprout modules. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ExtractorDeclaration { /// The filesystem device match extractor. /// This extractor finds a filesystem using some search criteria and returns diff --git a/src/extractors/filesystem_device_match.rs b/src/extractors/filesystem_device_match.rs index 54a91ec..dbe2694 100644 --- a/src/extractors/filesystem_device_match.rs +++ b/src/extractors/filesystem_device_match.rs @@ -20,7 +20,7 @@ use uefi_raw::Status; /// /// This function only requires one of the criteria to match. /// The fallback value can be used to provide a value if none is found. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct FilesystemDeviceMatchExtractor { /// Matches a filesystem that has the specified label. #[serde(default, rename = "has-label")] diff --git a/src/generators.rs b/src/generators.rs index 2c11b1a..2632e19 100644 --- a/src/generators.rs +++ b/src/generators.rs @@ -12,7 +12,7 @@ pub mod matrix; /// Declares a generator configuration. /// Generators allow generating entries at runtime based on a set of data. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct GeneratorDeclaration { /// Matrix generator configuration. /// Matrix allows you to specify multiple value-key values as arrays. diff --git a/src/generators/bls.rs b/src/generators/bls.rs index d18a99a..9dc8d7d 100644 --- a/src/generators/bls.rs +++ b/src/generators/bls.rs @@ -20,7 +20,7 @@ const BLS_TEMPLATE_PATH: &str = "\\loader"; /// The configuration of the BLS generator. /// The BLS uses the Bootloader Specification to produce /// entries from an input template. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct BlsConfiguration { /// The entry to use for as a template. pub entry: EntryDeclaration, @@ -86,7 +86,7 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result Result<()> { autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?; } + // Unload the context so that it can be modified. + let Some(mut context) = context.unload() else { + bail!("context safety violation while trying to unload context"); + }; + + // Perform root context modification in a block to release the modification when complete. + { + // Modify the root context to include the autoconfigured actions. + let Some(root) = context.root_mut() else { + bail!("context safety violation while trying to modify root context"); + }; + + // Extend the root context with the autoconfigured actions. + root.actions_mut().extend(config.actions); + } + + // Refreeze the context to ensure that further operations can share the context. + let context = context.freeze(); + // Run all the extractors declared in the configuration. let mut extracted = BTreeMap::new(); for (name, extractor) in &config.extractors { diff --git a/src/phases.rs b/src/phases.rs index 68de3c4..a6303e9 100644 --- a/src/phases.rs +++ b/src/phases.rs @@ -7,7 +7,7 @@ use std::rc::Rc; /// Configures the various phases of the boot process. /// This allows hooking various phases to run actions. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct PhasesConfiguration { /// The early phase is run before drivers are loaded. #[serde(default)] @@ -23,7 +23,7 @@ pub struct PhasesConfiguration { /// Configures a single phase of the boot process. /// There can be multiple phase configurations that are /// executed sequentially. -#[derive(Serialize, Deserialize, Default, Clone)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct PhaseConfiguration { /// The actions to run when the phase is executed. #[serde(default)]