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        println!("Preparing bootloader: {}", self.bootloader.name());
222        let bootloader_files = self.bootloader.prepare(&ctx)?;
223
224        // Get config files and process templates
225        let config_files = self.bootloader.config_files(&ctx)?;
226        let mut all_files = Vec::new();
227
228        // Add bootloader files
229        all_files.extend(bootloader_files.bios_files);
230        all_files.extend(bootloader_files.uefi_files);
231        all_files.extend(bootloader_files.system_files);
232
233        // Process config files with templates
234        for config_file in config_files {
235            if config_file.needs_template_processing {
236                // Read, process, and write template
237                let content = std::fs::read_to_string(&config_file.source)?;
238                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
239
240                // Write to temporary file
241                let temp_path = ctx.output_dir.join("processed_config");
242                std::fs::create_dir_all(&temp_path)?;
243                let processed_file = temp_path.join(
244                    config_file
245                        .source
246                        .file_name()
247                        .ok_or_else(|| Error::config("invalid config file path"))?,
248                );
249                std::fs::write(&processed_file, processed)?;
250
251                all_files.push(crate::bootloader::FileEntry::new(
252                    processed_file,
253                    config_file.dest,
254                ));
255            } else {
256                all_files.push(crate::bootloader::FileEntry::new(
257                    config_file.source,
258                    config_file.dest,
259                ));
260            }
261        }
262
263        // Build image
264        println!("Building image: {}", self.image_builder.name());
265        let image_path = self.image_builder.build(&ctx, &all_files)?;
266
267        Ok(image_path)
268    }
269
270    /// Run the full pipeline: prepare bootloader, build image, execute.
271    pub fn run(self) -> Result<()> {
272        // Create context
273        let ctx = Context::new(self.config, self.workspace_root, self.executable)?;
274
275        // Validate all components
276        self.bootloader.validate_config(&ctx)?;
277        self.image_builder.validate_boot_type(&ctx)?;
278        self.runner.validate(&ctx)?;
279
280        // Prepare bootloader files
281        println!("Preparing bootloader: {}", self.bootloader.name());
282        let bootloader_files = self.bootloader.prepare(&ctx)?;
283
284        // Get config files and process templates
285        let config_files = self.bootloader.config_files(&ctx)?;
286        let mut all_files = Vec::new();
287
288        // Add bootloader files
289        all_files.extend(bootloader_files.bios_files);
290        all_files.extend(bootloader_files.uefi_files);
291        all_files.extend(bootloader_files.system_files);
292
293        // Process config files with templates
294        for config_file in config_files {
295            if config_file.needs_template_processing {
296                // Read, process, and write template
297                let content = std::fs::read_to_string(&config_file.source)?;
298                let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
299
300                // Write to temporary file
301                let temp_path = ctx.output_dir.join("processed_config");
302                std::fs::create_dir_all(&temp_path)?;
303                let processed_file = temp_path.join(
304                    config_file
305                        .source
306                        .file_name()
307                        .ok_or_else(|| Error::config("invalid config file path"))?,
308                );
309                std::fs::write(&processed_file, processed)?;
310
311                all_files.push(crate::bootloader::FileEntry::new(
312                    processed_file,
313                    config_file.dest,
314                ));
315            } else {
316                all_files.push(crate::bootloader::FileEntry::new(
317                    config_file.source,
318                    config_file.dest,
319                ));
320            }
321        }
322
323        // Build image
324        println!("Building image: {}", self.image_builder.name());
325        let image_path = self.image_builder.build(&ctx, &all_files)?;
326
327        // Run image
328        println!("Running with: {}", self.runner.name());
329        let result = self.runner.run(&ctx, &image_path)?;
330
331        // Check result
332        if ctx.is_test {
333            // For tests, check if we have a specific success exit code
334            if let Some(success_code) = ctx.test_success_exit_code() {
335                if result.exit_code == success_code {
336                    println!("Test passed (exit code: {})", result.exit_code);
337                    return Ok(());
338                } else {
339                    return Err(Error::runner(format!(
340                        "Test failed: expected exit code {}, got {}",
341                        success_code, result.exit_code
342                    )));
343                }
344            }
345        }
346
347        if !result.success {
348            return Err(Error::runner(format!(
349                "Execution failed with exit code: {}",
350                result.exit_code
351            )));
352        }
353
354        Ok(())
355    }
356}
357
358// --- Factory Functions ---
359
360/// Create a bootloader from configuration.
361fn create_bootloader_from_config(config: &Config) -> Result<Box<dyn Bootloader>> {
362    match config.bootloader.kind {
363        #[cfg(feature = "limine")]
364        BootloaderKind::Limine => Ok(Box::new(crate::bootloader::limine::LimineBootloader::new())),
365
366        #[cfg(not(feature = "limine"))]
367        BootloaderKind::Limine => Err(Error::feature_not_enabled("limine")),
368
369        BootloaderKind::Grub => Ok(Box::new(crate::bootloader::grub::GrubBootloader::new())),
370
371        BootloaderKind::None => Ok(Box::new(crate::bootloader::none::NoneBootloader::new())),
372    }
373}
374
375/// Create an image builder from configuration.
376fn create_image_builder_from_config(config: &Config) -> Result<Box<dyn ImageBuilder>> {
377    match config.image.format {
378        #[cfg(feature = "iso")]
379        ImageFormat::Iso => Ok(Box::new(crate::image::iso::IsoImageBuilder::new())),
380
381        #[cfg(not(feature = "iso"))]
382        ImageFormat::Iso => Err(Error::feature_not_enabled("iso")),
383
384        #[cfg(feature = "fat")]
385        ImageFormat::Fat => Ok(Box::new(crate::image::fat::FatImageBuilder::new())),
386
387        #[cfg(not(feature = "fat"))]
388        ImageFormat::Fat => Err(Error::feature_not_enabled("fat")),
389
390        ImageFormat::Directory => Ok(Box::new(crate::image::directory::DirectoryBuilder::new())),
391    }
392}
393
394/// Create a runner from configuration.
395fn create_runner_from_config(config: &Config) -> Result<Box<dyn Runner>> {
396    match config.runner.kind {
397        #[cfg(feature = "qemu")]
398        RunnerKind::Qemu => Ok(Box::new(crate::runner::qemu::QemuRunner::new())),
399
400        #[cfg(not(feature = "qemu"))]
401        RunnerKind::Qemu => Err(Error::feature_not_enabled("qemu")),
402    }
403}