1use anyhow::{Context, Result};
6use clap::ValueEnum;
7use fn_error_context::context;
8use serde::{Deserialize, Serialize};
9
10#[cfg(feature = "install-to-disk")]
11use super::baseline::BlockSetup;
12
13pub(crate) struct EnvProperties {
16 pub(crate) sys_arch: String,
17}
18
19#[derive(clap::ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub(crate) enum Filesystem {
23 Xfs,
24 Ext4,
25 Btrfs,
26}
27
28impl std::fmt::Display for Filesystem {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 self.to_possible_value().unwrap().get_name().fmt(f)
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37#[serde(deny_unknown_fields)]
38pub(crate) struct InstallConfigurationToplevel {
39 pub(crate) install: Option<InstallConfiguration>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44#[serde(deny_unknown_fields)]
45pub(crate) struct RootFS {
46 #[serde(rename = "type")]
47 pub(crate) fstype: Option<Filesystem>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, Default)]
53#[serde(deny_unknown_fields)]
54pub(crate) struct BasicFilesystems {
55 pub(crate) root: Option<RootFS>,
56 }
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
63#[serde(rename = "install", rename_all = "kebab-case", deny_unknown_fields)]
64pub(crate) struct InstallConfiguration {
65 pub(crate) root_fs_type: Option<Filesystem>,
67 #[cfg(feature = "install-to-disk")]
69 pub(crate) block: Option<Vec<BlockSetup>>,
70 pub(crate) filesystem: Option<BasicFilesystems>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub(crate) kargs: Option<Vec<String>>,
74 pub(crate) match_architectures: Option<Vec<String>>,
76}
77
78fn merge_basic<T>(s: &mut Option<T>, o: Option<T>, _env: &EnvProperties) {
79 if let Some(o) = o {
80 *s = Some(o);
81 }
82}
83
84trait Mergeable {
85 fn merge(&mut self, other: Self, env: &EnvProperties)
86 where
87 Self: Sized;
88}
89
90impl<T> Mergeable for Option<T>
91where
92 T: Mergeable,
93{
94 fn merge(&mut self, other: Self, env: &EnvProperties)
95 where
96 Self: Sized,
97 {
98 if let Some(other) = other {
99 if let Some(s) = self.as_mut() {
100 s.merge(other, env)
101 } else {
102 *self = Some(other);
103 }
104 }
105 }
106}
107
108impl Mergeable for RootFS {
109 fn merge(&mut self, other: Self, env: &EnvProperties) {
111 merge_basic(&mut self.fstype, other.fstype, env)
112 }
113}
114
115impl Mergeable for BasicFilesystems {
116 fn merge(&mut self, other: Self, env: &EnvProperties) {
118 self.root.merge(other.root, env)
119 }
120}
121
122impl Mergeable for InstallConfiguration {
123 fn merge(&mut self, other: Self, env: &EnvProperties) {
125 if other
128 .match_architectures
129 .map(|a| a.contains(&env.sys_arch))
130 .unwrap_or(true)
131 {
132 merge_basic(&mut self.root_fs_type, other.root_fs_type, env);
133 #[cfg(feature = "install-to-disk")]
134 merge_basic(&mut self.block, other.block, env);
135 self.filesystem.merge(other.filesystem, env);
136 if let Some(other_kargs) = other.kargs {
137 self.kargs
138 .get_or_insert_with(Default::default)
139 .extend(other_kargs)
140 }
141 }
142 }
143}
144
145impl InstallConfiguration {
146 pub(crate) fn canonicalize(&mut self) {
152 if let Some(rootfs_type) = self.filesystem_root().and_then(|f| f.fstype.as_ref()) {
154 self.root_fs_type = Some(*rootfs_type)
155 } else if let Some(rootfs) = self.root_fs_type.as_ref() {
156 let fs = self.filesystem.get_or_insert_with(Default::default);
157 let root = fs.root.get_or_insert_with(Default::default);
158 root.fstype = Some(*rootfs);
159 }
160
161 #[cfg(feature = "install-to-disk")]
162 if self.block.is_none() {
163 self.block = Some(vec![BlockSetup::Direct]);
164 }
165 }
166
167 pub(crate) fn filesystem_root(&self) -> Option<&RootFS> {
169 self.filesystem.as_ref().and_then(|fs| fs.root.as_ref())
170 }
171
172 pub(crate) fn filter_to_external(&mut self) {
174 self.kargs.take();
175 }
176
177 #[cfg(feature = "install-to-disk")]
178 pub(crate) fn get_block_setup(&self, default: Option<BlockSetup>) -> Result<BlockSetup> {
179 let valid_block_setups = self.block.as_deref().unwrap_or_default();
180 let default_block = valid_block_setups.iter().next().ok_or_else(|| {
181 anyhow::anyhow!("Empty block storage configuration in install configuration")
182 })?;
183 let block_setup = default.as_ref().unwrap_or(default_block);
184 if !valid_block_setups.contains(block_setup) {
185 anyhow::bail!("Block setup {block_setup:?} is not enabled in installation config");
186 }
187 Ok(*block_setup)
188 }
189}
190
191#[context("Loading configuration")]
192pub(crate) fn load_config() -> Result<Option<InstallConfiguration>> {
194 let env = EnvProperties {
195 sys_arch: std::env::consts::ARCH.to_string(),
196 };
197 const SYSTEMD_CONVENTIONAL_BASES: &[&str] = &["/usr/lib", "/usr/local/lib", "/etc", "/run"];
198 let fragments = liboverdrop::scan(SYSTEMD_CONVENTIONAL_BASES, "bootc/install", &["toml"], true);
199 let mut config: Option<InstallConfiguration> = None;
200 for (_name, path) in fragments {
201 let buf = std::fs::read_to_string(&path)?;
202 let mut unused = std::collections::HashSet::new();
203 let de = toml::Deserializer::parse(&buf).with_context(|| format!("Parsing {path:?}"))?;
204 let mut c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| {
205 unused.insert(path.to_string());
206 })
207 .with_context(|| format!("Parsing {path:?}"))?;
208 for key in unused {
209 eprintln!("warning: {path:?}: Unknown key {key}");
210 }
211 if let Some(config) = config.as_mut() {
212 if let Some(install) = c.install {
213 tracing::debug!("Merging install config: {install:?}");
214 config.merge(install, &env);
215 }
216 } else {
217 if let Some(ref mut install) = c.install {
220 if install
221 .match_architectures
222 .as_ref()
223 .map(|a| a.contains(&env.sys_arch))
224 .unwrap_or(true)
225 {
226 config = c.install;
227 }
228 }
229 }
230 }
231 if let Some(config) = config.as_mut() {
232 config.canonicalize();
233 }
234 Ok(config)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_parse_config() {
244 let env = EnvProperties {
245 sys_arch: "x86_64".to_string(),
246 };
247 let c: InstallConfigurationToplevel = toml::from_str(
248 r##"[install]
249root-fs-type = "xfs"
250"##,
251 )
252 .unwrap();
253 let mut install = c.install.unwrap();
254 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Xfs);
255 let other = InstallConfigurationToplevel {
256 install: Some(InstallConfiguration {
257 root_fs_type: Some(Filesystem::Ext4),
258 ..Default::default()
259 }),
260 };
261 install.merge(other.install.unwrap(), &env);
262 assert_eq!(
263 install.root_fs_type.as_ref().copied().unwrap(),
264 Filesystem::Ext4
265 );
266 assert!(install.filesystem_root().is_none());
268 install.canonicalize();
269 assert_eq!(install.root_fs_type.as_ref().unwrap(), &Filesystem::Ext4);
270 assert_eq!(
271 install.filesystem_root().unwrap().fstype.unwrap(),
272 Filesystem::Ext4
273 );
274
275 let c: InstallConfigurationToplevel = toml::from_str(
276 r##"[install]
277root-fs-type = "ext4"
278kargs = ["console=ttyS0", "foo=bar"]
279"##,
280 )
281 .unwrap();
282 let mut install = c.install.unwrap();
283 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
284 let other = InstallConfigurationToplevel {
285 install: Some(InstallConfiguration {
286 kargs: Some(
287 ["console=tty0", "nosmt"]
288 .into_iter()
289 .map(ToOwned::to_owned)
290 .collect(),
291 ),
292 ..Default::default()
293 }),
294 };
295 install.merge(other.install.unwrap(), &env);
296 assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
297 assert_eq!(
298 install.kargs,
299 Some(
300 ["console=ttyS0", "foo=bar", "console=tty0", "nosmt"]
301 .into_iter()
302 .map(ToOwned::to_owned)
303 .collect()
304 )
305 )
306 }
307
308 #[test]
309 fn test_parse_filesystems() {
310 let env = EnvProperties {
311 sys_arch: "x86_64".to_string(),
312 };
313 let c: InstallConfigurationToplevel = toml::from_str(
314 r##"[install.filesystem.root]
315type = "xfs"
316"##,
317 )
318 .unwrap();
319 let mut install = c.install.unwrap();
320 assert_eq!(
321 install.filesystem_root().unwrap().fstype.unwrap(),
322 Filesystem::Xfs
323 );
324 let other = InstallConfigurationToplevel {
325 install: Some(InstallConfiguration {
326 filesystem: Some(BasicFilesystems {
327 root: Some(RootFS {
328 fstype: Some(Filesystem::Ext4),
329 }),
330 }),
331 ..Default::default()
332 }),
333 };
334 install.merge(other.install.unwrap(), &env);
335 assert_eq!(
336 install.filesystem_root().unwrap().fstype.unwrap(),
337 Filesystem::Ext4
338 );
339 }
340
341 #[test]
342 fn test_parse_block() {
343 let env = EnvProperties {
344 sys_arch: "x86_64".to_string(),
345 };
346 let c: InstallConfigurationToplevel = toml::from_str(
347 r##"[install.filesystem.root]
348type = "xfs"
349"##,
350 )
351 .unwrap();
352 let mut install = c.install.unwrap();
353 {
355 let mut install = install.clone();
356 install.canonicalize();
357 assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Direct);
358 }
359 let other = InstallConfigurationToplevel {
360 install: Some(InstallConfiguration {
361 block: Some(vec![]),
362 ..Default::default()
363 }),
364 };
365 install.merge(other.install.unwrap(), &env);
366 assert_eq!(install.block.as_ref().unwrap().len(), 0);
368 assert!(install.get_block_setup(None).is_err());
369
370 let c: InstallConfigurationToplevel = toml::from_str(
371 r##"[install]
372block = ["tpm2-luks"]"##,
373 )
374 .unwrap();
375 let mut install = c.install.unwrap();
376 install.canonicalize();
377 assert_eq!(install.block.as_ref().unwrap().len(), 1);
378 assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Tpm2Luks);
379
380 assert!(install.get_block_setup(Some(BlockSetup::Direct)).is_err());
382 }
383
384 #[test]
385 fn test_arch() {
387 let env = EnvProperties {
389 sys_arch: "x86_64".to_string(),
390 };
391 let c: InstallConfigurationToplevel = toml::from_str(
392 r##"[install]
393root-fs-type = "xfs"
394"##,
395 )
396 .unwrap();
397 let mut install = c.install.unwrap();
398 let other = InstallConfigurationToplevel {
399 install: Some(InstallConfiguration {
400 kargs: Some(
401 ["console=tty0", "nosmt"]
402 .into_iter()
403 .map(ToOwned::to_owned)
404 .collect(),
405 ),
406 ..Default::default()
407 }),
408 };
409 install.merge(other.install.unwrap(), &env);
410 assert_eq!(
411 install.kargs,
412 Some(
413 ["console=tty0", "nosmt"]
414 .into_iter()
415 .map(ToOwned::to_owned)
416 .collect()
417 )
418 );
419 let env = EnvProperties {
420 sys_arch: "aarch64".to_string(),
421 };
422 let c: InstallConfigurationToplevel = toml::from_str(
423 r##"[install]
424root-fs-type = "xfs"
425"##,
426 )
427 .unwrap();
428 let mut install = c.install.unwrap();
429 let other = InstallConfigurationToplevel {
430 install: Some(InstallConfiguration {
431 kargs: Some(
432 ["console=tty0", "nosmt"]
433 .into_iter()
434 .map(ToOwned::to_owned)
435 .collect(),
436 ),
437 ..Default::default()
438 }),
439 };
440 install.merge(other.install.unwrap(), &env);
441 assert_eq!(
442 install.kargs,
443 Some(
444 ["console=tty0", "nosmt"]
445 .into_iter()
446 .map(ToOwned::to_owned)
447 .collect()
448 )
449 );
450
451 let env = EnvProperties {
453 sys_arch: "aarch64".to_string(),
454 };
455 let c: InstallConfigurationToplevel = toml::from_str(
456 r##"[install]
457root-fs-type = "xfs"
458"##,
459 )
460 .unwrap();
461 let mut install = c.install.unwrap();
462 let other = InstallConfigurationToplevel {
463 install: Some(InstallConfiguration {
464 kargs: Some(
465 ["console=ttyS0", "foo=bar"]
466 .into_iter()
467 .map(ToOwned::to_owned)
468 .collect(),
469 ),
470 match_architectures: Some(["x86_64"].into_iter().map(ToOwned::to_owned).collect()),
471 ..Default::default()
472 }),
473 };
474 install.merge(other.install.unwrap(), &env);
475 assert_eq!(install.kargs, None);
476 let other = InstallConfigurationToplevel {
477 install: Some(InstallConfiguration {
478 kargs: Some(
479 ["console=tty0", "nosmt"]
480 .into_iter()
481 .map(ToOwned::to_owned)
482 .collect(),
483 ),
484 match_architectures: Some(["aarch64"].into_iter().map(ToOwned::to_owned).collect()),
485 ..Default::default()
486 }),
487 };
488 install.merge(other.install.unwrap(), &env);
489 assert_eq!(
490 install.kargs,
491 Some(
492 ["console=tty0", "nosmt"]
493 .into_iter()
494 .map(ToOwned::to_owned)
495 .collect()
496 )
497 );
498
499 let env = EnvProperties {
501 sys_arch: "x86_64".to_string(),
502 };
503 let c: InstallConfigurationToplevel = toml::from_str(
504 r##"[install]
505root-fs-type = "xfs"
506"##,
507 )
508 .unwrap();
509 let mut install = c.install.unwrap();
510 let other = InstallConfigurationToplevel {
511 install: Some(InstallConfiguration {
512 kargs: Some(
513 ["console=tty0", "nosmt"]
514 .into_iter()
515 .map(ToOwned::to_owned)
516 .collect(),
517 ),
518 match_architectures: Some(
519 ["x86_64", "aarch64"]
520 .into_iter()
521 .map(ToOwned::to_owned)
522 .collect(),
523 ),
524 ..Default::default()
525 }),
526 };
527 install.merge(other.install.unwrap(), &env);
528 assert_eq!(
529 install.kargs,
530 Some(
531 ["console=tty0", "nosmt"]
532 .into_iter()
533 .map(ToOwned::to_owned)
534 .collect()
535 )
536 );
537 let env = EnvProperties {
538 sys_arch: "aarch64".to_string(),
539 };
540 let c: InstallConfigurationToplevel = toml::from_str(
541 r##"[install]
542root-fs-type = "xfs"
543"##,
544 )
545 .unwrap();
546 let mut install = c.install.unwrap();
547 let other = InstallConfigurationToplevel {
548 install: Some(InstallConfiguration {
549 kargs: Some(
550 ["console=tty0", "nosmt"]
551 .into_iter()
552 .map(ToOwned::to_owned)
553 .collect(),
554 ),
555 match_architectures: Some(
556 ["x86_64", "aarch64"]
557 .into_iter()
558 .map(ToOwned::to_owned)
559 .collect(),
560 ),
561 ..Default::default()
562 }),
563 };
564 install.merge(other.install.unwrap(), &env);
565 assert_eq!(
566 install.kargs,
567 Some(
568 ["console=tty0", "nosmt"]
569 .into_iter()
570 .map(ToOwned::to_owned)
571 .collect()
572 )
573 );
574 }
575}