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