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}
18
19impl ImageRunnerBuilder {
20    /// Create a new builder with default settings.
21    pub fn new() -> Self {
22        Self {
23            config: None,
24            workspace_root: None,
25            executable: None,
26            bootloader: None,
27            image_builder: None,
28            runner: None,
29        }
30    }
31
32    /// Set the configuration directly.
33    pub fn with_config(mut self, config: Config) -> Self {
34        self.config = Some(config);
35        self
36    }
37
38    /// Load configuration from Cargo.toml metadata.
39    pub fn from_cargo_metadata(mut self) -> Result<Self> {
40        let (config, workspace_root) = ConfigLoader::new().load()?;
41        self.config = Some(config);
42        self.workspace_root = Some(workspace_root);
43        Ok(self)
44    }
45
46    /// Load configuration from a standalone TOML file.
47    pub fn from_config_file(mut self, path: impl Into<PathBuf>) -> Result<Self> {
48        let (config, workspace_root) = ConfigLoader::new().config_file(path).load()?;
49        self.config = Some(config);
50        self.workspace_root = Some(workspace_root);
51        Ok(self)
52    }
53
54    /// Set the executable path.
55    pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
56        self.executable = Some(path.into());
57        self
58    }
59
60    /// Set the workspace root.
61    pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
62        self.workspace_root = Some(path.into());
63        self
64    }
65
66    // --- Bootloader Configuration ---
67
68    /// Set a custom bootloader implementation.
69    pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
70        self.bootloader = Some(Box::new(bootloader));
71        self
72    }
73
74    /// Use Limine bootloader.
75    #[cfg(feature = "limine")]
76    pub fn limine(mut self) -> Self {
77        self.bootloader = Some(Box::new(crate::bootloader::limine::LimineBootloader::new()));
78        self
79    }
80
81    /// Use GRUB bootloader.
82    pub fn grub(mut self) -> Self {
83        self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
84        self
85    }
86
87    /// Use no bootloader (direct boot).
88    pub fn no_bootloader(mut self) -> Self {
89        self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
90        self
91    }
92
93    // --- Image Format Configuration ---
94
95    /// Set a custom image builder implementation.
96    pub fn image_builder<I: ImageBuilder + 'static>(mut self, builder: I) -> Self {
97        self.image_builder = Some(Box::new(builder));
98        self
99    }
100
101    /// Build an ISO image.
102    #[cfg(feature = "iso")]
103    pub fn iso_image(mut self) -> Self {
104        self.image_builder = Some(Box::new(crate::image::iso::IsoImageBuilder::new()));
105        self
106    }
107
108    /// Build a FAT filesystem image.
109    #[cfg(feature = "fat")]
110    pub fn fat_image(mut self) -> Self {
111        self.image_builder = Some(Box::new(crate::image::fat::FatImageBuilder::new()));
112        self
113    }
114
115    /// Output to a directory (for QEMU fat:rw:).
116    pub fn directory_output(mut self) -> Self {
117        self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
118        self
119    }
120
121    // --- Runner Configuration ---
122
123    /// Set a custom runner implementation.
124    pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
125        self.runner = Some(Box::new(runner));
126        self
127    }
128
129    /// Use QEMU runner.
130    #[cfg(feature = "qemu")]
131    pub fn qemu(mut self) -> Self {
132        self.runner = Some(Box::new(crate::runner::qemu::QemuRunner::new()));
133        self
134    }
135
136    // --- Build and Execute ---
137
138    /// Build the image runner.
139    pub fn build(self) -> Result<ImageRunner> {
140        let config = self.config.ok_or_else(|| Error::config("no configuration provided"))?;
141
142        let workspace_root = self.workspace_root.ok_or_else(|| {
143            Error::config("workspace root not set (call from_cargo_metadata or workspace_root)")
144        })?;
145
146        let executable = self.executable.ok_or_else(|| {
147            Error::config("executable not set (call executable or get from CLI args)")
148        })?;
149
150        // Create bootloader from config if not explicitly set
151        let bootloader = if let Some(bl) = self.bootloader {
152            bl
153        } else {
154            create_bootloader_from_config(&config)?
155        };
156
157        // Create image builder from config if not explicitly set
158        let image_builder = if let Some(ib) = self.image_builder {
159            ib
160        } else {
161            create_image_builder_from_config(&config)?
162        };
163
164        // Create runner from config if not explicitly set
165        let runner = if let Some(r) = self.runner {
166            r
167        } else {
168            create_runner_from_config(&config)?
169        };
170
171        Ok(ImageRunner {
172            config,
173            workspace_root,
174            executable,
175            bootloader,
176            image_builder,
177            runner,
178        })
179    }
180
181    /// Build and immediately run.
182    pub fn run(self) -> Result<()> {
183        let runner = self.build()?;
184        runner.run()
185    }
186}
187
188impl Default for ImageRunnerBuilder {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194/// Image runner that orchestrates the build and run process.
195pub struct ImageRunner {
196    config: Config,
197    workspace_root: PathBuf,
198    executable: PathBuf,
199    bootloader: Box<dyn Bootloader>,
200    image_builder: Box<dyn ImageBuilder>,
201    runner: Box<dyn Runner>,
202}
203
204impl ImageRunner {
205    /// Build the image without running it.
206    ///
207    /// Returns the path to the built image.
208    pub fn build_image(&self) -> Result<PathBuf> {
209        // Create context
210        let ctx = Context::new(
211            self.config.clone(),
212            self.workspace_root.clone(),
213            self.executable.clone(),
214        )?;
215
216        // Validate all components
217        self.bootloader.validate_config(&ctx)?;
218        self.image_builder.validate_boot_type(&ctx)?;
219
220        // Prepare bootloader files
221        if ctx.config.verbose {
222            println!("Preparing bootloader: {}", self.bootloader.name());
223        }
224        let bootloader_files = self.bootloader.prepare(&ctx)?;
225
226        // Get config files and process templates
227        let config_files = self.bootloader.config_files(&ctx)?;
228        let mut all_files = Vec::new();
229
230        // Add bootloader files
231        all_files.extend(bootloader_files.bios_files);
232        all_files.extend(bootloader_files.uefi_files);
233        all_files.extend(bootloader_files.system_files);
234
235        // Process config files with templates
236        for config_file in config_files {
237            if config_file.needs_template_processing {
238                // Read, process, and write template
239                let content = std::fs::read_to_string(&config_file.source)?;
240                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
241
242                // Write to temporary file
243                let temp_path = ctx.output_dir.join("processed_config");
244                std::fs::create_dir_all(&temp_path)?;
245                let processed_file = temp_path.join(
246                    config_file
247                        .source
248                        .file_name()
249                        .ok_or_else(|| Error::config("invalid config file path"))?,
250                );
251                std::fs::write(&processed_file, processed)?;
252
253                all_files.push(crate::bootloader::FileEntry::new(
254                    processed_file,
255                    config_file.dest,
256                ));
257            } else {
258                all_files.push(crate::bootloader::FileEntry::new(
259                    config_file.source,
260                    config_file.dest,
261                ));
262            }
263        }
264
265        // Build image
266        if ctx.config.verbose {
267            println!("Building image: {}", self.image_builder.name());
268        }
269        let image_path = self.image_builder.build(&ctx, &all_files)?;
270
271        Ok(image_path)
272    }
273
274    /// Run the full pipeline: prepare bootloader, build image, execute.
275    pub fn run(self) -> Result<()> {
276        // Create context
277        let ctx = Context::new(self.config, self.workspace_root, self.executable)?;
278
279        // Validate all components
280        self.bootloader.validate_config(&ctx)?;
281        self.image_builder.validate_boot_type(&ctx)?;
282        self.runner.validate(&ctx)?;
283
284        // Prepare bootloader files
285        if ctx.config.verbose {
286            println!("Preparing bootloader: {}", self.bootloader.name());
287        }
288        let bootloader_files = self.bootloader.prepare(&ctx)?;
289
290        // Get config files and process templates
291        let config_files = self.bootloader.config_files(&ctx)?;
292        let mut all_files = Vec::new();
293
294        // Add bootloader files
295        all_files.extend(bootloader_files.bios_files);
296        all_files.extend(bootloader_files.uefi_files);
297        all_files.extend(bootloader_files.system_files);
298
299        // Process config files with templates
300        for config_file in config_files {
301            if config_file.needs_template_processing {
302                // Read, process, and write template
303                let content = std::fs::read_to_string(&config_file.source)?;
304                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
305
306                // Write to temporary file
307                let temp_path = ctx.output_dir.join("processed_config");
308                std::fs::create_dir_all(&temp_path)?;
309                let processed_file = temp_path.join(
310                    config_file
311                        .source
312                        .file_name()
313                        .ok_or_else(|| Error::config("invalid config file path"))?,
314                );
315                std::fs::write(&processed_file, processed)?;
316
317                all_files.push(crate::bootloader::FileEntry::new(
318                    processed_file,
319                    config_file.dest,
320                ));
321            } else {
322                all_files.push(crate::bootloader::FileEntry::new(
323                    config_file.source,
324                    config_file.dest,
325                ));
326            }
327        }
328
329        // Build image
330        if ctx.config.verbose {
331            println!("Building image: {}", self.image_builder.name());
332        }
333        let image_path = self.image_builder.build(&ctx, &all_files)?;
334
335        // Run image
336        if ctx.config.verbose {
337            println!("Running with: {}", self.runner.name());
338        }
339        let result = self.runner.run(&ctx, &image_path)?;
340
341        // Check result
342        if ctx.is_test {
343            // For tests, check if we have a specific success exit code
344            if let Some(success_code) = ctx.test_success_exit_code() {
345                if result.exit_code == success_code {
346                    if ctx.config.verbose {
347                        println!("Test passed (exit code: {})", result.exit_code);
348                    }
349                    return Ok(());
350                } else {
351                    return Err(Error::runner(format!(
352                        "Test failed: expected exit code {}, got {}",
353                        success_code, result.exit_code
354                    )));
355                }
356            }
357        }
358
359        if !result.success {
360            return Err(Error::runner(format!(
361                "Execution failed with exit code: {}",
362                result.exit_code
363            )));
364        }
365
366        Ok(())
367    }
368}
369
370// --- Factory Functions ---
371
372/// Create a bootloader from configuration.
373fn create_bootloader_from_config(config: &Config) -> Result<Box<dyn Bootloader>> {
374    match config.bootloader.kind {
375        #[cfg(feature = "limine")]
376        BootloaderKind::Limine => Ok(Box::new(crate::bootloader::limine::LimineBootloader::new())),
377
378        #[cfg(not(feature = "limine"))]
379        BootloaderKind::Limine => Err(Error::feature_not_enabled("limine")),
380
381        BootloaderKind::Grub => Ok(Box::new(crate::bootloader::grub::GrubBootloader::new())),
382
383        BootloaderKind::None => Ok(Box::new(crate::bootloader::none::NoneBootloader::new())),
384    }
385}
386
387/// Create an image builder from configuration.
388fn create_image_builder_from_config(config: &Config) -> Result<Box<dyn ImageBuilder>> {
389    match config.image.format {
390        #[cfg(feature = "iso")]
391        ImageFormat::Iso => Ok(Box::new(crate::image::iso::IsoImageBuilder::new())),
392
393        #[cfg(not(feature = "iso"))]
394        ImageFormat::Iso => Err(Error::feature_not_enabled("iso")),
395
396        #[cfg(feature = "fat")]
397        ImageFormat::Fat => Ok(Box::new(crate::image::fat::FatImageBuilder::new())),
398
399        #[cfg(not(feature = "fat"))]
400        ImageFormat::Fat => Err(Error::feature_not_enabled("fat")),
401
402        ImageFormat::Directory => Ok(Box::new(crate::image::directory::DirectoryBuilder::new())),
403    }
404}
405
406/// Create a runner from configuration.
407fn create_runner_from_config(config: &Config) -> Result<Box<dyn Runner>> {
408    match config.runner.kind {
409        #[cfg(feature = "qemu")]
410        RunnerKind::Qemu => Ok(Box::new(crate::runner::qemu::QemuRunner::new())),
411
412        #[cfg(not(feature = "qemu"))]
413        RunnerKind::Qemu => Err(Error::feature_not_enabled("qemu")),
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_builder_error_missing_config() {
423        let result = ImageRunnerBuilder::new()
424            .workspace_root("/tmp")
425            .executable("/tmp/kernel")
426            .build();
427        let err = result.err().expect("should fail");
428        assert!(err.to_string().contains("no configuration"));
429    }
430
431    #[test]
432    fn test_builder_error_missing_workspace_root() {
433        let result = ImageRunnerBuilder::new()
434            .with_config(Config::default())
435            .executable("/tmp/kernel")
436            .build();
437        let err = result.err().expect("should fail");
438        assert!(err.to_string().contains("workspace root"));
439    }
440
441    #[test]
442    fn test_builder_error_missing_executable() {
443        let result = ImageRunnerBuilder::new()
444            .with_config(Config::default())
445            .workspace_root("/tmp")
446            .build();
447        let err = result.err().expect("should fail");
448        assert!(err.to_string().contains("executable"));
449    }
450
451    #[test]
452    fn test_builder_with_none_bootloader_and_directory() {
453        let dir = tempfile::tempdir().unwrap();
454        let exe = dir.path().join("kernel");
455        std::fs::write(&exe, b"fake").unwrap();
456
457        // Config defaults: BootloaderKind::None, ImageFormat::Directory
458        let result = ImageRunnerBuilder::new()
459            .with_config(Config::default())
460            .workspace_root(dir.path())
461            .executable(&exe)
462            .build();
463        assert!(result.is_ok());
464    }
465
466    #[test]
467    fn test_builder_explicit_components() {
468        let dir = tempfile::tempdir().unwrap();
469        let exe = dir.path().join("kernel");
470        std::fs::write(&exe, b"fake").unwrap();
471
472        let result = ImageRunnerBuilder::new()
473            .with_config(Config::default())
474            .workspace_root(dir.path())
475            .executable(&exe)
476            .no_bootloader()
477            .directory_output()
478            .build();
479        assert!(result.is_ok());
480    }
481}