Skip to main content

cargo_image_runner/core/
builder.rs

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
11/// Builder for creating and running bootable images.
12pub 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    /// Create a new builder with default settings.
24    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    /// Set the configuration directly.
37    pub fn with_config(mut self, config: Config) -> Self {
38        self.config = Some(config);
39        self
40    }
41
42    /// Load configuration from Cargo.toml metadata.
43    #[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    /// Load configuration from a standalone TOML file.
52    #[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    /// Set the executable path.
61    pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
62        self.executable = Some(path.into());
63        self
64    }
65
66    /// Set the workspace root.
67    pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
68        self.workspace_root = Some(path.into());
69        self
70    }
71
72    /// Set extra QEMU arguments from CLI passthrough (`-- args`).
73    pub fn extra_args(mut self, args: Vec<String>) -> Self {
74        self.cli_extra_args = args;
75        self
76    }
77
78    // --- Bootloader Configuration ---
79
80    /// Set a custom bootloader implementation.
81    pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
82        self.bootloader = Some(Box::new(bootloader));
83        self
84    }
85
86    /// Use Limine bootloader.
87    #[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    /// Use GRUB bootloader.
94    pub fn grub(mut self) -> Self {
95        self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
96        self
97    }
98
99    /// Use no bootloader (direct boot).
100    pub fn no_bootloader(mut self) -> Self {
101        self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
102        self
103    }
104
105    // --- Image Format Configuration ---
106
107    /// Set a custom image builder implementation.
108    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    /// Build an ISO image.
114    #[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    /// Build a FAT filesystem image.
121    #[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    /// Output to a directory (for QEMU fat:rw:).
128    pub fn directory_output(mut self) -> Self {
129        self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
130        self
131    }
132
133    // --- Runner Configuration ---
134
135    /// Set a custom runner implementation.
136    pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
137        self.runner = Some(Box::new(runner));
138        self
139    }
140
141    /// Use QEMU runner.
142    #[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    // --- Build and Execute ---
149
150    /// Build the image runner.
151    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        // Create bootloader from config if not explicitly set
163        let bootloader = if let Some(bl) = self.bootloader {
164            bl
165        } else {
166            create_bootloader_from_config(&config)?
167        };
168
169        // Create image builder from config if not explicitly set
170        let image_builder = if let Some(ib) = self.image_builder {
171            ib
172        } else {
173            create_image_builder_from_config(&config)?
174        };
175
176        // Create runner from config if not explicitly set
177        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    /// Build and immediately run.
195    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
207/// Image runner that orchestrates the build and run process.
208pub 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    /// Build the image without running it.
220    ///
221    /// Returns the path to the built image.
222    pub fn build_image(&self) -> Result<PathBuf> {
223        // Create context
224        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        // Update ARGS template variable with CLI args
233        ctx.template_vars.insert(
234            "ARGS".to_string(),
235            ctx.cli_extra_args.join(" "),
236        );
237
238        // Validate all components
239        self.bootloader.validate_config(&ctx)?;
240        self.image_builder.validate_boot_type(&ctx)?;
241
242        // Prepare bootloader files
243        if ctx.config.verbose {
244            println!("Preparing bootloader: {}", self.bootloader.name());
245        }
246        let bootloader_files = self.bootloader.prepare(&ctx)?;
247
248        // Get config files and process templates
249        let config_files = self.bootloader.config_files(&ctx)?;
250        let mut all_files = Vec::new();
251
252        // Add bootloader files
253        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        // Process config files with templates
258        for config_file in config_files {
259            if config_file.needs_template_processing {
260                // Read, process, and write template
261                let content = std::fs::read_to_string(&config_file.source)?;
262                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
263
264                // Write to temporary file
265                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        // Add extra files from config
288        all_files.extend(collect_extra_files(&ctx)?);
289
290        // Build image
291        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    /// Run the full pipeline: prepare bootloader, build image, execute.
300    pub fn run(self) -> Result<()> {
301        // Create context
302        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        // Update ARGS template variable with CLI args
307        ctx.template_vars.insert(
308            "ARGS".to_string(),
309            ctx.cli_extra_args.join(" "),
310        );
311
312        // Validate all components
313        self.bootloader.validate_config(&ctx)?;
314        self.image_builder.validate_boot_type(&ctx)?;
315        self.runner.validate(&ctx)?;
316
317        // Prepare bootloader files
318        if ctx.config.verbose {
319            println!("Preparing bootloader: {}", self.bootloader.name());
320        }
321        let bootloader_files = self.bootloader.prepare(&ctx)?;
322
323        // Get config files and process templates
324        let config_files = self.bootloader.config_files(&ctx)?;
325        let mut all_files = Vec::new();
326
327        // Add bootloader files
328        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        // Process config files with templates
333        for config_file in config_files {
334            if config_file.needs_template_processing {
335                // Read, process, and write template
336                let content = std::fs::read_to_string(&config_file.source)?;
337                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
338
339                // Write to temporary file
340                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        // Add extra files from config
363        all_files.extend(collect_extra_files(&ctx)?);
364
365        // Build image
366        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        // Run image
372        if ctx.config.verbose {
373            println!("Running with: {}", self.runner.name());
374        }
375        let result = self.runner.run(&ctx, &image_path)?;
376
377        // Check result
378        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
406// --- Extra Files ---
407
408/// Collect extra files specified in config, resolving source paths relative to workspace root.
409fn 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        // Strip leading '/' so dest is always relative to image root.
421        // Users may write "/boot/file" meaning "boot/file within the image".
422        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
431// --- Factory Functions ---
432
433/// Create a bootloader from configuration.
434fn 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
448/// Create an image builder from configuration.
449fn 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
467/// Create a runner from configuration.
468fn 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        // Config defaults: BootloaderKind::None, ImageFormat::Directory
519        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}