Skip to main content

cargo_image_runner/config/
mod.rs

1//! Configuration types and loading from `[package.metadata.image-runner]` in Cargo.toml.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7pub mod env;
8mod loader;
9pub use loader::ConfigLoader;
10
11/// Complete configuration for image runner.
12#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct Config {
14    /// Boot type configuration.
15    #[serde(default)]
16    pub boot: BootConfig,
17
18    /// Bootloader configuration.
19    #[serde(default)]
20    pub bootloader: BootloaderConfig,
21
22    /// Image format configuration.
23    #[serde(default)]
24    pub image: ImageConfig,
25
26    /// Runner configuration.
27    #[serde(default)]
28    pub runner: RunnerConfig,
29
30    /// Test-specific configuration.
31    #[serde(default)]
32    pub test: TestConfig,
33
34    /// Run-specific configuration (non-test).
35    #[serde(default)]
36    pub run: RunConfig,
37
38    /// Template variables for substitution.
39    #[serde(default)]
40    pub variables: HashMap<String, String>,
41
42    /// Extra files to include in the image (dest path → source path).
43    #[serde(default, rename = "extra-files")]
44    pub extra_files: HashMap<String, String>,
45
46    /// Enable verbose output (show build progress messages).
47    #[serde(default)]
48    pub verbose: bool,
49}
50
51impl Config {
52    /// Parse a `Config` directly from a TOML string.
53    ///
54    /// This bypasses `ConfigLoader` entirely — no profiles, no env var overrides.
55    /// Useful for standalone/library consumers who don't use `cargo_metadata`.
56    pub fn from_toml_str(toml: &str) -> crate::core::Result<Self> {
57        toml::from_str(toml).map_err(|e| crate::core::Error::config(format!("failed to parse TOML config: {}", e)))
58    }
59
60    /// Parse a `Config` from a TOML file on disk.
61    ///
62    /// This bypasses `ConfigLoader` entirely — no profiles, no env var overrides.
63    /// Useful for standalone/library consumers who don't use `cargo_metadata`.
64    pub fn from_toml_file(path: impl AsRef<std::path::Path>) -> crate::core::Result<Self> {
65        let content = std::fs::read_to_string(path.as_ref())
66            .map_err(|e| crate::core::Error::config(format!("failed to read config file: {}", e)))?;
67        Self::from_toml_str(&content)
68    }
69}
70
71/// Boot type configuration.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct BootConfig {
74    /// Boot type: BIOS, UEFI, or Hybrid.
75    #[serde(rename = "type")]
76    pub boot_type: BootType,
77}
78
79impl Default for BootConfig {
80    fn default() -> Self {
81        Self {
82            boot_type: BootType::Uefi,
83        }
84    }
85}
86
87/// Boot type enumeration.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum BootType {
91    /// BIOS boot only.
92    Bios,
93    /// UEFI boot only.
94    Uefi,
95    /// Support both BIOS and UEFI.
96    Hybrid,
97}
98
99impl BootType {
100    /// Check if BIOS boot is required.
101    pub fn needs_bios(self) -> bool {
102        matches!(self, BootType::Bios | BootType::Hybrid)
103    }
104
105    /// Check if UEFI boot is required.
106    pub fn needs_uefi(self) -> bool {
107        matches!(self, BootType::Uefi | BootType::Hybrid)
108    }
109}
110
111/// Bootloader configuration.
112#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct BootloaderConfig {
114    /// Bootloader type.
115    pub kind: BootloaderKind,
116
117    /// Path to bootloader configuration file.
118    #[serde(rename = "config-file")]
119    pub config_file: Option<PathBuf>,
120
121    /// Limine-specific configuration.
122    #[serde(default)]
123    pub limine: LimineConfig,
124
125    /// GRUB-specific configuration.
126    #[serde(default)]
127    pub grub: GrubConfig,
128}
129
130/// Bootloader type enumeration.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
132#[serde(rename_all = "lowercase")]
133pub enum BootloaderKind {
134    /// Limine bootloader.
135    Limine,
136    /// GRUB bootloader.
137    Grub,
138    /// No bootloader (direct boot).
139    #[default]
140    None,
141}
142
143/// Limine bootloader configuration.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct LimineConfig {
146    /// Limine version to use.
147    pub version: String,
148}
149
150impl Default for LimineConfig {
151    fn default() -> Self {
152        Self {
153            version: "v8.x-binary".to_string(),
154        }
155    }
156}
157
158/// GRUB bootloader configuration.
159#[derive(Debug, Clone, Serialize, Deserialize, Default)]
160pub struct GrubConfig {
161    /// GRUB modules to include.
162    #[serde(default)]
163    pub modules: Vec<String>,
164}
165
166/// Image format configuration.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ImageConfig {
169    /// Image format type.
170    pub format: ImageFormat,
171
172    /// Output path for the image.
173    pub output: Option<PathBuf>,
174
175    /// Volume label (for ISO/FAT).
176    #[serde(default = "default_volume_label")]
177    pub volume_label: String,
178}
179
180impl Default for ImageConfig {
181    fn default() -> Self {
182        Self {
183            format: ImageFormat::Directory,
184            output: None,
185            volume_label: default_volume_label(),
186        }
187    }
188}
189
190fn default_volume_label() -> String {
191    "BOOT".to_string()
192}
193
194/// Image format enumeration.
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "lowercase")]
197pub enum ImageFormat {
198    /// ISO 9660 image.
199    Iso,
200    /// FAT filesystem image.
201    Fat,
202    /// Directory (for QEMU fat:rw:).
203    #[default]
204    Directory,
205}
206
207/// Runner configuration.
208#[derive(Debug, Clone, Serialize, Deserialize, Default)]
209pub struct RunnerConfig {
210    /// Runner type.
211    pub kind: RunnerKind,
212
213    /// QEMU-specific configuration.
214    #[serde(default)]
215    pub qemu: QemuConfig,
216}
217
218/// Runner type enumeration.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
220#[serde(rename_all = "lowercase")]
221pub enum RunnerKind {
222    /// QEMU emulator.
223    #[default]
224    Qemu,
225}
226
227/// QEMU runner configuration.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(default)]
230pub struct QemuConfig {
231    /// QEMU binary to use.
232    #[serde(default = "default_qemu_binary")]
233    pub binary: String,
234
235    /// Machine type.
236    #[serde(default = "default_machine")]
237    pub machine: String,
238
239    /// Memory size in MB.
240    #[serde(default = "default_memory")]
241    pub memory: u32,
242
243    /// Number of CPU cores.
244    #[serde(default = "default_cores")]
245    pub cores: u32,
246
247    /// Enable KVM acceleration.
248    #[serde(default = "default_true")]
249    pub kvm: bool,
250
251    /// Additional QEMU arguments.
252    #[serde(default)]
253    pub extra_args: Vec<String>,
254}
255
256fn default_qemu_binary() -> String {
257    "qemu-system-x86_64".to_string()
258}
259
260fn default_machine() -> String {
261    "q35".to_string()
262}
263
264fn default_memory() -> u32 {
265    1024
266}
267
268fn default_cores() -> u32 {
269    1
270}
271
272impl Default for QemuConfig {
273    fn default() -> Self {
274        Self {
275            binary: "qemu-system-x86_64".to_string(),
276            machine: "q35".to_string(),
277            memory: 1024,
278            cores: 1,
279            kvm: true,
280            extra_args: Vec::new(),
281        }
282    }
283}
284
285fn default_true() -> bool {
286    true
287}
288
289/// Test-specific configuration.
290#[derive(Debug, Clone, Serialize, Deserialize, Default)]
291pub struct TestConfig {
292    /// Exit code that indicates test success.
293    #[serde(rename = "success-exit-code")]
294    pub success_exit_code: Option<i32>,
295
296    /// Additional arguments for test runs.
297    #[serde(default, rename = "extra-args")]
298    pub extra_args: Vec<String>,
299
300    /// Timeout for tests in seconds.
301    pub timeout: Option<u64>,
302}
303
304/// Run-specific configuration (non-test).
305#[derive(Debug, Clone, Serialize, Deserialize, Default)]
306pub struct RunConfig {
307    /// Additional arguments for normal runs.
308    #[serde(default, rename = "extra-args")]
309    pub extra_args: Vec<String>,
310
311    /// Whether to use GUI display.
312    #[serde(default)]
313    pub gui: bool,
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn test_config_default_values() {
322        let config = Config::default();
323        assert_eq!(config.boot.boot_type, BootType::Uefi);
324        assert_eq!(config.bootloader.kind, BootloaderKind::None);
325        assert!(config.bootloader.config_file.is_none());
326        assert_eq!(config.image.format, ImageFormat::Directory);
327        assert!(config.image.output.is_none());
328        assert_eq!(config.image.volume_label, "BOOT");
329        assert_eq!(config.runner.kind, RunnerKind::Qemu);
330        assert!(config.test.success_exit_code.is_none());
331        assert!(config.test.extra_args.is_empty());
332        assert!(config.test.timeout.is_none());
333        assert!(!config.run.gui);
334        assert!(config.run.extra_args.is_empty());
335        assert!(config.variables.is_empty());
336        assert!(config.extra_files.is_empty());
337        assert!(!config.verbose);
338    }
339
340    #[test]
341    fn test_config_deserialize_minimal() {
342        let toml_str = r#"
343        [boot]
344        type = "uefi"
345        "#;
346        let config: Config = toml::from_str(toml_str).unwrap();
347        assert_eq!(config.boot.boot_type, BootType::Uefi);
348        assert_eq!(config.bootloader.kind, BootloaderKind::None);
349        assert_eq!(config.image.format, ImageFormat::Directory);
350    }
351
352    #[test]
353    fn test_config_deserialize_full() {
354        let toml_str = r#"
355        verbose = true
356
357        [boot]
358        type = "hybrid"
359
360        [bootloader]
361        kind = "limine"
362        config-file = "limine.conf"
363
364        [bootloader.limine]
365        version = "v8.4.0-binary"
366
367        [image]
368        format = "iso"
369        output = "my-os.iso"
370        volume_label = "MYOS"
371
372        [runner]
373        kind = "qemu"
374
375        [runner.qemu]
376        binary = "qemu-system-x86_64"
377        memory = 2048
378        cores = 2
379        kvm = false
380
381        [test]
382        success-exit-code = 33
383        timeout = 30
384        extra-args = ["-device", "isa-debug-exit"]
385
386        [run]
387        gui = true
388        extra-args = ["-serial", "stdio"]
389
390        [variables]
391        TIMEOUT = "5"
392
393        [extra-files]
394        "boot/extra.bin" = "extra.bin"
395        "#;
396        let config: Config = toml::from_str(toml_str).unwrap();
397        assert_eq!(config.boot.boot_type, BootType::Hybrid);
398        assert_eq!(config.bootloader.kind, BootloaderKind::Limine);
399        assert_eq!(
400            config.bootloader.config_file,
401            Some(PathBuf::from("limine.conf"))
402        );
403        assert_eq!(config.bootloader.limine.version, "v8.4.0-binary");
404        assert_eq!(config.image.format, ImageFormat::Iso);
405        assert_eq!(config.image.output, Some(PathBuf::from("my-os.iso")));
406        assert_eq!(config.image.volume_label, "MYOS");
407        assert_eq!(config.runner.qemu.memory, 2048);
408        assert_eq!(config.runner.qemu.cores, 2);
409        assert!(!config.runner.qemu.kvm);
410        assert_eq!(config.test.success_exit_code, Some(33));
411        assert_eq!(config.test.timeout, Some(30));
412        assert!(config.run.gui);
413        assert_eq!(config.variables.get("TIMEOUT").unwrap(), "5");
414        assert_eq!(config.extra_files.get("boot/extra.bin").unwrap(), "extra.bin");
415        assert!(config.verbose);
416    }
417
418    #[test]
419    fn test_config_deserialize_bios_boot_type() {
420        let toml_str = r#"
421        [boot]
422        type = "bios"
423
424        [bootloader]
425        kind = "grub"
426
427        [image]
428        format = "fat"
429        "#;
430        let config: Config = toml::from_str(toml_str).unwrap();
431        assert_eq!(config.boot.boot_type, BootType::Bios);
432        assert_eq!(config.bootloader.kind, BootloaderKind::Grub);
433        assert_eq!(config.image.format, ImageFormat::Fat);
434    }
435
436    #[test]
437    fn test_config_deserialize_invalid_boot_type() {
438        let toml_str = r#"
439        [boot]
440        type = "invalid"
441        "#;
442        let result: std::result::Result<Config, _> = toml::from_str(toml_str);
443        assert!(result.is_err());
444    }
445
446    #[test]
447    fn test_boot_type_needs_bios() {
448        assert!(BootType::Bios.needs_bios());
449        assert!(!BootType::Uefi.needs_bios());
450        assert!(BootType::Hybrid.needs_bios());
451    }
452
453    #[test]
454    fn test_boot_type_needs_uefi() {
455        assert!(!BootType::Bios.needs_uefi());
456        assert!(BootType::Uefi.needs_uefi());
457        assert!(BootType::Hybrid.needs_uefi());
458    }
459
460    #[test]
461    fn test_qemu_config_defaults() {
462        let qemu = QemuConfig::default();
463        assert_eq!(qemu.binary, "qemu-system-x86_64");
464        assert_eq!(qemu.machine, "q35");
465        assert_eq!(qemu.memory, 1024);
466        assert_eq!(qemu.cores, 1);
467        assert!(qemu.kvm);
468        assert!(qemu.extra_args.is_empty());
469    }
470
471    #[test]
472    fn test_limine_config_default_version() {
473        let limine = LimineConfig::default();
474        assert_eq!(limine.version, "v8.x-binary");
475    }
476
477    #[test]
478    fn test_extra_files_deserialize_empty() {
479        let toml_str = r#"
480        [boot]
481        type = "uefi"
482        "#;
483        let config: Config = toml::from_str(toml_str).unwrap();
484        assert!(config.extra_files.is_empty());
485    }
486
487    #[test]
488    fn test_extra_files_deserialize_nested_paths() {
489        let toml_str = r#"
490        [extra-files]
491        "boot/initramfs.cpio" = "build/initramfs.cpio"
492        "boot/data/config.txt" = "data/config.txt"
493        "#;
494        let config: Config = toml::from_str(toml_str).unwrap();
495        assert_eq!(config.extra_files.len(), 2);
496        assert_eq!(
497            config.extra_files.get("boot/initramfs.cpio").unwrap(),
498            "build/initramfs.cpio"
499        );
500        assert_eq!(
501            config.extra_files.get("boot/data/config.txt").unwrap(),
502            "data/config.txt"
503        );
504    }
505}