cargo_image_runner/config/
mod.rs1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7pub mod env;
8mod loader;
9pub use loader::ConfigLoader;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct Config {
14 #[serde(default)]
16 pub boot: BootConfig,
17
18 #[serde(default)]
20 pub bootloader: BootloaderConfig,
21
22 #[serde(default)]
24 pub image: ImageConfig,
25
26 #[serde(default)]
28 pub runner: RunnerConfig,
29
30 #[serde(default)]
32 pub test: TestConfig,
33
34 #[serde(default)]
36 pub run: RunConfig,
37
38 #[serde(default)]
40 pub variables: HashMap<String, String>,
41
42 #[serde(default, rename = "extra-files")]
44 pub extra_files: HashMap<String, String>,
45
46 #[serde(default)]
48 pub verbose: bool,
49}
50
51impl Config {
52 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct BootConfig {
74 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum BootType {
91 Bios,
93 Uefi,
95 Hybrid,
97}
98
99impl BootType {
100 pub fn needs_bios(self) -> bool {
102 matches!(self, BootType::Bios | BootType::Hybrid)
103 }
104
105 pub fn needs_uefi(self) -> bool {
107 matches!(self, BootType::Uefi | BootType::Hybrid)
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, Default)]
113pub struct BootloaderConfig {
114 pub kind: BootloaderKind,
116
117 #[serde(rename = "config-file")]
119 pub config_file: Option<PathBuf>,
120
121 #[serde(default)]
123 pub limine: LimineConfig,
124
125 #[serde(default)]
127 pub grub: GrubConfig,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
132#[serde(rename_all = "lowercase")]
133pub enum BootloaderKind {
134 Limine,
136 Grub,
138 #[default]
140 None,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct LimineConfig {
146 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
160pub struct GrubConfig {
161 #[serde(default)]
163 pub modules: Vec<String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ImageConfig {
169 pub format: ImageFormat,
171
172 pub output: Option<PathBuf>,
174
175 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "lowercase")]
197pub enum ImageFormat {
198 Iso,
200 Fat,
202 #[default]
204 Directory,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, Default)]
209pub struct RunnerConfig {
210 pub kind: RunnerKind,
212
213 #[serde(default)]
215 pub qemu: QemuConfig,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
220#[serde(rename_all = "lowercase")]
221pub enum RunnerKind {
222 #[default]
224 Qemu,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(default)]
230pub struct QemuConfig {
231 #[serde(default = "default_qemu_binary")]
233 pub binary: String,
234
235 #[serde(default = "default_machine")]
237 pub machine: String,
238
239 #[serde(default = "default_memory")]
241 pub memory: u32,
242
243 #[serde(default = "default_cores")]
245 pub cores: u32,
246
247 #[serde(default = "default_true")]
249 pub kvm: bool,
250
251 #[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
291pub struct TestConfig {
292 #[serde(rename = "success-exit-code")]
294 pub success_exit_code: Option<i32>,
295
296 #[serde(default, rename = "extra-args")]
298 pub extra_args: Vec<String>,
299
300 pub timeout: Option<u64>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, Default)]
306pub struct RunConfig {
307 #[serde(default, rename = "extra-args")]
309 pub extra_args: Vec<String>,
310
311 #[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}