roxy_loader_utils/
build_image.rs1use 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
21pub 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
28pub 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 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 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 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
95pub 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}