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 new file mode 100644 index 0000000..3f90e53 --- /dev/null +++ b/src/autoconfigure.rs @@ -0,0 +1,129 @@ +use crate::actions::ActionDeclaration; +use crate::actions::chainload::ChainloadConfiguration; +use crate::config::RootConfiguration; +use crate::entries::EntryDeclaration; +use crate::generators::GeneratorDeclaration; +use crate::generators::bls::BlsConfiguration; +use anyhow::{Context, Result}; +use uefi::cstr16; +use uefi::fs::{FileSystem, Path}; +use uefi::proto::device_path::DevicePath; +use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly}; +use uefi::proto::media::fs::SimpleFileSystem; + +/// The name prefix of the BLS chainload action that will be used +/// by the BLS generator to chainload entries. +const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-"; + +/// Scan the specified `filesystem` for BLS configurations. +fn scan_for_bls( + filesystem: &mut FileSystem, + root: &DevicePath, + config: &mut RootConfiguration, +) -> Result { + // BLS has a loader.conf file that can specify its own auto-entries mechanism. + let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf")); + // BLS also has an entries directory that can specify explicit entries. + let bls_entries_path = Path::new(cstr16!("\\loader\\entries")); + + // Convert the device path root to a string we can use in the configuration. + 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 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 + .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) { + return Ok(false); + } + + // Generate a unique name for the BLS chainload action. + let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root); + + // BLS is now detected, generate a configuration for it. + let generator = BlsConfiguration { + entry: EntryDeclaration { + title: "$title".to_string(), + actions: vec![chainload_action_name.clone()], + ..Default::default() + }, + path: format!("{}\\loader", root), + }; + + // Generate a unique name for the BLS generator and insert the generator into the configuration. + config.generators.insert( + format!("autoconfigure-bls-{}", root), + GeneratorDeclaration { + bls: Some(generator), + ..Default::default() + }, + ); + + // Generate a chainload configuration for BLS. + // BLS will provide these values to us. + let chainload = ChainloadConfiguration { + path: format!("{}\\$chainload", root), + options: vec!["$options".to_string()], + linux_initrd: Some(format!("{}\\$initrd", root)), + }; + + // Insert the chainload action into the configuration. + config.actions.insert( + chainload_action_name, + ActionDeclaration { + chainload: Some(chainload), + ..Default::default() + }, + ); + + // We had a BLS supported configuration, so return true. + Ok(true) +} + +/// Generate a [RootConfiguration] based on the environment. +/// Intakes a `config` to use as the basis of the autoconfiguration. +pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> { + // Find all the filesystems that are on the system. + let filesystem_handles = + uefi::boot::find_handles::().context("unable to scan filesystems")?; + + // For each filesystem that was detected, scan it for supported autoconfig mechanisms. + for handle in filesystem_handles { + // Acquire the device path root for the filesystem. + let root = { + uefi::boot::open_protocol_exclusive::(handle) + .context("unable to get root for filesystem")? + .to_boxed() + }; + + // Open the filesystem that was detected. + let filesystem = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to open filesystem")?; + + // Trade the filesystem protocol for the uefi filesystem helper. + let mut filesystem = FileSystem::new(filesystem); + + // Scan the filesystem for BLS supported configurations. + // If we find any, we will add a BLS generator to the configuration. + scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; + } + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 4ad4225..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. @@ -74,6 +74,8 @@ pub struct DefaultsConfiguration { /// The timeout of the boot menu. #[serde(rename = "menu-timeout", default = "default_menu_timeout")] pub menu_timeout: u64, + /// Enables autoconfiguration of Sprout based on the environment. + pub autoconfigure: bool, } fn latest_version() -> u32 { 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<()> { // Parse the options to the sprout executable. let options = SproutOptions::parse().context("unable to parse options")?; - // Load the configuration of sprout. - // At this point, the configuration has been validated and the specified - // version is checked to ensure compatibility. - let config = config::loader::load(&options)?; + // If --autoconfigure is specified, we use a stub configuration. + let mut config = if options.autoconfigure { + info!("autoconfiguration enabled, configuration file will be ignored"); + RootConfiguration::default() + } else { + // Load the configuration of sprout. + // At this point, the configuration has been validated and the specified + // version is checked to ensure compatibility. + config::loader::load(&options)? + }; // Load the root context. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol. @@ -98,6 +108,31 @@ fn main() -> Result<()> { // Load all configured drivers. drivers::load(context.clone(), &config.drivers).context("unable to load drivers")?; + // If --autoconfigure is specified or the loaded configuration has autoconfigure enabled, + // trigger the autoconfiguration mechanism. + if context.root().options().autoconfigure || config.defaults.autoconfigure { + 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/options.rs b/src/options.rs index 3f73eb2..cf02bba 100644 --- a/src/options.rs +++ b/src/options.rs @@ -11,6 +11,8 @@ const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml"; /// The parsed options of sprout. #[derive(Debug)] pub struct SproutOptions { + /// Configures Sprout automatically based on the environment. + pub autoconfigure: bool, /// Path to a configuration file to load. pub config: String, /// Entry to boot without showing the boot menu. @@ -25,6 +27,7 @@ pub struct SproutOptions { impl Default for SproutOptions { fn default() -> Self { Self { + autoconfigure: false, config: DEFAULT_CONFIG_PATH.to_string(), boot: None, force_menu: false, @@ -86,6 +89,11 @@ impl OptionsRepresentable for SproutOptions { for (key, value) in options { match key.as_str() { + "autoconfigure" => { + // Enable autoconfiguration. + result.autoconfigure = true; + } + "config" => { // The configuration file to load. result.config = value.context("--config option requires a value")?; 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)]