Skip to main content

cargo_image_runner/core/
builder.rs

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
9/// Builder for creating and running bootable images.
10pub 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    /// Create a new builder with default settings.
22    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    /// Set the configuration directly.
35    pub fn with_config(mut self, config: Config) -> Self {
36        self.config = Some(config);
37        self
38    }
39
40    /// Load configuration from Cargo.toml metadata.
41    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    /// Load configuration from a standalone TOML file.
49    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    /// Set the executable path.
57    pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
58        self.executable = Some(path.into());
59        self
60    }
61
62    /// Set the workspace root.
63    pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
64        self.workspace_root = Some(path.into());
65        self
66    }
67
68    /// Set extra QEMU arguments from CLI passthrough (`-- args`).
69    pub fn extra_args(mut self, args: Vec<String>) -> Self {
70        self.cli_extra_args = args;
71        self
72    }
73
74    // --- Bootloader Configuration ---
75
76    /// Set a custom bootloader implementation.
77    pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
78        self.bootloader = Some(Box::new(bootloader));
79        self
80    }
81
82    /// Use Limine bootloader.
83    #[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    /// Use GRUB bootloader.
90    pub fn grub(mut self) -> Self {
91        self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
92        self
93    }
94
95    /// Use no bootloader (direct boot).
96    pub fn no_bootloader(mut self) -> Self {
97        self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
98        self
99    }
100
101    // --- Image Format Configuration ---
102
103    /// Set a custom image builder implementation.
104    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    /// Build an ISO image.
110    #[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    /// Build a FAT filesystem image.
117    #[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    /// Output to a directory (for QEMU fat:rw:).
124    pub fn directory_output(mut self) -> Self {
125        self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
126        self
127    }
128
129    // --- Runner Configuration ---
130
131    /// Set a custom runner implementation.
132    pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
133        self.runner = Some(Box::new(runner));
134        self
135    }
136
137    /// Use QEMU runner.
138    #[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    // --- Build and Execute ---
145
146    /// Build the image runner.
147    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        // Create bootloader from config if not explicitly set
159        let bootloader = if let Some(bl) = self.bootloader {
160            bl
161        } else {
162            create_bootloader_from_config(&config)?
163        };
164
165        // Create image builder from config if not explicitly set
166        let image_builder = if let Some(ib) = self.image_builder {
167            ib
168        } else {
169            create_image_builder_from_config(&config)?
170        };
171
172        // Create runner from config if not explicitly set
173        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    /// Build and immediately run.
191    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
203/// Image runner that orchestrates the build and run process.
204pub 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    /// Build the image without running it.
216    ///
217    /// Returns the path to the built image.
218    pub fn build_image(&self) -> Result<PathBuf> {
219        // Create context
220        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        // Update ARGS template variable with CLI args
229        ctx.template_vars.insert(
230            "ARGS".to_string(),
231            ctx.cli_extra_args.join(" "),
232        );
233
234        // Validate all components
235        self.bootloader.validate_config(&ctx)?;
236        self.image_builder.validate_boot_type(&ctx)?;
237
238        // Prepare bootloader files
239        if ctx.config.verbose {
240            println!("Preparing bootloader: {}", self.bootloader.name());
241        }
242        let bootloader_files = self.bootloader.prepare(&ctx)?;
243
244        // Get config files and process templates
245        let config_files = self.bootloader.config_files(&ctx)?;
246        let mut all_files = Vec::new();
247
248        // Add bootloader files
249        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        // Process config files with templates
254        for config_file in config_files {
255            if config_file.needs_template_processing {
256                // Read, process, and write template
257                let content = std::fs::read_to_string(&config_file.source)?;
258                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
259
260                // Write to temporary file
261                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        // Build image
284        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    /// Run the full pipeline: prepare bootloader, build image, execute.
293    pub fn run(self) -> Result<()> {
294        // Create context
295        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        // Update ARGS template variable with CLI args
300        ctx.template_vars.insert(
301            "ARGS".to_string(),
302            ctx.cli_extra_args.join(" "),
303        );
304
305        // Validate all components
306        self.bootloader.validate_config(&ctx)?;
307        self.image_builder.validate_boot_type(&ctx)?;
308        self.runner.validate(&ctx)?;
309
310        // Prepare bootloader files
311        if ctx.config.verbose {
312            println!("Preparing bootloader: {}", self.bootloader.name());
313        }
314        let bootloader_files = self.bootloader.prepare(&ctx)?;
315
316        // Get config files and process templates
317        let config_files = self.bootloader.config_files(&ctx)?;
318        let mut all_files = Vec::new();
319
320        // Add bootloader files
321        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        // Process config files with templates
326        for config_file in config_files {
327            if config_file.needs_template_processing {
328                // Read, process, and write template
329                let content = std::fs::read_to_string(&config_file.source)?;
330                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
331
332                // Write to temporary file
333                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        // Build image
356        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        // Run image
362        if ctx.config.verbose {
363            println!("Running with: {}", self.runner.name());
364        }
365        let result = self.runner.run(&ctx, &image_path)?;
366
367        // Check result
368        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
396// --- Factory Functions ---
397
398/// Create a bootloader from configuration.
399fn 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
413/// Create an image builder from configuration.
414fn 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
432/// Create a runner from configuration.
433fn 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        // Config defaults: BootloaderKind::None, ImageFormat::Directory
484        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}