cargo_image_runner/core/
builder.rs1use 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}
18
19impl ImageRunnerBuilder {
20 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 pub fn with_config(mut self, config: Config) -> Self {
34 self.config = Some(config);
35 self
36 }
37
38 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 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 pub fn executable(mut self, path: impl Into<PathBuf>) -> Self {
56 self.executable = Some(path.into());
57 self
58 }
59
60 pub fn workspace_root(mut self, path: impl Into<PathBuf>) -> Self {
62 self.workspace_root = Some(path.into());
63 self
64 }
65
66 pub fn bootloader<B: Bootloader + 'static>(mut self, bootloader: B) -> Self {
70 self.bootloader = Some(Box::new(bootloader));
71 self
72 }
73
74 #[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 pub fn grub(mut self) -> Self {
83 self.bootloader = Some(Box::new(crate::bootloader::grub::GrubBootloader::new()));
84 self
85 }
86
87 pub fn no_bootloader(mut self) -> Self {
89 self.bootloader = Some(Box::new(crate::bootloader::none::NoneBootloader::new()));
90 self
91 }
92
93 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 #[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 #[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 pub fn directory_output(mut self) -> Self {
117 self.image_builder = Some(Box::new(crate::image::directory::DirectoryBuilder::new()));
118 self
119 }
120
121 pub fn runner<R: Runner + 'static>(mut self, runner: R) -> Self {
125 self.runner = Some(Box::new(runner));
126 self
127 }
128
129 #[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 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 let bootloader = if let Some(bl) = self.bootloader {
152 bl
153 } else {
154 create_bootloader_from_config(&config)?
155 };
156
157 let image_builder = if let Some(ib) = self.image_builder {
159 ib
160 } else {
161 create_image_builder_from_config(&config)?
162 };
163
164 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 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
194pub 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 pub fn build_image(&self) -> Result<PathBuf> {
209 let ctx = Context::new(
211 self.config.clone(),
212 self.workspace_root.clone(),
213 self.executable.clone(),
214 )?;
215
216 self.bootloader.validate_config(&ctx)?;
218 self.image_builder.validate_boot_type(&ctx)?;
219
220 println!("Preparing bootloader: {}", self.bootloader.name());
222 let bootloader_files = self.bootloader.prepare(&ctx)?;
223
224 let config_files = self.bootloader.config_files(&ctx)?;
226 let mut all_files = Vec::new();
227
228 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 for config_file in config_files {
235 if config_file.needs_template_processing {
236 let content = std::fs::read_to_string(&config_file.source)?;
238 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
239
240 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 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 pub fn run(self) -> Result<()> {
272 let ctx = Context::new(self.config, self.workspace_root, self.executable)?;
274
275 self.bootloader.validate_config(&ctx)?;
277 self.image_builder.validate_boot_type(&ctx)?;
278 self.runner.validate(&ctx)?;
279
280 println!("Preparing bootloader: {}", self.bootloader.name());
282 let bootloader_files = self.bootloader.prepare(&ctx)?;
283
284 let config_files = self.bootloader.config_files(&ctx)?;
286 let mut all_files = Vec::new();
287
288 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 for config_file in config_files {
295 if config_file.needs_template_processing {
296 let content = std::fs::read_to_string(&config_file.source)?;
298 let processed = self.bootloader.process_templates(&content, &ctx.template_vars)?;
299
300 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 println!("Building image: {}", self.image_builder.name());
325 let image_path = self.image_builder.build(&ctx, &all_files)?;
326
327 println!("Running with: {}", self.runner.name());
329 let result = self.runner.run(&ctx, &image_path)?;
330
331 if ctx.is_test {
333 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
358fn 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
375fn 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
394fn 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}