fix(autoconfigure): apply the actions properly in the root

This commit is contained in:
2025-10-27 03:37:09 -04:00
parent facd2000a5
commit 1799419bfa
22 changed files with 79 additions and 38 deletions

View File

@@ -65,13 +65,8 @@ set -- "${@}" \
-drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \ -drive "if=pflash,file=${FINAL_DIR}/ovmf-boot.fd,format=raw,readonly=on" \
-device nvme,drive=disk1,serial=cafebabe -device nvme,drive=disk1,serial=cafebabe
if [ "${DISK_BOOT}" = "1" ]; then set -- "${@}" \
set -- "${@}" \ -drive "if=none,file=${FINAL_DIR}/sprout.img,format=raw,id=disk1,readonly=on"
-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 -- "${@}" -name "sprout ${TARGET_ARCH}" set -- "${@}" -name "sprout ${TARGET_ARCH}"

View File

@@ -13,6 +13,7 @@ COPY xen.efi /work/XEN.EFI
COPY xen.cfg /work/XEN.CFG COPY xen.cfg /work/XEN.CFG
COPY initramfs /work/INITRAMFS COPY initramfs /work/INITRAMFS
COPY edera-splash.png /work/EDERA-SPLASH.PNG COPY edera-splash.png /work/EDERA-SPLASH.PNG
COPY bls.conf /work/BLS.CONF
RUN truncate -s128MiB sprout.img && \ RUN truncate -s128MiB sprout.img && \
parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \ parted --script sprout.img mklabel gpt > /dev/null 2>&1 && \
parted --script sprout.img mkpart primary fat32 1MiB 100% > /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 && \ mkfs.vfat -F32 -n EFI sprout.img && \
mmd -i sprout.img ::/EFI && \ mmd -i sprout.img ::/EFI && \
mmd -i sprout.img ::/EFI/BOOT && \ 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 ${EFI_NAME}.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \ mcopy -i sprout.img KERNEL.EFI ::/EFI/BOOT/ && \
mcopy -i sprout.img SHELL.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 SPROUT.TOML ::/ && \
mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \ mcopy -i sprout.img EDERA-SPLASH.PNG ::/ && \
mcopy -i sprout.img INITRAMFS ::/ && \ mcopy -i sprout.img INITRAMFS ::/ && \
mcopy -i sprout.img BLS.CONF ::/LOADER/ENTRIES/ && \
mv sprout.img /sprout.img mv sprout.img /sprout.img
FROM scratch AS final FROM scratch AS final

View File

@@ -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/${SPROUT_CONFIG_NAME}.sprout.toml" "${FINAL_DIR}/sprout.toml"
cp "hack/dev/configs/xen.cfg" "${FINAL_DIR}/xen.cfg" 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/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" mkdir -p "${FINAL_DIR}/efi/EFI/BOOT"
cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI" cp "${FINAL_DIR}/sprout.efi" "${FINAL_DIR}/efi/EFI/BOOT/${EFI_NAME}.EFI"

View File

@@ -0,0 +1,4 @@
version = 1
[defaults]
autoconfigure = true

View File

@@ -0,0 +1,4 @@
title Boot Linux
linux /efi/boot/kernel.efi
options console=hvc0
initrd /initramfs

View File

@@ -19,7 +19,7 @@ pub mod splash;
/// that you can specify via other concepts. /// that you can specify via other concepts.
/// ///
/// Actions are the main work that Sprout gets done, like booting Linux. /// 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 { pub struct ActionDeclaration {
/// Chainload to another EFI application. /// Chainload to another EFI application.
/// This allows you to load any EFI application, either to boot an operating system /// This allows you to load any EFI application, either to boot an operating system

View File

@@ -10,7 +10,7 @@ use uefi::CString16;
use uefi::proto::loaded_image::LoadedImage; use uefi::proto::loaded_image::LoadedImage;
/// The configuration of the chainload action. /// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration { pub struct ChainloadConfiguration {
/// The path to the image to chainload. /// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable. /// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
@@ -99,10 +99,6 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
initrd_handle = Some(handle); 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. // Start the loaded image.
// This call might return, or it may pass full control to another image that will never return. // 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 // Capture the result to ensure we can return an error if the image fails to start, but only

