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::Runner;
9use std::path::PathBuf;
10
11pub struct ImageRunnerBuilder {
13 config: Option<Config>,
14 workspace_root: Option<PathBuf>,
15 executable: Option<PathBuf>,
16 bootloader: Option<Box<dyn Bootloader>>,
17 image_builder: Option<Box<dyn ImageBuilder>>,
18 runner: Option<Box<dyn Runner>>,
19 cli_extra_args: Vec<String>,
20}
21
22impl ImageRunnerBuilder {
23 pub fn new() -> Self {
25 Self {
26 config: None,
27 workspace_root: None,
28 executable: None,
29 bootloader: None,
30 image_builder: None,
31 runner: None,
32 cli_extra_args: Vec::new(),
33 }
34 }
35
36 pub fn with_config(mut self, config: Config) -> Self {
38 self.config = Some(config);
39 self
40 }
41
42 #[cfg(feature = "cargo-metadata")]
44 pub fn from_cargo_metadata(mut self) -> Result<Self> {
45 let (config, workspace_root) = ConfigLoader::new().load()?;
46 self.config = Some(config);
47 self.workspace_root = Some(workspace_root);
48 Ok(self)
49 }
50
51 #[cfg(feature = "cargo-metadata")]
53 pub fn from_config_file(mut self, path: impl Into<PathBuf>) -> Result<Self> {
54 let (config, workspace_root) = ConfigLoader::new().config_file(path).load()?;
55 self.config = Some(config);
56 self.workspace_root = Some(workspace_root);
57 Ok(self)
58 }
59
60 pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
62 self.executable = Some(path.into());
63 self
64 }
65
66 pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
68 self.workspace_root = Some(path.into());
69 self
70 }
71
72 pub fn extra_args(mut self, args: Vec<String>) -> Self {
74 self.cli_extra_args = args;
75 self
76 }
77
78 pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
82 self.bootloader = Some(Box::new(bootloader));
83 self
84 }
85
86 #[cfg(feature = "limine")]
88 pub fn limine(mut self) -> Self {
89 self.bootloader = Some(Box::new(crate::bootloader::limine::LimineBootloader::new()));
90 self
91 }
92
93 pub fn grub(mut self) -> Self {
95 self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
96 self
97 }
98
99 pub fn no_bootloader(mut self) -> Self {
101 self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
102 self
103 }
104
105 pub fn image_builder<I: ImageBuilder + 'static>(mut self, builder: I) -> Self {
109 self.image_builder = Some(Box::new(builder));
110 self
111 }
112
113 #[cfg(feature = "iso")]
115 pub fn iso_image(mut self) -> Self {
116 self.image_builder = Some(Box::new(crate::image::iso::IsoImageBuilder::new()));
117 self
118 }
119
120 #[cfg(feature = "fat")]
122 pub fn fat_image(mut self) -> Self {
123 self.image_builder = Some(Box::new(crate::image::fat::FatImageBuilder::new()));
124 self
125 }
126
127 pub fn directory_output(mut self) -> Self {
129 self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
130 self
131 }
132
133 pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
137 self.runner = Some(Box::new(runner));
138 self
139 }
140
141 #[cfg(feature = "qemu")]
143 pub fn qemu(mut self) -> Self {
144 self.runner = Some(Box::new(crate::runner::qemu::QemuRunner::new()));
145 self
146 }
147
148 pub fn build(self) -> Result<ImageRunner> {
152 let config = self.config.ok_or_else(|| Error::config("no configuration provided"))?;
153
154 let workspace_root = self.workspace_root.ok_or_else(|| {
155 Error::config("workspace root not set (call from_cargo_metadata or workspace_root)")
156 })?;
157
158 let executable = self.executable.ok_or_else(|| {
159 Error::config("executable not set (call executable or get from CLI args)")
160 })?;
161
162 let bootloader = if let Some(bl) = self.bootloader {
164 bl
165 } else {
166 create_bootloader_from_config(&config)?
167 };
168
169 let image_builder = if let Some(ib) = self.image_builder {
171 ib
172 } else {
173 create_image_builder_from_config(&config)?
174 };
175
176 let runner = if let Some(r) = self.runner {
178 r
179 } else {
180 create_runner_from_config(&config)?
181 };
182
183 Ok(ImageRunner {
184 config,
185 workspace_root,
186 executable,
187 bootloader,
188 image_builder,
189 runner,
190 cli_extra_args: self.cli_extra_args,
191 })
192 }
193
194 pub fn run(self) -> Result<()> {
196 let runner = self.build()?;
197 runner.run()
198 }
199}
200
201impl Default for ImageRunnerBuilder {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207pub struct ImageRunner {
209 config: Config,
210 workspace_root: PathBuf,
211 executable: PathBuf,
212 bootloader: Box<dyn Bootloader>,
213 image_builder: Box<dyn ImageBuilder>,
214 runner: Box<dyn Runner>,
215 cli_extra_args: Vec<String>,
216}
217
218impl ImageRunner {
219 pub fn build_image(&self) -> Result<PathBuf> {
223 let mut ctx = Context::new(
225 self.config.clone(),
226 self.workspace_root.clone(),
227 self.executable.clone(),
228 )?;
229 ctx.cli_extra_args = self.cli_extra_args.clone();
230 ctx.env_extra_args = crate::config::env::get_extra_qemu_args();
231
232 ctx.template_vars.insert(
234 "ARGS".to_string(),
235 ctx.cli_extra_args.join(" "),
236 );
237
238 self.bootloader.validate_config(&ctx)?;
240 self.image_builder.validate_boot_type(&ctx)?;
241
242 if ctx.config.verbose {
244 println!("Preparing bootloader: {}", self.bootloader.name());
245 }
246 let bootloader_files = self.bootloader.prepare(&ctx)?;
247
248 let config_files = self.bootloader.config_files(&ctx)?;
250 let mut all_files = Vec::new();
251
252 all_files.extend(bootloader_files.bios_files);
254 all_files.extend(bootloader_files.uefi_files);
255 all_files.extend(bootloader_files.system_files);
256
257 for config_file in config_files {
259 if config_file.needs_template_processing {
260 let content = std::fs::read_to_string(&config_file.source)?;
262 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
263
264 let temp_path = ctx.output_dir.join("processed_config");
266 std::fs::create_dir_all(&temp_path)?;
267 let processed_file = temp_path.join(
268 config_file
269 .source
270 .file_name()
271 .ok_or_else(|| Error::config("invalid config file path"))?,
272 );
273 std::fs::write(&processed_file, processed)?;
274
275 all_files.push(crate::bootloader::FileEntry::new(
276 processed_file,
277 config_file.dest,
278 ));
279 } else {
280 all_files.push(crate::bootloader::FileEntry::new(
281 config_file.source,
282 config_file.dest,
283 ));
284 }
285 }
286
287 all_files.extend(collect_extra_files(&ctx)?);
289
290 if ctx.config.verbose {
292 println!("Building image: {}", self.image_builder.name());
293 }
294 let image_path = self.image_builder.build(&ctx, &all_files)?;
295
296 Ok(image_path)
297 }
298
299 pub fn run(self) -> Result<()> {
301 let mut ctx = Context::new(self.config, self.workspace_root, self.executable)?;
303 ctx.cli_extra_args = self.cli_extra_args;
304 ctx.env_extra_args = crate::config::env::get_extra_qemu_args();
305
306 ctx.template_vars.insert(
308 "ARGS".to_string(),
309 ctx.cli_extra_args.join(" "),
310 );
311
312 self.bootloader.validate_config(&ctx)?;
314 self.image_builder.validate_boot_type(&ctx)?;
315 self.runner.validate(&ctx)?;
316
317 if ctx.config.verbose {
319 println!("Preparing bootloader: {}", self.bootloader.name());
320 }
321 let bootloader_files = self.bootloader.prepare(&ctx)?;
322
323 let config_files = self.bootloader.config_files(&ctx)?;
325 let mut all_files = Vec::new();
326
327 all_files.extend(bootloader_files.bios_files);
329 all_files.extend(bootloader_files.uefi_files);
330 all_files.extend(bootloader_files.system_files);
331
332 for config_file in config_files {
334 if config_file.needs_template_processing {
335 let content = std::fs::read_to_string(&config_file.source)?;
337 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
338
339 let temp_path = ctx.output_dir.join("processed_config");
341 std::fs::create_dir_all(&temp_path)?;
342 let processed_file = temp_path.join(
343 config_file
344 .source
345 .file_name()
346 .ok_or_else(|| Error::config("invalid config file path"))?,
347 );
348 std::fs::write(&processed_file, processed)?;
349
350 all_files.push(crate::bootloader::FileEntry::new(
351 processed_file,
352 config_file.dest,
353 ));
354 } else {
355 all_files.push(crate::bootloader::FileEntry::new(
356 config_file.source,
357 config_file.dest,
358 ));
359 }
360 }
361
362 all_files.extend(collect_extra_files(&ctx)?);
364
365 if ctx.config.verbose {
367 println!("Building image: {}", self.image_builder.name());
368 }
369 let image_path = self.image_builder.build(&ctx, &all_files)?;
370
371 if ctx.config.verbose {
373 println!("Running with: {}", self.runner.name());
374 }
375 let result = self.runner.run(&ctx, &image_path)?;
376
377 if ctx.is_test {
379 if result.timed_out {
380 return Err(Error::runner("test timed out"));
381 }
382
383 if let Some(success_code) = ctx.test_success_exit_code() {
384 if result.exit_code == success_code {
385 return Ok(());
386 } else {
387 return Err(Error::runner(format!(
388 "Test failed: expected exit code {}, got {}",
389 success_code, result.exit_code
390 )));
391 }
392 }
393 }
394
395 if !result.success {
396 return Err(Error::runner(format!(
397 "Execution failed with exit code: {}",
398 result.exit_code
399 )));
400 }
401
402 Ok(())
403 }
404}
405
406fn collect_extra_files(ctx: &Context) -> Result<Vec<crate::bootloader::FileEntry>> {
410 let mut files = Vec::new();
411 for (dest, src) in &ctx.config.extra_files {
412 let source_path = ctx.workspace_root.join(src);
413 if !source_path.exists() {
414 return Err(Error::config(format!(
415 "extra file not found: {} (resolved to {})",
416 src,
417 source_path.display()
418 )));
419 }
420 let dest_path = PathBuf::from(dest.strip_prefix('/').unwrap_or(dest));
423 files.push(crate::bootloader::FileEntry::new(
424 source_path,
425 dest_path,
426 ));
427 }
428 Ok(files)
429}
430
431fn create_bootloader_from_config(config: &Config) -> Result<Box<dyn Bootloader>> {
435 match config.bootloader.kind {
436 #[cfg(feature = "limine")]
437 BootloaderKind::Limine => Ok(Box::new(crate::bootloader::limine::LimineBootloader::new())),
438
439 #[cfg(not(feature = "limine"))]
440 BootloaderKind::Limine => Err(Error::feature_not_enabled("limine")),
441
442 BootloaderKind::Grub => Ok(Box::new(crate::bootloader::grub::GrubBootloader::new())),
443
444 BootloaderKind::None => Ok(Box::new(crate::bootloader::none::NoneBootloader::new())),
445 }
446}
447
448fn create_image_builder_from_config(config: &Config) -> Result<Box<dyn ImageBuilder>> {
450 match config.image.format {
451 #[cfg(feature = "iso")]
452 ImageFormat::Iso => Ok(Box::new(crate::image::iso::IsoImageBuilder::new())),
453
454 #[cfg(not(feature = "iso"))]
455 ImageFormat::Iso => Err(Error::feature_not_enabled("iso")),
456
457 #[cfg(feature = "fat")]
458 ImageFormat::Fat => Ok(Box::new(crate::image::fat::FatImageBuilder::new())),
459
460 #[cfg(not(feature = "fat"))]
461 ImageFormat::Fat => Err(Error::feature_not_enabled("fat")),
462
463 ImageFormat::Directory => Ok(Box::new(crate::image::directory::DirectoryBuilder::new())),
464 }
465}
466
467fn create_runner_from_config(config: &Config) -> Result<Box<dyn Runner>> {
469 match config.runner.kind {
470 #[cfg(feature = "qemu")]
471 RunnerKind::Qemu => Ok(Box::new(crate::runner::qemu::QemuRunner::new())),
472
473 #[cfg(not(feature = "qemu"))]
474 RunnerKind::Qemu => Err(Error::feature_not_enabled("qemu")),
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_builder_error_missing_config() {
484 let result = ImageRunnerBuilder::new()
485 .workspace_root("/tmp")
486 .executable("/tmp/kernel")
487 .build();
488 let err = result.err().expect("should fail");
489 assert!(err.to_string().contains("no configuration"));
490 }
491
492 #[test]
493 fn test_builder_error_missing_workspace_root() {
494 let result = ImageRunnerBuilder::new()
495 .with_config(Config::default())
496 .executable("/tmp/kernel")
497 .build();
498 let err = result.err().expect("should fail");
499 assert!(err.to_string().contains("workspace root"));
500 }
501
502 #[test]
503 fn test_builder_error_missing_executable() {
504 let result = ImageRunnerBuilder::new()
505 .with_config(Config::default())
506 .workspace_root("/tmp")
507 .build();
508 let err = result.err().expect("should fail");
509 assert!(err.to_string().contains("executable"));
510 }
511
512 #[test]
513 fn test_builder_with_none_bootloader_and_directory() {
514 let dir = tempfile::tempdir().unwrap();
515 let exe = dir.path().join("kernel");
516 std::fs::write(&exe, b"fake").unwrap();
517
518 let result = ImageRunnerBuilder::new()
520 .with_config(Config::default())
521 .workspace_root(dir.path())
522 .executable(&exe)
523 .build();
524 assert!(result.is_ok());
525 }
526
527 #[test]
528 fn test_builder_explicit_components() {
529 let dir = tempfile::tempdir().unwrap();
530 let exe = dir.path().join("kernel");
531 std::fs::write(&exe, b"fake").unwrap();
532
533 let result = ImageRunnerBuilder::new()
534 .with_config(Config::default())
535 .workspace_root(dir.path())
536 .executable(&exe)
537 .no_bootloader()
538 .directory_output()
539 .build();
540 assert!(result.is_ok());
541 }
542}