cargo_image_runner/core/
builder.rs1use crate::bootloader::Bootloader;
2use crate::config::{BootloaderKind, Config, ConfigLoader, ImageFormat, RunnerKind};
3use crate::core::context::Context;
4use crate::core::error::{Error, Result};
5use crate::image::ImageBuilder;
6use crate::runner::Runner;
7use std::path::PathBuf;
8
9pub struct ImageRunnerBuilder {
11 config: Option<Config>,
12 workspace_root: Option<PathBuf>,
13 executable: Option<PathBuf>,
14 bootloader: Option<Box<dyn Bootloader>>,
15 image_builder: Option<Box<dyn ImageBuilder>>,
16 runner: Option<Box<dyn Runner>>,
17}
18
19impl ImageRunnerBuilder {
20 pub fn new() -> Self {
22 Self {
23 config: None,
24 workspace_root: None,
25 executable: None,
26 bootloader: None,
27 image_builder: None,
28 runner: None,
29 }
30 }
31
32 pub fn with_config(mut self, config: Config) -> Self {
34 self.config = Some(config);
35 self
36 }
37
38 pub fn from_cargo_metadata(mut self) -> Result<Self> {
40 let (config, workspace_root) = ConfigLoader::new().load()?;
41 self.config = Some(config);
42 self.workspace_root = Some(workspace_root);
43 Ok(self)
44 }
45
46 pub fn from_config_file(mut self, path: impl Into<PathBuf>) -> Result<Self> {
48 let (config, workspace_root) = ConfigLoader::new().config_file(path).load()?;
49 self.config = Some(config);
50 self.workspace_root = Some(workspace_root);
51 Ok(self)
52 }
53
54 pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
56 self.executable = Some(path.into());
57 self
58 }
59
60 pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
62 self.workspace_root = Some(path.into());
63 self
64 }
65
66 pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
70 self.bootloader = Some(Box::new(bootloader));
71 self
72 }
73
74 #[cfg(feature = "limine")]
76 pub fn limine(mut self) -> Self {
77 self.bootloader = Some(Box::new(crate::bootloader::limine::LimineBootloader::new()));
78 self
79 }
80
81 pub fn grub(mut self) -> Self {
83 self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
84 self
85 }
86
87 pub fn no_bootloader(mut self) -> Self {
89 self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
90 self
91 }
92
93 pub fn image_builder<I: ImageBuilder + 'static>(mut self, builder: I) -> Self {
97 self.image_builder = Some(Box::new(builder));
98 self
99 }
100
101 #[cfg(feature = "iso")]
103 pub fn iso_image(mut self) -> Self {
104 self.image_builder = Some(Box::new(crate::image::iso::IsoImageBuilder::new()));
105 self
106 }
107
108 #[cfg(feature = "fat")]
110 pub fn fat_image(mut self) -> Self {
111 self.image_builder = Some(Box::new(crate::image::fat::FatImageBuilder::new()));
112 self
113 }
114
115 pub fn directory_output(mut self) -> Self {
117 self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
118 self
119 }
120
121 pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
125 self.runner = Some(Box::new(runner));
126 self
127 }
128
129 #[cfg(feature = "qemu")]
131 pub fn qemu(mut self) -> Self {
132 self.runner = Some(Box::new(crate::runner::qemu::QemuRunner::new()));
133 self
134 }
135
136 pub fn build(self) -> Result<ImageRunner> {
140 let config = self.config.ok_or_else(|| Error::config("no configuration provided"))?;
141
142 let workspace_root = self.workspace_root.ok_or_else(|| {
143 Error::config("workspace root not set (call from_cargo_metadata or workspace_root)")
144 })?;
145
146 let executable = self.executable.ok_or_else(|| {
147 Error::config("executable not set (call executable or get from CLI args)")
148 })?;
149
150 let bootloader = if let Some(bl) = self.bootloader {
152 bl
153 } else {
154 create_bootloader_from_config(&config)?
155 };
156
157 let image_builder = if let Some(ib) = self.image_builder {
159 ib
160 } else {
161 create_image_builder_from_config(&config)?
162 };
163
164 let runner = if let Some(r) = self.runner {
166 r
167 } else {
168 create_runner_from_config(&config)?
169 };
170
171 Ok(ImageRunner {
172 config,
173 workspace_root,
174 executable,
175 bootloader,
176 image_builder,
177 runner,
178 })
179 }
180
181 pub fn run(self) -> Result<()> {
183 let runner = self.build()?;
184 runner.run()
185 }
186}
187
188impl Default for ImageRunnerBuilder {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194pub struct ImageRunner {
196 config: Config,
197 workspace_root: PathBuf,
198 executable: PathBuf,
199 bootloader: Box<dyn Bootloader>,
200 image_builder: Box<dyn ImageBuilder>,
201 runner: Box<dyn Runner>,
202}
203
204impl ImageRunner {
205 pub fn build_image(&self) -> Result<PathBuf> {
209 let ctx = Context::new(
211 self.config.clone(),
212 self.workspace_root.clone(),
213 self.executable.clone(),
214 )?;
215
216 self.bootloader.validate_config(&ctx)?;
218 self.image_builder.validate_boot_type(&ctx)?;
219
220 if ctx.config.verbose {
222 println!("Preparing bootloader: {}", self.bootloader.name());
223 }
224 let bootloader_files = self.bootloader.prepare(&ctx)?;
225
226 let config_files = self.bootloader.config_files(&ctx)?;
228 let mut all_files = Vec::new();
229
230 all_files.extend(bootloader_files.bios_files);
232 all_files.extend(bootloader_files.uefi_files);
233 all_files.extend(bootloader_files.system_files);
234
235 for config_file in config_files {
237 if config_file.needs_template_processing {
238 let content = std::fs::read_to_string(&config_file.source)?;
240 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
241
242 let temp_path = ctx.output_dir.join("processed_config");
244 std::fs::create_dir_all(&temp_path)?;
245 let processed_file = temp_path.join(
246 config_file
247 .source
248 .file_name()
249 .ok_or_else(|| Error::config("invalid config file path"))?,
250 );
251 std::fs::write(&processed_file, processed)?;
252
253 all_files.push(crate::bootloader::FileEntry::new(
254 processed_file,
255 config_file.dest,
256 ));
257 } else {
258 all_files.push(crate::bootloader::FileEntry::new(
259 config_file.source,
260 config_file.dest,
261 ));
262 }
263 }
264
265 if ctx.config.verbose {
267 println!("Building image: {}", self.image_builder.name());
268 }
269 let image_path = self.image_builder.build(&ctx, &all_files)?;
270
271 Ok(image_path)
272 }
273
274 pub fn run(self) -> Result<()> {
276 let ctx = Context::new(self.config, self.workspace_root, self.executable)?;
278
279 self.bootloader.validate_config(&ctx)?;
281 self.image_builder.validate_boot_type(&ctx)?;
282 self.runner.validate(&ctx)?;
283
284 if ctx.config.verbose {
286 println!("Preparing bootloader: {}", self.bootloader.name());
287 }
288 let bootloader_files = self.bootloader.prepare(&ctx)?;
289
290 let config_files = self.bootloader.config_files(&ctx)?;
292 let mut all_files = Vec::new();
293
294 all_files.extend(bootloader_files.bios_files);
296 all_files.extend(bootloader_files.uefi_files);
297 all_files.extend(bootloader_files.system_files);
298
299 for config_file in config_files {
301 if config_file.needs_template_processing {
302 let content = std::fs::read_to_string(&config_file.source)?;
304 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
305
306 let temp_path = ctx.output_dir.join("processed_config");
308 std::fs::create_dir_all(&temp_path)?;
309 let processed_file = temp_path.join(
310 config_file
311 .source
312 .file_name()
313 .ok_or_else(|| Error::config("invalid config file path"))?,
314 );
315 std::fs::write(&processed_file, processed)?;
316
317 all_files.push(crate::bootloader::FileEntry::new(
318 processed_file,
319 config_file.dest,
320 ));
321 } else {
322 all_files.push(crate::bootloader::FileEntry::new(
323 config_file.source,
324 config_file.dest,
325 ));
326 }
327 }
328
329 if ctx.config.verbose {
331 println!("Building image: {}", self.image_builder.name());
332 }
333 let image_path = self.image_builder.build(&ctx, &all_files)?;
334
335 if ctx.config.verbose {
337 println!("Running with: {}", self.runner.name());
338 }
339 let result = self.runner.run(&ctx, &image_path)?;
340
341 if ctx.is_test {
343 if let Some(success_code) = ctx.test_success_exit_code() {
345 if result.exit_code == success_code {
346 if ctx.config.verbose {
347 println!("Test passed (exit code: {})", result.exit_code);
348 }
349 return Ok(());
350 } else {
351 return Err(Error::runner(format!(
352 "Test failed: expected exit code {}, got {}",
353 success_code, result.exit_code
354 )));
355 }
356 }
357 }
358
359 if !result.success {
360 return Err(Error::runner(format!(
361 "Execution failed with exit code: {}",
362 result.exit_code
363 )));
364 }
365
366 Ok(())
367 }
368}
369
370fn create_bootloader_from_config(config: &Config) -> Result<Box<dyn Bootloader>> {
374 match config.bootloader.kind {
375 #[cfg(feature = "limine")]
376 BootloaderKind::Limine => Ok(Box::new(crate::bootloader::limine::LimineBootloader::new())),
377
378 #[cfg(not(feature = "limine"))]
379 BootloaderKind::Limine => Err(Error::feature_not_enabled("limine")),
380
381 BootloaderKind::Grub => Ok(Box::new(crate::bootloader::grub::GrubBootloader::new())),
382
383 BootloaderKind::None => Ok(Box::new(crate::bootloader::none::NoneBootloader::new())),
384 }
385}
386
387fn create_image_builder_from_config(config: &Config) -> Result<Box<dyn ImageBuilder>> {
389 match config.image.format {
390 #[cfg(feature = "iso")]
391 ImageFormat::Iso => Ok(Box::new(crate::image::iso::IsoImageBuilder::new())),
392
393 #[cfg(not(feature = "iso"))]
394 ImageFormat::Iso => Err(Error::feature_not_enabled("iso")),
395
396 #[cfg(feature = "fat")]
397 ImageFormat::Fat => Ok(Box::new(crate::image::fat::FatImageBuilder::new())),
398
399 #[cfg(not(feature = "fat"))]
400 ImageFormat::Fat => Err(Error::feature_not_enabled("fat")),
401
402 ImageFormat::Directory => Ok(Box::new(crate::image::directory::DirectoryBuilder::new())),
403 }
404}
405
406fn create_runner_from_config(config: &Config) -> Result<Box<dyn Runner>> {
408 match config.runner.kind {
409 #[cfg(feature = "qemu")]
410 RunnerKind::Qemu => Ok(Box::new(crate::runner::qemu::QemuRunner::new())),
411
412 #[cfg(not(feature = "qemu"))]
413 RunnerKind::Qemu => Err(Error::feature_not_enabled("qemu")),
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_builder_error_missing_config() {
423 let result = ImageRunnerBuilder::new()
424 .workspace_root("/tmp")
425 .executable("/tmp/kernel")
426 .build();
427 let err = result.err().expect("should fail");
428 assert!(err.to_string().contains("no configuration"));
429 }
430
431 #[test]
432 fn test_builder_error_missing_workspace_root() {
433 let result = ImageRunnerBuilder::new()
434 .with_config(Config::default())
435 .executable("/tmp/kernel")
436 .build();
437 let err = result.err().expect("should fail");
438 assert!(err.to_string().contains("workspace root"));
439 }
440
441 #[test]
442 fn test_builder_error_missing_executable() {
443 let result = ImageRunnerBuilder::new()
444 .with_config(Config::default())
445 .workspace_root("/tmp")
446 .build();
447 let err = result.err().expect("should fail");
448 assert!(err.to_string().contains("executable"));
449 }
450
451 #[test]
452 fn test_builder_with_none_bootloader_and_directory() {
453 let dir = tempfile::tempdir().unwrap();
454 let exe = dir.path().join("kernel");
455 std::fs::write(&exe, b"fake").unwrap();
456
457 let result = ImageRunnerBuilder::new()
459 .with_config(Config::default())
460 .workspace_root(dir.path())
461 .executable(&exe)
462 .build();
463 assert!(result.is_ok());
464 }
465
466 #[test]
467 fn test_builder_explicit_components() {
468 let dir = tempfile::tempdir().unwrap();
469 let exe = dir.path().join("kernel");
470 std::fs::write(&exe, b"fake").unwrap();
471
472 let result = ImageRunnerBuilder::new()
473 .with_config(Config::default())
474 .workspace_root(dir.path())
475 .executable(&exe)
476 .no_bootloader()
477 .directory_output()
478 .build();
479 assert!(result.is_ok());
480 }
481}