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