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