1use 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 cli_extra_args: Vec<String>,
18}
19
20impl ImageRunnerBuilder {
21 pub fn new() -> Self {
23 Self {
24 config: None,
25 workspace_root: None,
26 executable: None,
27 bootloader: None,
28 image_builder: None,
29 runner: None,
30 cli_extra_args: Vec::new(),
31 }
32 }
33
34 pub fn with_config(mut self, config: Config) -> Self {
36 self.config = Some(config);
37 self
38 }
39
40 pub fn from_cargo_metadata(mut self) -> Result<Self> {
42 let (config, workspace_root) = ConfigLoader::new().load()?;
43 self.config = Some(config);
44 self.workspace_root = Some(workspace_root);
45 Ok(self)
46 }
47
48 pub fn from_config_file(mut self, path: impl Into<PathBuf>) -> Result<Self> {
50 let (config, workspace_root) = ConfigLoader::new().config_file(path).load()?;
51 self.config = Some(config);
52 self.workspace_root = Some(workspace_root);
53 Ok(self)
54 }
55
56 pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
58 self.executable = Some(path.into());
59 self
60 }
61
62 pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
64 self.workspace_root = Some(path.into());
65 self
66 }
67
68 pub fn extra_args(mut self, args: Vec<String>) -> Self {
70 self.cli_extra_args = args;
71 self
72 }
73
74 pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
78 self.bootloader = Some(Box::new(bootloader));
79 self
80 }
81
82 #[cfg(feature = "limine")]
84 pub fn limine(mut self) -> Self {
85 self.bootloader = Some(Box::new(crate::bootloader::limine::LimineBootloader::new()));
86 self
87 }
88
89 pub fn grub(mut self) -> Self {
91 self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
92 self
93 }
94
95 pub fn no_bootloader(mut self) -> Self {
97 self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
98 self
99 }
100
101 pub fn image_builder<I: ImageBuilder + 'static>(mut self, builder: I) -> Self {
105 self.image_builder = Some(Box::new(builder));
106 self
107 }
108
109 #[cfg(feature = "iso")]
111 pub fn iso_image(mut self) -> Self {
112 self.image_builder = Some(Box::new(crate::image::iso::IsoImageBuilder::new()));
113 self
114 }
115
116 #[cfg(feature = "fat")]
118 pub fn fat_image(mut self) -> Self {
119 self.image_builder = Some(Box::new(crate::image::fat::FatImageBuilder::new()));
120 self
121 }
122
123 pub fn directory_output(mut self) -> Self {
125 self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
126 self
127 }
128
129 pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
133 self.runner = Some(Box::new(runner));
134 self
135 }
136
137 #[cfg(feature = "qemu")]
139 pub fn qemu(mut self) -> Self {
140 self.runner = Some(Box::new(crate::runner::qemu::QemuRunner::new()));
141 self
142 }
143
144 pub fn build(self) -> Result<ImageRunner> {
148 let config = self.config.ok_or_else(|| Error::config("no configuration provided"))?;
149
150 let workspace_root = self.workspace_root.ok_or_else(|| {
151 Error::config("workspace root not set (call from_cargo_metadata or workspace_root)")
152 })?;
153
154 let executable = self.executable.ok_or_else(|| {
155 Error::config("executable not set (call executable or get from CLI args)")
156 })?;
157
158 let bootloader = if let Some(bl) = self.bootloader {
160 bl
161 } else {
162 create_bootloader_from_config(&config)?
163 };
164
165 let image_builder = if let Some(ib) = self.image_builder {
167 ib
168 } else {
169 create_image_builder_from_config(&config)?
170 };
171
172 let runner = if let Some(r) = self.runner {
174 r
175 } else {
176 create_runner_from_config(&config)?
177 };
178
179 Ok(ImageRunner {
180 config,
181 workspace_root,
182 executable,
183 bootloader,
184 image_builder,
185 runner,
186 cli_extra_args: self.cli_extra_args,
187 })
188 }
189
190 pub fn run(self) -> Result<()> {
192 let runner = self.build()?;
193 runner.run()
194 }
195}
196
197impl Default for ImageRunnerBuilder {
198 fn default() -> Self {
199 Self::new()
200 }
201}
202
203pub struct ImageRunner {
205 config: Config,
206 workspace_root: PathBuf,
207 executable: PathBuf,
208 bootloader: Box<dyn Bootloader>,
209 image_builder: Box<dyn ImageBuilder>,
210 runner: Box<dyn Runner>,
211 cli_extra_args: Vec<String>,
212}
213
214impl ImageRunner {
215 pub fn build_image(&self) -> Result<PathBuf> {
219 let mut ctx = Context::new(
221 self.config.clone(),
222 self.workspace_root.clone(),
223 self.executable.clone(),
224 )?;
225 ctx.cli_extra_args = self.cli_extra_args.clone();
226 ctx.env_extra_args = crate::config::env::get_extra_qemu_args();
227
228 ctx.template_vars.insert(
230 "ARGS".to_string(),
231 ctx.cli_extra_args.join(" "),
232 );
233
234 self.bootloader.validate_config(&ctx)?;
236 self.image_builder.validate_boot_type(&ctx)?;
237
238 if ctx.config.verbose {
240 println!("Preparing bootloader: {}", self.bootloader.name());
241 }
242 let bootloader_files = self.bootloader.prepare(&ctx)?;
243
244 let config_files = self.bootloader.config_files(&ctx)?;
246 let mut all_files = Vec::new();
247
248 all_files.extend(bootloader_files.bios_files);
250 all_files.extend(bootloader_files.uefi_files);
251 all_files.extend(bootloader_files.system_files);
252
253 for config_file in config_files {
255 if config_file.needs_template_processing {
256 let content = std::fs::read_to_string(&config_file.source)?;
258 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
259
260 let temp_path = ctx.output_dir.join("processed_config");
262 std::fs::create_dir_all(&temp_path)?;
263 let processed_file = temp_path.join(
264 config_file
265 .source
266 .file_name()
267 .ok_or_else(|| Error::config("invalid config file path"))?,
268 );
269 std::fs::write(&processed_file, processed)?;
270
271 all_files.push(crate::bootloader::FileEntry::new(
272 processed_file,
273 config_file.dest,
274 ));
275 } else {
276 all_files.push(crate::bootloader::FileEntry::new(
277 config_file.source,
278 config_file.dest,
279 ));
280 }
281 }
282
283 if ctx.config.verbose {
285 println!("Building image: {}", self.image_builder.name());
286 }
287 let image_path = self.image_builder.build(&ctx, &all_files)?;
288
289 Ok(image_path)
290 }
291
292 pub fn run(self) -> Result<()> {
294 let mut ctx = Context::new(self.config, self.workspace_root, self.executable)?;
296 ctx.cli_extra_args = self.cli_extra_args;
297 ctx.env_extra_args = crate::config::env::get_extra_qemu_args();
298
299 ctx.template_vars.insert(
301 "ARGS".to_string(),
302 ctx.cli_extra_args.join(" "),
303 );
304
305 self.bootloader.validate_config(&ctx)?;
307 self.image_builder.validate_boot_type(&ctx)?;
308 self.runner.validate(&ctx)?;
309
310 if ctx.config.verbose {
312 println!("Preparing bootloader: {}", self.bootloader.name());
313 }
314 let bootloader_files = self.bootloader.prepare(&ctx)?;
315
316 let config_files = self.bootloader.config_files(&ctx)?;
318 let mut all_files = Vec::new();
319
320 all_files.extend(bootloader_files.bios_files);
322 all_files.extend(bootloader_files.uefi_files);
323 all_files.extend(bootloader_files.system_files);
324
325 for config_file in config_files {
327 if config_file.needs_template_processing {
328 let content = std::fs::read_to_string(&config_file.source)?;
330 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
331
332 let temp_path = ctx.output_dir.join("processed_config");
334 std::fs::create_dir_all(&temp_path)?;
335 let processed_file = temp_path.join(
336 config_file
337 .source
338 .file_name()
339 .ok_or_else(|| Error::config("invalid config file path"))?,
340 );
341 std::fs::write(&processed_file, processed)?;
342
343 all_files.push(crate::bootloader::FileEntry::new(
344 processed_file,
345 config_file.dest,
346 ));
347 } else {
348 all_files.push(crate::bootloader::FileEntry::new(
349 config_file.source,
350 config_file.dest,
351 ));
352 }
353 }
354
355 if ctx.config.verbose {
357 println!("Building image: {}", self.image_builder.name());
358 }
359 let image_path = self.image_builder.build(&ctx, &all_files)?;
360
361 if ctx.config.verbose {
363 println!("Running with: {}", self.runner.name());
364 }
365 let result = self.runner.run(&ctx, &image_path)?;
366
367 if ctx.is_test {
369 if result.timed_out {
370 return Err(Error::runner("test timed out"));
371 }
372
373 if let Some(success_code) = ctx.test_success_exit_code() {
374 if result.exit_code == success_code {
375 return Ok(());
376 } else {
377 return Err(Error::runner(format!(
378 "Test failed: expected exit code {}, got {}",
379 success_code, result.exit_code
380 )));
381 }
382 }
383 }
384
385 if !result.success {
386 return Err(Error::runner(format!(
387 "Execution failed with exit code: {}",
388 result.exit_code
389 )));
390 }
391
392 Ok(())
393 }
394}
395
396fn create_bootloader_from_config(config: &Config) -> Result<Box<dyn Bootloader>> {
400 match config.bootloader.kind {
401 #[cfg(feature = "limine")]
402 BootloaderKind::Limine => Ok(Box::new(crate::bootloader::limine::LimineBootloader::new())),
403
404 #[cfg(not(feature = "limine"))]
405 BootloaderKind::Limine => Err(Error::feature_not_enabled("limine")),
406
407 BootloaderKind::Grub => Ok(Box::new(crate::bootloader::grub::GrubBootloader::new())),
408
409 BootloaderKind::None => Ok(Box::new(crate::bootloader::none::NoneBootloader::new())),
410 }
411}
412
413fn create_image_builder_from_config(config: &Config) -> Result<Box<dyn ImageBuilder>> {
415 match config.image.format {
416 #[cfg(feature = "iso")]
417 ImageFormat::Iso => Ok(Box::new(crate::image::iso::IsoImageBuilder::new())),
418
419 #[cfg(not(feature = "iso"))]
420 ImageFormat::Iso => Err(Error::feature_not_enabled("iso")),
421
422 #[cfg(feature = "fat")]
423 ImageFormat::Fat => Ok(Box::new(crate::image::fat::FatImageBuilder::new())),
424
425 #[cfg(not(feature = "fat"))]
426 ImageFormat::Fat => Err(Error::feature_not_enabled("fat")),
427
428 ImageFormat::Directory => Ok(Box::new(crate::image::directory::DirectoryBuilder::new())),
429 }
430}
431
432fn create_runner_from_config(config: &Config) -> Result<Box<dyn Runner>> {
434 match config.runner.kind {
435 #[cfg(feature = "qemu")]
436 RunnerKind::Qemu => Ok(Box::new(crate::runner::qemu::QemuRunner::new())),
437
438 #[cfg(not(feature = "qemu"))]
439 RunnerKind::Qemu => Err(Error::feature_not_enabled("qemu")),
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_builder_error_missing_config() {
449 let result = ImageRunnerBuilder::new()
450 .workspace_root("/tmp")
451 .executable("/tmp/kernel")
452 .build();
453 let err = result.err().expect("should fail");
454 assert!(err.to_string().contains("no configuration"));
455 }
456
457 #[test]
458 fn test_builder_error_missing_workspace_root() {
459 let result = ImageRunnerBuilder::new()
460 .with_config(Config::default())
461 .executable("/tmp/kernel")
462 .build();
463 let err = result.err().expect("should fail");
464 assert!(err.to_string().contains("workspace root"));
465 }
466
467 #[test]
468 fn test_builder_error_missing_executable() {
469 let result = ImageRunnerBuilder::new()
470 .with_config(Config::default())
471 .workspace_root("/tmp")
472 .build();
473 let err = result.err().expect("should fail");
474 assert!(err.to_string().contains("executable"));
475 }
476
477 #[test]
478 fn test_builder_with_none_bootloader_and_directory() {
479 let dir = tempfile::tempdir().unwrap();
480 let exe = dir.path().join("kernel");
481 std::fs::write(&exe, b"fake").unwrap();
482
483 let result = ImageRunnerBuilder::new()
485 .with_config(Config::default())
486 .workspace_root(dir.path())
487 .executable(&exe)
488 .build();
489 assert!(result.is_ok());
490 }
491
492 #[test]
493 fn test_builder_explicit_components() {
494 let dir = tempfile::tempdir().unwrap();
495 let exe = dir.path().join("kernel");
496 std::fs::write(&exe, b"fake").unwrap();
497
498 let result = ImageRunnerBuilder::new()
499 .with_config(Config::default())
500 .workspace_root(dir.path())
501 .executable(&exe)
502 .no_bootloader()
503 .directory_output()
504 .build();
505 assert!(result.is_ok());
506 }
507}