mirror of
https://github.com/edera-dev/sprout.git
synced 2025-12-19 15:20:17 +00:00
document ( by hand :( ) all of the code of sprout
This commit is contained in:
@@ -19,53 +19,88 @@ 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, Default, Clone)]
|
||||||
pub struct EderaConfiguration {
|
pub struct EderaConfiguration {
|
||||||
|
/// The path to the Xen hypervisor EFI image.
|
||||||
pub xen: String,
|
pub xen: String,
|
||||||
|
/// The path to the kernel to boot for dom0.
|
||||||
pub kernel: String,
|
pub kernel: String,
|
||||||
|
/// The path to the initrd to load for dom0.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub initrd: Option<String>,
|
pub initrd: Option<String>,
|
||||||
|
/// The options to pass to the kernel.
|
||||||
#[serde(default, rename = "kernel-options")]
|
#[serde(default, rename = "kernel-options")]
|
||||||
pub kernel_options: Vec<String>,
|
pub kernel_options: Vec<String>,
|
||||||
|
/// The options to pass to the Xen hypervisor.
|
||||||
#[serde(default, rename = "xen-options")]
|
#[serde(default, rename = "xen-options")]
|
||||||
pub xen_options: Vec<String>,
|
pub xen_options: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`.
|
||||||
fn build_xen_config(configuration: &EderaConfiguration) -> String {
|
fn build_xen_config(configuration: &EderaConfiguration) -> String {
|
||||||
|
// xen config file format is ini-like
|
||||||
[
|
[
|
||||||
|
// global section
|
||||||
"[global]".to_string(),
|
"[global]".to_string(),
|
||||||
|
// default configuration section
|
||||||
"default=sprout".to_string(),
|
"default=sprout".to_string(),
|
||||||
|
// configuration section for sprout
|
||||||
"[sprout]".to_string(),
|
"[sprout]".to_string(),
|
||||||
|
// xen options
|
||||||
format!("options={}", configuration.xen_options.join(" ")),
|
format!("options={}", configuration.xen_options.join(" ")),
|
||||||
|
// kernel options, stub replaces the kernel path
|
||||||
|
// the kernel is provided via media loader
|
||||||
format!("kernel=stub {}", configuration.kernel_options.join(" ")),
|
format!("kernel=stub {}", configuration.kernel_options.join(" ")),
|
||||||
"".to_string(), // required or else the last line will be ignored
|
// required or else the last line will be ignored
|
||||||
|
"".to_string(),
|
||||||
]
|
]
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a media loader for some `text` with the vendor `guid`.
|
||||||
|
/// `what` should indicate some identifying value for error messages
|
||||||
|
/// like `config` or `kernel`.
|
||||||
|
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
|
||||||
fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result<MediaLoaderHandle> {
|
fn register_media_loader_text(guid: Guid, what: &str, text: String) -> Result<MediaLoaderHandle> {
|
||||||
MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice())
|
MediaLoaderHandle::register(guid, text.as_bytes().to_vec().into_boxed_slice())
|
||||||
.context(format!("unable to register {} media loader", what)) /* */
|
.context(format!("unable to register {} media loader", what)) /* */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a media loader for the file `path` with the vendor `guid`.
|
||||||
|
/// `what` should indicate some identifying value for error messages
|
||||||
|
/// like `config` or `kernel`.
|
||||||
|
/// Provides a [MediaLoaderHandle] that can be used to unregister the media loader.
|
||||||
fn register_media_loader_file(
|
fn register_media_loader_file(
|
||||||
context: &Rc<SproutContext>,
|
context: &Rc<SproutContext>,
|
||||||
guid: Guid,
|
guid: Guid,
|
||||||
what: &str,
|
what: &str,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<MediaLoaderHandle> {
|
) -> Result<MediaLoaderHandle> {
|
||||||
|
// Stamp the path to the file.
|
||||||
let path = context.stamp(path);
|
let path = context.stamp(path);
|
||||||
|
// Read the file contents.
|
||||||
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path)
|
let content = utils::read_file_contents(context.root().loaded_image_path()?, &path)
|
||||||
.context(format!("unable to read {} file", what))?;
|
.context(format!("unable to read {} file", what))?;
|
||||||
|
// Register the media loader.
|
||||||
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
|
let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice())
|
||||||
.context(format!("unable to register {} media loader", what))?;
|
.context(format!("unable to register {} media loader", what))?;
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the edera action which will boot the Edera hypervisor with the specified
|
||||||
|
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
|
||||||
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
|
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
|
||||||
|
// Build the Xen config file content for this configuration.
|
||||||
let config = build_xen_config(configuration);
|
let config = build_xen_config(configuration);
|
||||||
|
|
||||||
|
// Register the media loader for the config.
|
||||||
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
|
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
|
||||||
.context("unable to register config media loader")?;
|
.context("unable to register config media loader")?;
|
||||||
|
|
||||||
|
// Register the media loaders for the kernel.
|
||||||
let kernel = register_media_loader_file(
|
let kernel = register_media_loader_file(
|
||||||
&context,
|
&context,
|
||||||
XEN_EFI_KERNEL_MEDIA_GUID,
|
XEN_EFI_KERNEL_MEDIA_GUID,
|
||||||
@@ -74,8 +109,10 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
|
|||||||
)
|
)
|
||||||
.context("unable to register kernel media loader")?;
|
.context("unable to register kernel media loader")?;
|
||||||
|
|
||||||
|
// Create a vector of media loaders to unregister on error.
|
||||||
let mut media_loaders = vec![config, kernel];
|
let mut media_loaders = vec![config, kernel];
|
||||||
|
|
||||||
|
// Register the initrd if it is provided.
|
||||||
if let Some(ref initrd) = configuration.initrd {
|
if let Some(ref initrd) = configuration.initrd {
|
||||||
let initrd =
|
let initrd =
|
||||||
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
|
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
|
||||||
@@ -83,6 +120,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
|
|||||||
media_loaders.push(initrd);
|
media_loaders.push(initrd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chainload to the Xen EFI stub.
|
||||||
let result = actions::chainload::chainload(
|
let result = actions::chainload::chainload(
|
||||||
context.clone(),
|
context.clone(),
|
||||||
&ChainloadConfiguration {
|
&ChainloadConfiguration {
|
||||||
@@ -93,6 +131,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
|
|||||||
)
|
)
|
||||||
.context("unable to chainload to xen");
|
.context("unable to chainload to xen");
|
||||||
|
|
||||||
|
// Unregister the media loaders on error.
|
||||||
for media_loader in media_loaders {
|
for media_loader in media_loaders {
|
||||||
if let Err(error) = media_loader.unregister() {
|
if let Err(error) = media_loader.unregister() {
|
||||||
error!("unable to unregister media loader: {}", error);
|
error!("unable to unregister media loader: {}", error);
|
||||||
|
|||||||
@@ -12,25 +12,39 @@ use std::time::Duration;
|
|||||||
use uefi::boot::ScopedProtocol;
|
use uefi::boot::ScopedProtocol;
|
||||||
use uefi::proto::console::gop::GraphicsOutput;
|
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, Default, Clone)]
|
||||||
pub struct SplashConfiguration {
|
pub struct SplashConfiguration {
|
||||||
|
/// The path to the image to display.
|
||||||
|
/// Currently, only PNG images are supported.
|
||||||
pub image: String,
|
pub image: String,
|
||||||
|
/// The time to display the splash image without interruption, in seconds.
|
||||||
|
/// The default value is `0` which will display the image and let everything
|
||||||
|
/// continue.
|
||||||
#[serde(default = "default_splash_time")]
|
#[serde(default = "default_splash_time")]
|
||||||
pub time: u32,
|
pub time: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_splash_time() -> u32 {
|
fn default_splash_time() -> u32 {
|
||||||
0
|
DEFAULT_SPLASH_TIME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acquire the [GraphicsOutput]. We will find the first graphics output only.
|
||||||
fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> {
|
fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> {
|
||||||
|
// Grab the handle for the graphics output protocol.
|
||||||
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
|
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
|
||||||
.context("unable to get graphics output")?;
|
.context("unable to get graphics output")?;
|
||||||
|
// Open the graphics output protocol exclusively.
|
||||||
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)
|
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)
|
||||||
.context("unable to open graphics output")
|
.context("unable to open graphics output")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Produces a [Rect] that fits the `image` inside the specified `frame`.
|
||||||
|
/// The output [Rect] should be used to resize the image.
|
||||||
fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
|
fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
|
||||||
|
// Convert the image dimensions to a [Rect].
|
||||||
let input = Rect {
|
let input = Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -38,9 +52,13 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
|
|||||||
height: image.height(),
|
height: image.height(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate the ratio of the image dimensions.
|
||||||
let input_ratio = input.width as f32 / input.height as f32;
|
let input_ratio = input.width as f32 / input.height as f32;
|
||||||
|
|
||||||
|
// Calculate the ratio of the frame dimensions.
|
||||||
let frame_ratio = frame.width as f32 / frame.height as f32;
|
let frame_ratio = frame.width as f32 / frame.height as f32;
|
||||||
|
|
||||||
|
// Create [Rect] to store the output dimensions.
|
||||||
let mut output = Rect {
|
let mut output = Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -63,24 +81,38 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resize the input `image` to fit the `frame`.
|
||||||
fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
|
fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||||
let image = image.to_rgba8();
|
let image = image.to_rgba8();
|
||||||
resize(&image, frame.width, frame.height, FilterType::Lanczos3)
|
resize(&image, frame.width, frame.height, FilterType::Lanczos3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw the `image` on the screen using [GraphicsOutput].
|
||||||
fn draw(image: DynamicImage) -> Result<()> {
|
fn draw(image: DynamicImage) -> Result<()> {
|
||||||
|
// Acquire the [GraphicsOutput] protocol.
|
||||||
let mut gop = setup_graphics()?;
|
let mut gop = setup_graphics()?;
|
||||||
|
|
||||||
|
// Acquire the current screen size.
|
||||||
let (width, height) = gop.current_mode_info().resolution();
|
let (width, height) = gop.current_mode_info().resolution();
|
||||||
|
|
||||||
|
// Create a display frame.
|
||||||
let display_frame = Rect {
|
let display_frame = Rect {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: width as _,
|
width: width as _,
|
||||||
height: height as _,
|
height: height as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fit the image to the display frame.
|
||||||
let fit = fit_to_frame(&image, display_frame);
|
let fit = fit_to_frame(&image, display_frame);
|
||||||
|
|
||||||
|
// Resize the image to fit the display frame.
|
||||||
let image = resize_to_fit(&image, fit);
|
let image = resize_to_fit(&image, fit);
|
||||||
|
|
||||||
|
// Create a framebuffer to draw the image on.
|
||||||
let mut framebuffer = Framebuffer::new(width, height);
|
let mut framebuffer = Framebuffer::new(width, height);
|
||||||
|
|
||||||
|
// Iterate over the pixels in the image and put them on the framebuffer.
|
||||||
for (x, y, pixel) in image.enumerate_pixels() {
|
for (x, y, pixel) in image.enumerate_pixels() {
|
||||||
let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else {
|
let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -90,17 +122,27 @@ fn draw(image: DynamicImage) -> Result<()> {
|
|||||||
fb.blue = pixel[2];
|
fb.blue = pixel[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blit the framebuffer to the screen.
|
||||||
framebuffer.blit(&mut gop)?;
|
framebuffer.blit(&mut gop)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the splash action with the specified `configuration` inside the provided `context`.
|
||||||
pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> {
|
pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> {
|
||||||
|
// Stamp the image path value.
|
||||||
let image = context.stamp(&configuration.image);
|
let image = context.stamp(&configuration.image);
|
||||||
|
// Read the image contents.
|
||||||
let image = read_file_contents(context.root().loaded_image_path()?, &image)?;
|
let image = read_file_contents(context.root().loaded_image_path()?, &image)?;
|
||||||
|
// Decode the image as a PNG.
|
||||||
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
|
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
|
||||||
.decode()
|
.decode()
|
||||||
.context("unable to decode splash image")?;
|
.context("unable to decode splash image")?;
|
||||||
|
// Draw the image on the screen.
|
||||||
draw(image)?;
|
draw(image)?;
|
||||||
|
|
||||||
|
// Sleep for the specified time.
|
||||||
std::thread::sleep(Duration::from_secs(configuration.time as u64));
|
std::thread::sleep(Duration::from_secs(configuration.time as u64));
|
||||||
|
|
||||||
|
// Return control to sprout.
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,36 +5,49 @@ use std::ops::Deref;
|
|||||||
use toml::Value;
|
use toml::Value;
|
||||||
use uefi::proto::device_path::LoadedImageDevicePath;
|
use uefi::proto::device_path::LoadedImageDevicePath;
|
||||||
|
|
||||||
|
/// Loads the raw configuration from the sprout.toml file as data.
|
||||||
fn load_raw_config() -> Result<Vec<u8>> {
|
fn load_raw_config() -> Result<Vec<u8>> {
|
||||||
|
// Open the LoadedImageDevicePath protocol to get the path to the current image.
|
||||||
let current_image_device_path_protocol =
|
let current_image_device_path_protocol =
|
||||||
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle())
|
uefi::boot::open_protocol_exclusive::<LoadedImageDevicePath>(uefi::boot::image_handle())
|
||||||
.context("unable to get loaded image device path")?;
|
.context("unable to get loaded image device path")?;
|
||||||
|
// Acquire the device path as a boxed device path.
|
||||||
let path = current_image_device_path_protocol.deref().to_boxed();
|
let path = current_image_device_path_protocol.deref().to_boxed();
|
||||||
|
// Read the contents of the sprout.toml file.
|
||||||
let content = utils::read_file_contents(&path, "sprout.toml")
|
let content = utils::read_file_contents(&path, "sprout.toml")
|
||||||
.context("unable to read sprout.toml file")?;
|
.context("unable to read sprout.toml file")?;
|
||||||
|
// Return the contents of the sprout.toml file.
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads the [RootConfiguration] for Sprout.
|
||||||
pub fn load() -> Result<RootConfiguration> {
|
pub fn load() -> Result<RootConfiguration> {
|
||||||
|
// Load the raw configuration from the sprout.toml file.
|
||||||
let content = load_raw_config()?;
|
let content = load_raw_config()?;
|
||||||
|
// Parse the raw configuration into a toml::Value which can represent any TOML file.
|
||||||
let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?;
|
let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?;
|
||||||
|
|
||||||
|
// Check the version of the configuration without parsing the full configuration.
|
||||||
let version = value
|
let version = value
|
||||||
.get("version")
|
.get("version")
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| Value::Integer(latest_version() as i64));
|
.unwrap_or_else(|| Value::Integer(latest_version() as i64));
|
||||||
|
|
||||||
|
// Parse the version into an u32.
|
||||||
let version: u32 = version
|
let version: u32 = version
|
||||||
.try_into()
|
.try_into()
|
||||||
.context("unable to get configuration version")?;
|
.context("unable to get configuration version")?;
|
||||||
|
|
||||||
|
// Check if the version is supported.
|
||||||
if version != latest_version() {
|
if version != latest_version() {
|
||||||
bail!("unsupported configuration version: {}", version);
|
bail!("unsupported configuration version: {}", version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the version is supported, parse the full configuration.
|
||||||
let config: RootConfiguration = value
|
let config: RootConfiguration = value
|
||||||
.try_into()
|
.try_into()
|
||||||
.context("unable to parse sprout.toml file")?;
|
.context("unable to parse sprout.toml file")?;
|
||||||
|
|
||||||
|
// Return the parsed configuration.
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,37 @@ use std::collections::{BTreeMap, BTreeSet};
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use uefi::proto::device_path::DevicePath;
|
use uefi::proto::device_path::DevicePath;
|
||||||
|
|
||||||
|
/// Declares a root context for Sprout.
|
||||||
|
/// This contains data that needs to be shared across Sprout.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct RootContext {
|
pub struct RootContext {
|
||||||
|
/// The actions that are available in Sprout.
|
||||||
actions: BTreeMap<String, ActionDeclaration>,
|
actions: BTreeMap<String, ActionDeclaration>,
|
||||||
|
/// The device path of the loaded Sprout image.
|
||||||
loaded_image_path: Option<Box<DevicePath>>,
|
loaded_image_path: Option<Box<DevicePath>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RootContext {
|
impl RootContext {
|
||||||
|
/// Creates a new root context with the `loaded_image_device_path` which will be stored
|
||||||
|
/// in the context for easy access.
|
||||||
pub fn new(loaded_image_device_path: Box<DevicePath>) -> Self {
|
pub fn new(loaded_image_device_path: Box<DevicePath>) -> Self {
|
||||||
RootContext {
|
Self {
|
||||||
actions: BTreeMap::new(),
|
actions: BTreeMap::new(),
|
||||||
loaded_image_path: Some(loaded_image_device_path),
|
loaded_image_path: Some(loaded_image_device_path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the actions configured inside Sprout.
|
||||||
pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> {
|
pub fn actions(&self) -> &BTreeMap<String, ActionDeclaration> {
|
||||||
&self.actions
|
&self.actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the actions configured inside Sprout mutably for modification.
|
||||||
pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> {
|
pub fn actions_mut(&mut self) -> &mut BTreeMap<String, ActionDeclaration> {
|
||||||
&mut self.actions
|
&mut self.actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the device path of the loaded Sprout image.
|
||||||
pub fn loaded_image_path(&self) -> Result<&DevicePath> {
|
pub fn loaded_image_path(&self) -> Result<&DevicePath> {
|
||||||
self.loaded_image_path
|
self.loaded_image_path
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -34,6 +43,12 @@ impl RootContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A context of Sprout. This is passed around different parts of Sprout and represents
|
||||||
|
/// a [RootContext] which is data that is shared globally, and [SproutContext] which works
|
||||||
|
/// sort of like a tree of values. You can cheaply clone a [SproutContext] and modify it with
|
||||||
|
/// new values, which override the values of contexts above it.
|
||||||
|
///
|
||||||
|
/// This is a core part of the value mechanism in Sprout which makes templating possible.
|
||||||
pub struct SproutContext {
|
pub struct SproutContext {
|
||||||
root: Rc<RootContext>,
|
root: Rc<RootContext>,
|
||||||
parent: Option<Rc<SproutContext>>,
|
parent: Option<Rc<SproutContext>>,
|
||||||
@@ -41,6 +56,7 @@ pub struct SproutContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SproutContext {
|
impl SproutContext {
|
||||||
|
/// Create a new [SproutContext] using `root` as the root context.
|
||||||
pub fn new(root: RootContext) -> Self {
|
pub fn new(root: RootContext) -> Self {
|
||||||
Self {
|
Self {
|
||||||
root: Rc::new(root),
|
root: Rc::new(root),
|
||||||
@@ -49,10 +65,13 @@ impl SproutContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the root context of this context.
|
||||||
pub fn root(&self) -> &RootContext {
|
pub fn root(&self) -> &RootContext {
|
||||||
self.root.as_ref()
|
self.root.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<str>) -> Option<&String> {
|
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
|
||||||
self.values.get(key.as_ref()).or_else(|| {
|
self.values.get(key.as_ref()).or_else(|| {
|
||||||
self.parent
|
self.parent
|
||||||
@@ -61,6 +80,8 @@ impl SproutContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all keys that are present in this context or its parents.
|
||||||
|
/// This is useful for iterating over all keys in a context.
|
||||||
pub fn all_keys(&self) -> Vec<String> {
|
pub fn all_keys(&self) -> Vec<String> {
|
||||||
let mut keys = BTreeSet::new();
|
let mut keys = BTreeSet::new();
|
||||||
|
|
||||||
@@ -74,6 +95,8 @@ impl SproutContext {
|
|||||||
keys.into_iter().collect()
|
keys.into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all values that are present in this context or its parents.
|
||||||
|
/// This is useful for iterating over all values in a context.
|
||||||
pub fn all_values(&self) -> BTreeMap<String, String> {
|
pub fn all_values(&self) -> BTreeMap<String, String> {
|
||||||
let mut values = BTreeMap::new();
|
let mut values = BTreeMap::new();
|
||||||
for key in self.all_keys() {
|
for key in self.all_keys() {
|
||||||
@@ -82,17 +105,24 @@ impl SproutContext {
|
|||||||
values
|
values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the value `key` to the value specified by `value` in this context.
|
||||||
|
/// If the parent context has this key, this will override that key.
|
||||||
pub fn set(&mut self, key: impl AsRef<str>, value: impl ToString) {
|
pub fn set(&mut self, key: impl AsRef<str>, value: impl ToString) {
|
||||||
self.values
|
self.values
|
||||||
.insert(key.as_ref().to_string(), value.to_string());
|
.insert(key.as_ref().to_string(), value.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts all the specified `values` into this context.
|
||||||
|
/// These values will take precedence over its parent context.
|
||||||
pub fn insert(&mut self, values: &BTreeMap<String, String>) {
|
pub fn insert(&mut self, values: &BTreeMap<String, String>) {
|
||||||
for (key, value) in values {
|
for (key, value) in values {
|
||||||
self.values.insert(key.clone(), value.clone());
|
self.values.insert(key.clone(), value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forks this context as an owned [SproutContext]. This makes it possible
|
||||||
|
/// to cheaply modify a context without cloning the parent context map.
|
||||||
|
/// The parent of the returned context is [self].
|
||||||
pub fn fork(self: &Rc<SproutContext>) -> Self {
|
pub fn fork(self: &Rc<SproutContext>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
root: self.root.clone(),
|
root: self.root.clone(),
|
||||||
@@ -101,11 +131,19 @@ impl SproutContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Freezes this context into a [Rc] which makes it possible to cheaply clone
|
||||||
|
/// and makes it less easy to modify a context. This can be used to pass the context
|
||||||
|
/// to various other parts of Sprout and ensure it won't be modified. Instead, once
|
||||||
|
/// a context is frozen, it should be [self.fork]'d to be modified.
|
||||||
pub fn freeze(self) -> Rc<SproutContext> {
|
pub fn freeze(self) -> Rc<SproutContext> {
|
||||||
Rc::new(self)
|
Rc::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finalizes a context by producing a context with no parent that contains all the values
|
||||||
|
/// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no
|
||||||
|
/// inheritance with other [SproutContext]s. It will still contain a [RootContext] however.
|
||||||
pub fn finalize(&self) -> SproutContext {
|
pub fn finalize(&self) -> SproutContext {
|
||||||
|
// Collect all the values from the context and its parents.
|
||||||
let mut current_values = self.all_values();
|
let mut current_values = self.all_values();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -114,16 +152,21 @@ impl SproutContext {
|
|||||||
for (key, value) in ¤t_values {
|
for (key, value) in ¤t_values {
|
||||||
let (changed, result) = Self::stamp_values(¤t_values, value);
|
let (changed, result) = Self::stamp_values(¤t_values, value);
|
||||||
if changed {
|
if changed {
|
||||||
|
// If the value changed, we need to re-stamp it.
|
||||||
did_change = true;
|
did_change = true;
|
||||||
}
|
}
|
||||||
|
// Insert the new value into the value map.
|
||||||
values.insert(key.clone(), result);
|
values.insert(key.clone(), result);
|
||||||
}
|
}
|
||||||
current_values = values;
|
current_values = values;
|
||||||
|
|
||||||
|
// If the values did not change, we can stop.
|
||||||
if !did_change {
|
if !did_change {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Produce the final context.
|
||||||
Self {
|
Self {
|
||||||
root: self.root.clone(),
|
root: self.root.clone(),
|
||||||
parent: None,
|
parent: None,
|
||||||
@@ -131,6 +174,8 @@ impl SproutContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stamps the `text` value with the specified `values` map. The returned value indicates
|
||||||
|
/// whether the `text` has been changed and the value that was stamped and changed.
|
||||||
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
|
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
|
||||||
let mut result = text.as_ref().to_string();
|
let mut result = text.as_ref().to_string();
|
||||||
let mut did_change = false;
|
let mut did_change = false;
|
||||||
@@ -144,6 +189,9 @@ impl SproutContext {
|
|||||||
(did_change, result)
|
(did_change, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stamps the input `text` with all the values in this [SproutContext] and it's parents.
|
||||||
|
/// For example, if this context contains {"a":"b"}, and the text "hello\\$a", it will produce
|
||||||
|
/// "hello\\b" as an output string.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,70 +11,109 @@ use uefi::fs::{FileSystem, Path};
|
|||||||
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
||||||
use uefi::proto::media::fs::SimpleFileSystem;
|
use uefi::proto::media::fs::SimpleFileSystem;
|
||||||
|
|
||||||
|
/// BLS entry parser.
|
||||||
mod entry;
|
mod entry;
|
||||||
|
|
||||||
|
/// The default path to the BLS entries directory.
|
||||||
|
const BLS_TEMPLATE_PATH: &str = "\\loader\\entries";
|
||||||
|
|
||||||
|
/// 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, Default, Clone)]
|
||||||
pub struct BlsConfiguration {
|
pub struct BlsConfiguration {
|
||||||
|
/// The entry to use for as a template.
|
||||||
pub entry: EntryDeclaration,
|
pub entry: EntryDeclaration,
|
||||||
|
/// The path to the BLS entries directory.
|
||||||
#[serde(default = "default_bls_path")]
|
#[serde(default = "default_bls_path")]
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_bls_path() -> String {
|
fn default_bls_path() -> String {
|
||||||
"\\loader\\entries".to_string()
|
BLS_TEMPLATE_PATH.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
pub fn generate(
|
pub fn generate(
|
||||||
context: Rc<SproutContext>,
|
context: Rc<SproutContext>,
|
||||||
bls: &BlsConfiguration,
|
bls: &BlsConfiguration,
|
||||||
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
|
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
// Stamp the path to the BLS entries directory.
|
||||||
let path = context.stamp(&bls.path);
|
let path = context.stamp(&bls.path);
|
||||||
|
|
||||||
|
// Resolve the path to the BLS entries directory.
|
||||||
let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
|
let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
|
||||||
.context("unable to resolve bls path")?;
|
.context("unable to resolve bls path")?;
|
||||||
|
|
||||||
|
// Open exclusive access to the BLS filesystem.
|
||||||
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
|
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
|
||||||
.context("unable to open bls filesystem")?;
|
.context("unable to open bls filesystem")?;
|
||||||
let mut fs = FileSystem::new(fs);
|
let mut fs = FileSystem::new(fs);
|
||||||
|
|
||||||
|
// Convert the subpath to the BLS entries directory to a string.
|
||||||
let sub_text_path = resolved
|
let sub_text_path = resolved
|
||||||
.sub_path
|
.sub_path
|
||||||
.to_string(DisplayOnly(false), AllowShortcuts(false))
|
.to_string(DisplayOnly(false), AllowShortcuts(false))
|
||||||
.context("unable to convert subpath to string")?;
|
.context("unable to convert subpath to string")?;
|
||||||
|
|
||||||
|
// Produce a path to the BLS entries directory.
|
||||||
let entries_path = Path::new(&sub_text_path);
|
let entries_path = Path::new(&sub_text_path);
|
||||||
|
|
||||||
|
// Read the BLS entries directory.
|
||||||
let entries_iter = fs
|
let entries_iter = fs
|
||||||
.read_dir(entries_path)
|
.read_dir(entries_path)
|
||||||
.context("unable to read bls entries")?;
|
.context("unable to read bls entries")?;
|
||||||
|
|
||||||
|
// For each entry in the BLS entries directory, parse the entry and add it to the list.
|
||||||
for entry in entries_iter {
|
for entry in entries_iter {
|
||||||
let entry = entry?;
|
// Unwrap the entry file info.
|
||||||
|
let entry = entry.context("unable to read bls item entry")?;
|
||||||
|
|
||||||
|
// Skip items that are not regular files.
|
||||||
if !entry.is_regular_file() {
|
if !entry.is_regular_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the file name of the filesystem item.
|
||||||
let name = entry.file_name().to_string();
|
let name = entry.file_name().to_string();
|
||||||
|
|
||||||
|
// Ignore files that are not .conf files.
|
||||||
if !name.ends_with(".conf") {
|
if !name.ends_with(".conf") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Produce the full path to the entry file.
|
||||||
let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str())
|
let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str())
|
||||||
.context("unable to construct full entry path")?;
|
.context("unable to construct full entry path")?;
|
||||||
let full_entry_path = Path::new(&full_entry_path);
|
let full_entry_path = Path::new(&full_entry_path);
|
||||||
|
|
||||||
|
// Read the entry file.
|
||||||
let content = fs
|
let content = fs
|
||||||
.read(full_entry_path)
|
.read(full_entry_path)
|
||||||
.context("unable to read bls file")?;
|
.context("unable to read bls file")?;
|
||||||
|
|
||||||
|
// Parse the entry file as a UTF-8 string.
|
||||||
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?;
|
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?;
|
||||||
|
|
||||||
|
// Parse the entry file as a BLS entry.
|
||||||
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?;
|
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?;
|
||||||
|
|
||||||
|
// Ignore entries that are not valid for Sprout.
|
||||||
if !entry.is_valid() {
|
if !entry.is_valid() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Produce a new sprout context for the entry with the extracted values.
|
||||||
let mut context = context.fork();
|
let mut context = context.fork();
|
||||||
context.set("title", entry.title().unwrap_or(name));
|
context.set("title", entry.title().unwrap_or(name));
|
||||||
context.set("chainload", entry.chainload_path().unwrap_or_default());
|
context.set("chainload", entry.chainload_path().unwrap_or_default());
|
||||||
context.set("options", entry.options().unwrap_or_default());
|
context.set("options", entry.options().unwrap_or_default());
|
||||||
context.set("initrd", entry.initrd_path().unwrap_or_default());
|
context.set("initrd", entry.initrd_path().unwrap_or_default());
|
||||||
|
|
||||||
|
// Add the entry to the list with a frozen context.
|
||||||
entries.push((context.freeze(), bls.entry.clone()));
|
entries.push((context.freeze(), bls.entry.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,82 @@
|
|||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Represents a parsed BLS entry.
|
||||||
|
/// Fields unrelated to Sprout are not included.
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct BlsEntry {
|
pub struct BlsEntry {
|
||||||
|
/// The title of the entry.
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
/// The options to pass to the entry.
|
||||||
pub options: Option<String>,
|
pub options: Option<String>,
|
||||||
|
/// The path to the linux kernel.
|
||||||
pub linux: Option<String>,
|
pub linux: Option<String>,
|
||||||
|
/// The path to the initrd.
|
||||||
pub initrd: Option<String>,
|
pub initrd: Option<String>,
|
||||||
|
/// The path to an EFI image.
|
||||||
pub efi: Option<String>,
|
pub efi: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parser for a BLS entry.
|
||||||
impl FromStr for BlsEntry {
|
impl FromStr for BlsEntry {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
|
/// Parses the `input` as a BLS entry file.
|
||||||
fn from_str(input: &str) -> Result<Self> {
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
// All the fields in a BLS entry we understand.
|
||||||
|
// Set all to None initially.
|
||||||
let mut title: Option<String> = None;
|
let mut title: Option<String> = None;
|
||||||
let mut options: Option<String> = None;
|
let mut options: Option<String> = None;
|
||||||
let mut linux: Option<String> = None;
|
let mut linux: Option<String> = None;
|
||||||
let mut initrd: Option<String> = None;
|
let mut initrd: Option<String> = None;
|
||||||
let mut efi: Option<String> = None;
|
let mut efi: Option<String> = None;
|
||||||
|
|
||||||
|
// Iterate over each line in the input and parse it.
|
||||||
for line in input.lines() {
|
for line in input.lines() {
|
||||||
|
// Trim the line.
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
|
|
||||||
|
// Split the line once by a space.
|
||||||
let Some((key, value)) = line.split_once(" ") else {
|
let Some((key, value)) = line.split_once(" ") else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Match the key to a field we understand.
|
||||||
match key {
|
match key {
|
||||||
|
// The title of the entry.
|
||||||
"title" => {
|
"title" => {
|
||||||
title = Some(value.trim().to_string());
|
title = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The options to pass to the entry.
|
||||||
"options" => {
|
"options" => {
|
||||||
options = Some(value.trim().to_string());
|
options = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The path to the linux kernel.
|
||||||
"linux" => {
|
"linux" => {
|
||||||
linux = Some(value.trim().to_string());
|
linux = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The path to the initrd.
|
||||||
"initrd" => {
|
"initrd" => {
|
||||||
initrd = Some(value.trim().to_string());
|
initrd = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The path to an EFI image.
|
||||||
"efi" => {
|
"efi" => {
|
||||||
efi = Some(value.trim().to_string());
|
efi = Some(value.trim().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore any other key.
|
||||||
_ => {
|
_ => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BlsEntry {
|
// Produce a BLS entry from the parsed fields.
|
||||||
|
Ok(Self {
|
||||||
title,
|
title,
|
||||||
options,
|
options,
|
||||||
linux,
|
linux,
|
||||||
@@ -64,10 +87,14 @@ impl FromStr for BlsEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl BlsEntry {
|
impl BlsEntry {
|
||||||
|
/// Checks if this BLS entry is something we can actually boot in Sprout.
|
||||||
pub fn is_valid(&self) -> bool {
|
pub fn is_valid(&self) -> bool {
|
||||||
self.linux.is_some() || self.efi.is_some()
|
self.linux.is_some() || self.efi.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the path to an EFI bootable image to boot, if any.
|
||||||
|
/// This prioritizes the linux field over efi.
|
||||||
|
/// It also converts / to \\ to match EFI path style.
|
||||||
pub fn chainload_path(&self) -> Option<String> {
|
pub fn chainload_path(&self) -> Option<String> {
|
||||||
self.linux
|
self.linux
|
||||||
.clone()
|
.clone()
|
||||||
@@ -75,16 +102,20 @@ impl BlsEntry {
|
|||||||
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
|
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the path to an initrd to pass to the kernel, if any.
|
||||||
|
/// It also converts / to \\ to match EFI path style.
|
||||||
pub fn initrd_path(&self) -> Option<String> {
|
pub fn initrd_path(&self) -> Option<String> {
|
||||||
self.initrd
|
self.initrd
|
||||||
.clone()
|
.clone()
|
||||||
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
|
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the options to pass to the kernel, if any.
|
||||||
pub fn options(&self) -> Option<String> {
|
pub fn options(&self) -> Option<String> {
|
||||||
self.options.clone()
|
self.options.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the title of the entry, if any.
|
||||||
pub fn title(&self) -> Option<String> {
|
pub fn title(&self) -> Option<String> {
|
||||||
self.title.clone()
|
self.title.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,37 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Matrix generator configuration.
|
||||||
|
/// The matrix generator produces multiple entries based
|
||||||
|
/// on input values multiplicatively.
|
||||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||||
pub struct MatrixConfiguration {
|
pub struct MatrixConfiguration {
|
||||||
|
/// The template entry to use for each generated entry.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub entry: EntryDeclaration,
|
pub entry: EntryDeclaration,
|
||||||
|
/// The values to use as the input for the matrix.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub values: BTreeMap<String, Vec<String>>,
|
pub values: BTreeMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds out multiple generations of `input` based on a matrix style.
|
||||||
|
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
|
||||||
|
/// It will produce:
|
||||||
|
/// x: a, y: c
|
||||||
|
/// x: a, y: d
|
||||||
|
/// x: b, y: c
|
||||||
|
/// x: b, y: d
|
||||||
fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
|
fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
|
||||||
|
// Convert the input into a vector of tuples.
|
||||||
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
|
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
|
||||||
|
|
||||||
|
// The result is a vector of maps.
|
||||||
let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()];
|
let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()];
|
||||||
|
|
||||||
for (key, values) in items {
|
for (key, values) in items {
|
||||||
let mut new_result = Vec::new();
|
let mut new_result = Vec::new();
|
||||||
|
|
||||||
|
// Produce all the combinations of the input values.
|
||||||
for combination in &result {
|
for combination in &result {
|
||||||
for value in &values {
|
for value in &values {
|
||||||
let mut new_combination = combination.clone();
|
let mut new_combination = combination.clone();
|
||||||
@@ -34,18 +50,23 @@ fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, S
|
|||||||
result.into_iter().filter(|item| !item.is_empty()).collect()
|
result.into_iter().filter(|item| !item.is_empty()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
|
||||||
pub fn generate(
|
pub fn generate(
|
||||||
context: Rc<SproutContext>,
|
context: Rc<SproutContext>,
|
||||||
matrix: &MatrixConfiguration,
|
matrix: &MatrixConfiguration,
|
||||||
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
|
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
|
||||||
|
// Produce all the combinations of the input values.
|
||||||
let combinations = build_matrix(&matrix.values);
|
let combinations = build_matrix(&matrix.values);
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
// For each combination, create a new context and entry.
|
||||||
for combination in combinations {
|
for combination in combinations {
|
||||||
let mut context = context.fork();
|
let mut context = context.fork();
|
||||||
|
// Insert the combination into the context.
|
||||||
context.insert(&combination);
|
context.insert(&combination);
|
||||||
let context = context.freeze();
|
let context = context.freeze();
|
||||||
|
|
||||||
|
// Stamp the entry title and actions from the template.
|
||||||
let mut entry = matrix.entry.clone();
|
let mut entry = matrix.entry.clone();
|
||||||
entry.title = context.stamp(&entry.title);
|
entry.title = context.stamp(&entry.title);
|
||||||
entry.actions = entry
|
entry.actions = entry
|
||||||
@@ -53,6 +74,7 @@ pub fn generate(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|action| context.stamp(action))
|
.map(|action| context.stamp(action))
|
||||||
.collect();
|
.collect();
|
||||||
|
// Push the entry into the list with the new context.
|
||||||
entries.push((context, entry));
|
entries.push((context, entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -10,15 +10,34 @@ use std::ops::Deref;
|
|||||||
use uefi::proto::device_path::LoadedImageDevicePath;
|
use uefi::proto::device_path::LoadedImageDevicePath;
|
||||||
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
||||||
|
|
||||||
|
/// actions: Code that can be configured and executed by Sprout.
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
|
|
||||||
|
/// config: Sprout configuration mechanism.
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
|
/// context: Stored values that can be cheaply forked and cloned.
|
||||||
pub mod context;
|
pub mod context;
|
||||||
|
|
||||||
|
/// drivers: EFI drivers to load and provide extra functionality.
|
||||||
pub mod drivers;
|
pub mod drivers;
|
||||||
|
|
||||||
|
/// entries: Boot menu entries that have a title and can execute actions.
|
||||||
pub mod entries;
|
pub mod entries;
|
||||||
|
|
||||||
|
/// extractors: Runtime code that can extract values into the Sprout context.
|
||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
|
|
||||||
|
/// generators: Runtime code that can generate entries with specific values.
|
||||||
pub mod generators;
|
pub mod generators;
|
||||||
|
|
||||||
|
/// phases: Hooks into specific parts of the boot process.
|
||||||
pub mod phases;
|
pub mod phases;
|
||||||
|
|
||||||
|
/// setup: Code that initializes the UEFI environment for Sprout.
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
|
|
||||||
|
/// utils: Utility functions that are used by other parts of Sprout.
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
/// The main entrypoint of sprout.
|
/// The main entrypoint of sprout.
|
||||||
|
|||||||
Reference in New Issue
Block a user