Skip to main content

roxy_loader_utils/
build_image.rs

1//! Helpers for producing bootable disk images for `roxy-loader`.
2//!
3//! The functions in this module are intended for host-side build tools that
4//! need to prepare a disk image containing the loader and a kernel binary.
5
6#![allow(unused_mut)]
7
8use std::{
9    fs::File,
10    io::{self, Seek, SeekFrom},
11    path::{Path, PathBuf},
12};
13
14use anyhow::Result;
15use cargo_artifact_dependency::ArtifactDependencyBuilder;
16use fatfs::{FileSystem, FormatVolumeOptions, FsOptions};
17
18use crate::utils::cargo_target_dir;
19
20const ROXY_LOADER_ARTIFACT_VERSION: &str = "0.2.2";
21
22/// Builds a bootable disk image for a kernel artifact.
23pub fn build_image(kernel_binary: PathBuf) -> Result<PathBuf> {
24    let image_path = default_image_path()?;
25    build_image_from_paths(&image_path, &roxyloader_artifact()?, &kernel_binary)?;
26    Ok(image_path)
27}
28
29/// Builds a bootable disk image from explicit paths.
30///
31/// Use this when you want full control over the image path, loader path, or
32/// kernel path.
33///
34/// # Examples
35///
36/// ```
37/// use roxy_loader_utils::build_image::build_image_from_paths;
38///
39/// let temp = std::env::temp_dir().join(format!(
40///     "roxy-loader-doc-{}",
41///     std::process::id()
42/// ));
43/// std::fs::create_dir_all(&temp)?;
44///
45/// let image = temp.join("image.img");
46/// let loader = temp.join("loader.efi");
47/// let kernel = temp.join("kernel.bin");
48///
49/// std::fs::write(&loader, b"loader")?;
50/// std::fs::write(&kernel, b"kernel")?;
51///
52/// build_image_from_paths(&image, &loader, &kernel)?;
53///
54/// assert!(image.exists());
55///
56/// std::fs::remove_dir_all(&temp)?;
57/// # Ok::<(), Box<dyn std::error::Error>>(())
58/// ```
59pub fn build_image_from_paths(
60    image_path: &Path,
61    roxyloader_artifact: &Path,
62    kernel_binary: &Path,
63) -> Result<()> {
64    const IMAGE_SIZE: u64 = 64 * 1024 * 1024;
65
66    let mut image = open_image(image_path)?;
67
68    // truncate
69    image.set_len(IMAGE_SIZE)?;
70
71    fatfs::format_volume(&mut image, FormatVolumeOptions::new())?;
72
73    image.seek(SeekFrom::Start(0))?;
74
75    let fs = FileSystem::new(image, FsOptions::new())?;
76
77    let root_dir = fs.root_dir();
78    root_dir.create_dir("EFI")?;
79    let efi_dir = root_dir.open_dir("EFI")?;
80    efi_dir.create_dir("BOOT")?;
81    let boot_dir = efi_dir.open_dir("BOOT")?;
82
83    // Copies roxyloader artifact
84    let mut src = File::open(roxyloader_artifact)?;
85    let mut dst = boot_dir.create_file("BOOTX64.EFI")?;
86    io::copy(&mut src, &mut dst)?;
87
88    // Installs kernel binary
89    let mut dst = fs.root_dir().create_file("KERNEL")?;
90    let mut src = File::open(kernel_binary)?;
91    io::copy(&mut src, &mut dst)?;
92
93    Ok(())
94}
95
96/// Returns the default output path used by [`build_image`].
97pub fn default_image_path() -> Result<PathBuf> {
98    const IMAGE_NAME: &str = "image.img";
99    Ok(cargo_target_dir()?.join(IMAGE_NAME))
100}
101
102fn open_image(path: &Path) -> Result<File> {
103    Ok(File::options()
104        .read(true)
105        .write(true)
106        .create(true)
107        .truncate(true)
108        .open(path)?)
109}
110
111fn roxyloader_artifact() -> Result<PathBuf> {
112    let mut builder = ArtifactDependencyBuilder::default()
113        .crate_name("roxy-loader")
114        .version(ROXY_LOADER_ARTIFACT_VERSION)
115        .target("x86_64-unknown-uefi");
116
117    #[cfg(feature = "local-dev")]
118    {
119        use workspace_root::get_workspace_root;
120        builder = builder.path(get_workspace_root());
121    }
122
123    Ok(builder.build()?.resolve()?)
124}
125
126#[cfg(test)]
127mod tests {
128    use workspace_root::get_workspace_root;
129
130    use super::*;
131    use std::{
132        io::Read,
133        sync::atomic::{AtomicU64, Ordering},
134        time::{SystemTime, UNIX_EPOCH},
135    };
136
137    fn test_temp_dir() -> Result<std::path::PathBuf> {
138        static COUNTER: AtomicU64 = AtomicU64::new(0);
139
140        let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
141        let dir = std::env::temp_dir().join(format!(
142            "roxy-loader-test-{}-{}",
143            std::process::id(),
144            SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos() + unique as u128
145        ));
146
147        std::fs::create_dir_all(&dir)?;
148        Ok(dir)
149    }
150
151    #[test]
152    fn roxyloader_artifact_version_matches_package_version() -> Result<()> {
153        let metadata = cargo_metadata::MetadataCommand::new()
154            .manifest_path(get_workspace_root().join("Cargo.toml"))
155            .exec()?;
156        let package = metadata
157            .packages
158            .iter()
159            .find(|package| package.name == "roxy-loader")
160            .expect("workspace should contain the roxy-loader package");
161
162        assert_eq!(
163            ROXY_LOADER_ARTIFACT_VERSION,
164            package.version.to_string(),
165            "update ROXY_LOADER_ARTIFACT_VERSION when bumping roxy-loader"
166        );
167
168        Ok(())
169    }
170
171    #[test]
172    fn roxyloader_artifact_resolves_to_valid_file() -> Result<()> {
173        let artifact_path = roxyloader_artifact()?;
174        let metadata = std::fs::metadata(&artifact_path)?;
175
176        assert!(
177            metadata.is_file(),
178            "roxy-loader artifact path should be a file: {}",
179            artifact_path.display()
180        );
181        assert!(
182            metadata.len() > 0,
183            "roxy-loader artifact should not be empty: {}",
184            artifact_path.display()
185        );
186
187        Ok(())
188    }
189
190    #[test]
191    fn build_image_contains_loader_and_kernel_payloads() -> Result<()> {
192        let dir = test_temp_dir()?;
193        let image_path = dir.join("image.img");
194        let loader_path = dir.join("loader.efi");
195        let kernel_path = dir.join("kernel.bin");
196
197        std::fs::write(&loader_path, b"loader-bytes")?;
198        std::fs::write(&kernel_path, b"kernel-bytes")?;
199
200        build_image_from_paths(&image_path, &loader_path, &kernel_path)?;
201
202        let image = File::options().read(true).write(true).open(&image_path)?;
203        let fs = FileSystem::new(image, FsOptions::new())?;
204
205        let root = fs.root_dir();
206        let efi_dir = root.open_dir("EFI")?;
207        let boot_dir = efi_dir.open_dir("BOOT")?;
208
209        let mut loader_file = boot_dir.open_file("BOOTX64.EFI")?;
210        let mut loader_bytes = Vec::new();
211        loader_file.read_to_end(&mut loader_bytes)?;
212        assert_eq!(loader_bytes, b"loader-bytes");
213
214        let mut kernel_file = root.open_file("KERNEL")?;
215        let mut kernel_bytes = Vec::new();
216        kernel_file.read_to_end(&mut kernel_bytes)?;
217        assert_eq!(kernel_bytes, b"kernel-bytes");
218
219        std::fs::remove_dir_all(&dir)?;
220        Ok(())
221    }
222
223    #[test]
224    fn build_image_truncates_existing_image_contents() -> Result<()> {
225        let dir = test_temp_dir()?;
226        let image_path = dir.join("image.img");
227        let loader_path = dir.join("loader.efi");
228        let kernel_path = dir.join("kernel.bin");
229
230        std::fs::write(&loader_path, b"a")?;
231        std::fs::write(&kernel_path, b"b")?;
232        std::fs::write(&image_path, b"stale-bytes")?;
233
234        build_image_from_paths(&image_path, &loader_path, &kernel_path)?;
235
236        let metadata = std::fs::metadata(&image_path)?;
237        assert_eq!(metadata.len(), 64 * 1024 * 1024);
238
239        std::fs::remove_dir_all(&dir)?;
240        Ok(())
241    }
242}