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