View File

@@ -22,7 +22,7 @@ use crate::{
/// The configuration of the edera action which boots the Edera hypervisor. /// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack. /// 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. /// 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 { pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image. /// The path to the Xen hypervisor EFI image.
pub xen: String, pub xen: String,

View File

@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
/// The configuration of the print action. /// The configuration of the print action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PrintConfiguration { pub struct PrintConfiguration {
/// The text to print to the console. /// The text to print to the console.
#[serde(default)] #[serde(default)]

View File

@@ -15,7 +15,7 @@ use uefi::proto::console::gop::GraphicsOutput;
const DEFAULT_SPLASH_TIME: u32 = 0; const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action. /// The configuration of the splash action.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SplashConfiguration { pub struct SplashConfiguration {
/// The path to the image to display. /// The path to the image to display.
/// Currently, only PNG images are supported. /// Currently, only PNG images are supported.

View File

@@ -27,24 +27,30 @@ fn scan_for_bls(
let bls_entries_path = Path::new(cstr16!("\\loader\\entries")); let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration. // 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)) .to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")? .context("unable to convert device root to string")?
.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. // Whether we have a loader.conf file.
let has_loader_conf = filesystem let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path) .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. // Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem let has_entries_dir = filesystem
.try_exists(bls_entries_path) .read_dir(bls_entries_path)
.context("unable to check for BLS loader.conf file")?; .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. // 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. // 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); return Ok(false);
} }
@@ -55,7 +61,7 @@ fn scan_for_bls(
actions: vec![BLS_CHAINLOAD_ACTION.to_string()], actions: vec![BLS_CHAINLOAD_ACTION.to_string()],
..Default::default() ..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. // 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 { let chainload = ChainloadConfiguration {
path: "$chainload".to_string(), path: "$chainload".to_string(),
options: vec!["$options".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. // 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. // Scan the filesystem for BLS supported configurations.
// If we find any, we will add a BLS generator to the configuration, and then // 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. // at the end we will add the BLS actions to the configuration.
has_any_bls = has_any_bls let bls_found =
|| scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; 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. // If we had any BLS-supported filesystems, add the BLS actions to the configuration.

View File

@@ -18,7 +18,7 @@ pub const LATEST_VERSION: u32 = 1;
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10; pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
/// The Sprout configuration format. /// The Sprout configuration format.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct RootConfiguration { pub struct RootConfiguration {
/// The version of the configuration. This should always be declared /// The version of the configuration. This should always be declared
/// and be the latest version that is supported. If not specified, it is assumed /// 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. /// 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 { pub struct DefaultsConfiguration {
/// The entry to boot without showing the boot menu. /// The entry to boot without showing the boot menu.
/// If not specified, a boot menu is shown. /// If not specified, a boot menu is shown.

View File

@@ -83,6 +83,11 @@ impl SproutContext {
self.root.as_ref() 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. /// Retrieve the value specified by `key` from this context or its parents.
/// Returns `None` if the value is not found. /// Returns `None` if the value is not found.
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> { pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
@@ -235,4 +240,10 @@ impl SproutContext {
pub fn stamp(&self, text: impl AsRef<str>) -> String { pub fn stamp(&self, text: impl AsRef<str>) -> String {
Self::stamp_values(&self.all_values(), text.as_ref()).1 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<SproutContext>) -> Option<SproutContext> {
Rc::into_inner(self)
}
} }

View File

@@ -12,7 +12,7 @@ use uefi::proto::device_path::LoadedImageDevicePath;
/// Drivers allow extending the functionality of Sprout. /// Drivers allow extending the functionality of Sprout.
/// Drivers are loaded at runtime and can provide extra functionality like filesystem support. /// 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. /// 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 { pub struct DriverDeclaration {
/// The filesystem path to the driver. /// The filesystem path to the driver.
/// This file should be an EFI executable that can be located and executed. /// This file should be an EFI executable that can be located and executed.

View File

@@ -7,7 +7,7 @@ use std::rc::Rc;
/// ///
/// Entries are the user-facing concept of Sprout, making it possible /// Entries are the user-facing concept of Sprout, making it possible
/// to run a set of actions with a specific context. /// to run a set of actions with a specific context.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EntryDeclaration { pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu. /// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value. /// This is the pre-stamped value.

View File

@@ -10,7 +10,7 @@ pub mod filesystem_device_match;
/// Declares an extractor configuration. /// Declares an extractor configuration.
/// Extractors allow calculating values at runtime /// Extractors allow calculating values at runtime
/// using built-in sprout modules. /// using built-in sprout modules.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ExtractorDeclaration { pub struct ExtractorDeclaration {
/// The filesystem device match extractor. /// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns /// This extractor finds a filesystem using some search criteria and returns

View File

@@ -20,7 +20,7 @@ use uefi_raw::Status;
/// ///
/// This function only requires one of the criteria to match. /// This function only requires one of the criteria to match.
/// The fallback value can be used to provide a value if none is found. /// 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 { pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label. /// Matches a filesystem that has the specified label.
#[serde(default, rename = "has-label")] #[serde(default, rename = "has-label")]

View File

@@ -12,7 +12,7 @@ pub mod matrix;
/// Declares a generator configuration. /// Declares a generator configuration.
/// Generators allow generating entries at runtime based on a set of data. /// 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 { pub struct GeneratorDeclaration {
/// Matrix generator configuration. /// Matrix generator configuration.
/// Matrix allows you to specify multiple value-key values as arrays. /// Matrix allows you to specify multiple value-key values as arrays.

View File

@@ -20,7 +20,7 @@ const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator. /// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce /// The BLS uses the Bootloader Specification to produce
/// entries from an input template. /// entries from an input template.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct BlsConfiguration { pub struct BlsConfiguration {
/// The entry to use for as a template. /// The entry to use for as a template.
pub entry: EntryDeclaration, pub entry: EntryDeclaration,
@@ -86,7 +86,7 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
let name = entry.file_name().to_string(); let name = entry.file_name().to_string();
// Ignore files that are not .conf files. // Ignore files that are not .conf files.
if !name.ends_with(".conf") { if !name.to_lowercase().ends_with(".conf") {
continue; continue;
} }

View File

@@ -8,7 +8,7 @@ use std::rc::Rc;
/// Matrix generator configuration. /// Matrix generator configuration.
/// The matrix generator produces multiple entries based /// The matrix generator produces multiple entries based
/// on input values multiplicatively. /// on input values multiplicatively.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MatrixConfiguration { pub struct MatrixConfiguration {
/// The template entry to use for each generated entry. /// The template entry to use for each generated entry.
#[serde(default)] #[serde(default)]

View File

@@ -7,7 +7,7 @@ use crate::entries::BootableEntry;
use crate::options::SproutOptions; use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result}; use anyhow::{Context, Result, bail};
use log::info; use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
@@ -114,6 +114,25 @@ fn main() -> Result<()> {
autoconfigure::autoconfigure(&mut config).context("unable to 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. // Run all the extractors declared in the configuration.
let mut extracted = BTreeMap::new(); let mut extracted = BTreeMap::new();
for (name, extractor) in &config.extractors { for (name, extractor) in &config.extractors {

View File

@@ -7,7 +7,7 @@ use std::rc::Rc;
/// Configures the various phases of the boot process. /// Configures the various phases of the boot process.
/// This allows hooking various phases to run actions. /// This allows hooking various phases to run actions.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhasesConfiguration { pub struct PhasesConfiguration {
/// The early phase is run before drivers are loaded. /// The early phase is run before drivers are loaded.
#[serde(default)] #[serde(default)]
@@ -23,7 +23,7 @@ pub struct PhasesConfiguration {
/// Configures a single phase of the boot process. /// Configures a single phase of the boot process.
/// There can be multiple phase configurations that are /// There can be multiple phase configurations that are
/// executed sequentially. /// executed sequentially.
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct PhaseConfiguration { pub struct PhaseConfiguration {
/// The actions to run when the phase is executed. /// The actions to run when the phase is executed.
#[serde(default)] #[serde(default)]