1use crate::bootloader::Bootloader;
2use crate::config::{BootloaderKind, Config, ImageFormat, RunnerKind};
3#[cfg(feature = "cargo-metadata")]
4use crate::config::ConfigLoader;
5use crate::core::context::Context;
6use crate::core::error::{Error, Result};
7use crate::image::ImageBuilder;
8use crate::runner::io::IoHandler;
9use crate::runner::{RunResult, Runner};
10use std::path::PathBuf;
11
12pub struct ImageRunnerBuilder {
14 config: Option<Config>,
15 workspace_root: Option<PathBuf>,
16 executable: Option<PathBuf>,
17 bootloader: Option<Box<dyn Bootloader>>,
18 image_builder: Option<Box<dyn ImageBuilder>>,
19 runner: Option<Box<dyn Runner>>,
20 cli_extra_args: Vec<String>,
21 io_handler: Option<Box<dyn IoHandler>>,
22}
23
24impl ImageRunnerBuilder {
25 pub fn new() -> Self {
27 Self {
28 config: None,
29 workspace_root: None,
30 executable: None,
31 bootloader: None,
32 image_builder: None,
33 runner: None,
34 cli_extra_args: Vec::new(),
35 io_handler: None,
36 }
37 }
38
39 pub fn with_config(mut self, config: Config) -> Self {
41 self.config = Some(config);
42 self
43 }
44
45 #[cfg(feature = "cargo-metadata")]
47 pub fn from_cargo_metadata(mut self) -> Result<Self> {
48 let (config, workspace_root) = ConfigLoader::new().load()?;
49 self.config = Some(config);
50 self.workspace_root = Some(workspace_root);
51 Ok(self)
52 }
53
54 #[cfg(feature = "cargo-metadata")]
56 pub fn from_config_file(mut self, path: impl Into<PathBuf>) -> Result<Self> {
57 let (config, workspace_root) = ConfigLoader::new().config_file(path).load()?;
58 self.config = Some(config);
59 self.workspace_root = Some(workspace_root);
60 Ok(self)
61 }
62
63 pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
65 self.executable = Some(path.into());
66 self
67 }
68
69 pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
71 self.workspace_root = Some(path.into());
72 self
73 }
74
75 pub fn extra_args(mut self, args: Vec<String>) -> Self {
77 self.cli_extra_args = args;
78 self
79 }
80
81 pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
85 self.bootloader = Some(Box::new(bootloader));
86 self
87 }
88
89 #[cfg(feature = "limine")]
91 pub fn limine(mut self) -> Self {
92 self.bootloader = Some(Box::new(crate::bootloader::limine::LimineBootloader::new()));
93 self
94 }
95
96 pub fn grub(mut self) -> Self {
98 self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
99 self
100 }
101
102 pub fn no_bootloader(mut self) -> Self {
104 self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
105 self
106 }
107
108 pub fn image_builder<I: ImageBuilder + 'static>(mut self, builder: I) -> Self {
112 self.image_builder = Some(Box::new(builder));
113 self
114 }
115
116 #[cfg(feature = "iso")]
118 pub fn iso_image(mut self) -> Self {
119 self.image_builder = Some(Box::new(crate::image::iso::IsoImageBuilder::new()));
120 self
121 }
122
123 #[cfg(feature = "fat")]
125 pub fn fat_image(mut self) -> Self {
126 self.image_builder = Some(Box::new(crate::image::fat::FatImageBuilder::new()));
127 self
128 }
129
130 pub fn directory_output(mut self) -> Self {
132 self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
133 self
134 }
135
136 pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
140 self.runner = Some(Box::new(runner));
141 self
142 }
143
144 #[cfg(feature = "qemu")]
146 pub fn qemu(mut self) -> Self {
147 self.runner = Some(Box::new(crate::runner::qemu::QemuRunner::new()));
148 self
149 }
150
151 pub fn io_handler<H: IoHandler + 'static>(mut self, handler: H) -> Self {
158 self.io_handler = Some(Box::new(handler));
159 self
160 }
161
162 pub fn build(self) -> Result<ImageRunner> {
166 let config = self.config.ok_or_else(|| Error::config("no configuration provided"))?;
167
168 let workspace_root = self.workspace_root.ok_or_else(|| {
169 Error::config("workspace root not set (call from_cargo_metadata or workspace_root)")
170 })?;
171
172 let executable = self.executable.ok_or_else(|| {
173 Error::config("executable not set (call executable or get from CLI args)")
174 })?;
175
176 let bootloader = if let Some(bl) = self.bootloader {
178 bl
179 } else {
180 create_bootloader_from_config(&config)?
181 };
182
183 let image_builder = if let Some(ib) = self.image_builder {
185 ib
186 } else {
187 create_image_builder_from_config(&config)?
188 };
189
190 let runner = if let Some(r) = self.runner {
192 r
193 } else {
194 create_runner_from_config(&config)?
195 };
196
197 Ok(ImageRunner {
198 config,
199 workspace_root,
200 executable,
201 bootloader,
202 image_builder,
203 runner,
204 cli_extra_args: self.cli_extra_args,
205 io_handler: self.io_handler,
206 })
207 }
208
209 pub fn run(self) -> Result<()> {
211 let runner = self.build()?;
212 runner.run()
213 }
214
215 pub fn run_with_result(self) -> Result<RunResult> {
217 let runner = self.build()?;
218 runner.run_with_result()
219 }
220}
221
222impl Default for ImageRunnerBuilder {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228pub struct ImageRunner {
230 config: Config,
231 workspace_root: PathBuf,
232 executable: PathBuf,
233 bootloader: Box<dyn Bootloader>,
234 image_builder: Box<dyn ImageBuilder>,
235 runner: Box<dyn Runner>,
236 cli_extra_args: Vec<String>,
237 io_handler: Option<Box<dyn IoHandler>>,
238}
239
240impl ImageRunner {
241 pub fn build_image(&self) -> Result<PathBuf> {
245 let mut ctx = Context::new(
247 self.config.clone(),
248 self.workspace_root.clone(),
249 self.executable.clone(),
250 )?;
251 ctx.cli_extra_args = self.cli_extra_args.clone();
252 ctx.env_extra_args = crate::config::env::get_extra_qemu_args();
253
254 ctx.template_vars.insert(
256 "ARGS".to_string(),
257 ctx.cli_extra_args.join(" "),
258 );
259
260 self.bootloader.validate_config(&ctx)?;
262 self.image_builder.validate_boot_type(&ctx)?;
263
264 if ctx.config.verbose {
266 println!("Preparing bootloader: {}", self.bootloader.name());
267 }
268 let bootloader_files = self.bootloader.prepare(&ctx)?;
269
270 let config_files = self.bootloader.config_files(&ctx)?;
272 let mut all_files = Vec::new();
273
274 all_files.extend(bootloader_files.bios_files);
276 all_files.extend(bootloader_files.uefi_files);
277 all_files.extend(bootloader_files.system_files);
278
279 for config_file in config_files {
281 if config_file.needs_template_processing {
282 let content = std::fs::read_to_string(&config_file.source)?;
284 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
285
286 let temp_path = ctx.output_dir.join("processed_config");
288 std::fs::create_dir_all(&temp_path)?;
289 let processed_file = temp_path.join(
290 config_file
291 .source
292 .file_name()
293 .ok_or_else(|| Error::config("invalid config file path"))?,
294 );
295 std::fs::write(&processed_file, processed)?;
296
297 all_files.push(crate::bootloader::FileEntry::new(
298 processed_file,
299 config_file.dest,
300 ));
301 } else {
302 all_files.push(crate::bootloader::FileEntry::new(
303 config_file.source,
304 config_file.dest,
305 ));
306 }
307 }
308
309 all_files.extend(collect_extra_files(&ctx)?);
311
312 if ctx.config.verbose {
314 println!("Building image: {}", self.image_builder.name());
315 }
316 let image_path = self.image_builder.build(&ctx, &all_files)?;
317
318 Ok(image_path)
319 }
320
321 pub fn run(self) -> Result<()> {
326 let result = self.run_with_result()?;
327
328 if result.timed_out {
332 return Err(Error::runner("test timed out"));
333 }
334
335 if !result.success {
336 return Err(Error::runner(format!(
337 "Execution failed with exit code: {}",
338 result.exit_code
339 )));
340 }
341
342 Ok(())
343 }
344
345 pub fn run_with_result(mut self) -> Result<RunResult> {
351 let mut ctx = Context::new(self.config, self.workspace_root, self.executable)?;
353 ctx.cli_extra_args = self.cli_extra_args;
354 ctx.env_extra_args = crate::config::env::get_extra_qemu_args();
355
356 ctx.template_vars.insert(
358 "ARGS".to_string(),
359 ctx.cli_extra_args.join(" "),
360 );
361
362 self.bootloader.validate_config(&ctx)?;
364 self.image_builder.validate_boot_type(&ctx)?;
365 self.runner.validate(&ctx)?;
366
367 if ctx.config.verbose {
369 println!("Preparing bootloader: {}", self.bootloader.name());
370 }
371 let bootloader_files = self.bootloader.prepare(&ctx)?;
372
373 let config_files = self.bootloader.config_files(&ctx)?;
375 let mut all_files = Vec::new();
376
377 all_files.extend(bootloader_files.bios_files);
379 all_files.extend(bootloader_files.uefi_files);
380 all_files.extend(bootloader_files.system_files);
381
382 for config_file in config_files {
384 if config_file.needs_template_processing {
385 let content = std::fs::read_to_string(&config_file.source)?;
386 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
387
388 let temp_path = ctx.output_dir.join("processed_config");
389 std::fs::create_dir_all(&temp_path)?;
390 let processed_file = temp_path.join(
391 config_file
392 .source
393 .file_name()
394 .ok_or_else(|| Error::config("invalid config file path"))?,
395 );
396 std::fs::write(&processed_file, processed)?;
397
398 all_files.push(crate::bootloader::FileEntry::new(
399 processed_file,
400 config_file.dest,
401 ));
402 } else {
403 all_files.push(crate::bootloader::FileEntry::new(
404 config_file.source,
405 config_file.dest,
406 ));
407 }
408 }
409
410 all_files.extend(collect_extra_files(&ctx)?);
412
413 if ctx.config.verbose {
415 println!("Building image: {}", self.image_builder.name());
416 }
417 let image_path = self.image_builder.build(&ctx, &all_files)?;
418
419 if ctx.config.verbose {
421 println!("Running with: {}", self.runner.name());
422 }
423
424 let mut result = if let Some(ref mut handler) = self.io_handler {
425 self.runner.run_with_io(&ctx, &image_path, handler.as_mut())?
426 } else {
427 self.runner.run(&ctx, &image_path)?
428 };
429
430 if let Some(handler) = self.io_handler {
432 if let Some(captured_io) = handler.finish() {
433 let serial_str = String::from_utf8_lossy(&captured_io.serial).into_owned();
434 result = result.with_serial(serial_str);
435 }
436 }
437
438 Ok(result)
439 }
440}
441
442fn collect_extra_files(ctx: &Context) -> Result<Vec<crate::bootloader::FileEntry>> {
446 let mut files = Vec::new();
447 for (dest, src) in &ctx.config.extra_files {
448 let source_path = ctx.workspace_root.join(src);
449 if !source_path.exists() {
450 return Err(Error::config(format!(
451 "extra file not found: {} (resolved to {})",
452 src,
453 source_path.display()
454 )));
455 }
456 let dest_path = PathBuf::from(dest.strip_prefix('/').unwrap_or(dest));
459 files.push(crate::bootloader::FileEntry::new(
460 source_path,
461 dest_path,
462 ));
463 }
464 Ok(files)
465}
466
467fn create_bootloader_from_config(config: &Config) -> Result<Box<dyn Bootloader>> {
471 match config.bootloader.kind {
472 #[cfg(feature = "limine")]
473 BootloaderKind::Limine => Ok(Box::new(crate::bootloader::limine::LimineBootloader::new())),
474
475 #[cfg(not(feature = "limine"))]
476 BootloaderKind::Limine => Err(Error::feature_not_enabled("limine")),
477
478 BootloaderKind::Grub => Ok(Box::new(crate::bootloader::grub::GrubBootloader::new())),
479
480 BootloaderKind::None => Ok(Box::new(crate::bootloader::none::NoneBootloader::new())),
481 }
482}
483
484fn create_image_builder_from_config(config: &Config) -> Result<Box<dyn ImageBuilder>> {
486 match config.image.format {
487 #[cfg(feature = "iso")]
488 ImageFormat::Iso => Ok(Box::new(crate::image::iso::IsoImageBuilder::new())),
489
490 #[cfg(not(feature = "iso"))]
491 ImageFormat::Iso => Err(Error::feature_not_enabled("iso")),
492
493 #[cfg(feature = "fat")]
494 ImageFormat::Fat => Ok(Box::new(crate::image::fat::FatImageBuilder::new())),
495
496 #[cfg(not(feature = "fat"))]
497 ImageFormat::Fat => Err(Error::feature_not_enabled("fat")),
498
499 ImageFormat::Directory => Ok(Box::new(crate::image::directory::DirectoryBuilder::new())),
500 }
501}
502
503fn create_runner_from_config(config: &Config) -> Result<Box<dyn Runner>> {
505 match config.runner.kind {
506 #[cfg(feature = "qemu")]
507 RunnerKind::Qemu => Ok(Box::new(crate::runner::qemu::QemuRunner::new())),
508
509 #[cfg(not(feature = "qemu"))]
510 RunnerKind::Qemu => Err(Error::feature_not_enabled("qemu")),
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn test_builder_error_missing_config() {
520 let result = ImageRunnerBuilder::new()
521 .workspace_root("/tmp")
522 .executable("/tmp/kernel")
523 .build();
524 let err = result.err().expect("should fail");
525 assert!(err.to_string().contains("no configuration"));
526 }
527
528 #[test]
529 fn test_builder_error_missing_workspace_root() {
530 let result = ImageRunnerBuilder::new()
531 .with_config(Config::default())
532 .executable("/tmp/kernel")
533 .build();
534 let err = result.err().expect("should fail");
535 assert!(err.to_string().contains("workspace root"));
536 }
537
538 #[test]
539 fn test_builder_error_missing_executable() {
540 let result = ImageRunnerBuilder::new()
541 .with_config(Config::default())
542 .workspace_root("/tmp")
543 .build();
544 let err = result.err().expect("should fail");
545 assert!(err.to_string().contains("executable"));
546 }
547
548 #[test]
549 fn test_builder_with_none_bootloader_and_directory() {
550 let dir = tempfile::tempdir().unwrap();
551 let exe = dir.path().join("kernel");
552 std::fs::write(&exe, b"fake").unwrap();
553
554 let result = ImageRunnerBuilder::new()
556 .with_config(Config::default())
557 .workspace_root(dir.path())
558 .executable(&exe)
559 .build();
560 assert!(result.is_ok());
561 }
562
563 #[test]
564 fn test_builder_explicit_components() {
565 let dir = tempfile::tempdir().unwrap();
566 let exe = dir.path().join("kernel");
567 std::fs::write(&exe, b"fake").unwrap();
568
569 let result = ImageRunnerBuilder::new()
570 .with_config(Config::default())
571 .workspace_root(dir.path())
572 .executable(&exe)
573 .no_bootloader()
574 .directory_output()
575 .build();
576 assert!(result.is_ok());
577 }
578}