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};
15
16use crate::utils::cargo_target_dir;
17
18const ROXY_LOADER_ARTIFACT_VERSION: &str = "0.2.1";
19
20pub 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
27pub 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 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 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 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
94pub 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}