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