bootc_lib/install/
config.rs

1//! # Configuration for `bootc install`
2//!
3//! This module handles the TOML configuration file for `bootc install`.
4
5use 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
13/// Properties of the environment, such as the system architecture
14/// Left open for future properties such as `platform.id`
15pub(crate) struct EnvProperties {
16    pub(crate) sys_arch: String,
17}
18
19/// A well known filesystem type.
20#[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/// The toplevel config entry for installation configs stored
35/// in bootc/install (e.g. /etc/bootc/install/05-custom.toml)
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37#[serde(deny_unknown_fields)]
38pub(crate) struct InstallConfigurationToplevel {
39    pub(crate) install: Option<InstallConfiguration>,
40}
41
42/// Configuration for a filesystem
43#[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/// This structure should only define "system" or "basic" filesystems; we are
51/// not trying to generalize this into e.g. supporting `/var` or other ones.
52#[derive(Debug, Clone, Serialize, Deserialize, Default)]
53#[serde(deny_unknown_fields)]
54pub(crate) struct BasicFilesystems {
55    pub(crate) root: Option<RootFS>,
56    // TODO allow configuration of these other filesystems too
57    // pub(crate) xbootldr: Option<FilesystemCustomization>,
58    // pub(crate) esp: Option<FilesystemCustomization>,
59}
60
61/// Configuration for ostree repository
62pub(crate) type OstreeRepoOpts = ostree_ext::repo_options::RepoOptions;
63
64/// The serialized [install] section
65#[derive(Debug, Clone, Serialize, Deserialize, Default)]
66#[serde(rename = "install", rename_all = "kebab-case", deny_unknown_fields)]
67pub(crate) struct InstallConfiguration {
68    /// Root filesystem type
69    pub(crate) root_fs_type: Option<Filesystem>,
70    /// Enabled block storage configurations
71    #[cfg(feature = "install-to-disk")]
72    pub(crate) block: Option<Vec<BlockSetup>>,
73    pub(crate) filesystem: Option<BasicFilesystems>,
74    /// Kernel arguments, applied at installation time
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub(crate) kargs: Option<Vec<String>>,
77    /// Supported architectures for this configuration
78    pub(crate) match_architectures: Option<Vec<String>>,
79    /// Ostree repository configuration
80    pub(crate) ostree: Option<OstreeRepoOpts>,
81    /// The stateroot name to use. Defaults to `default`
82    pub(crate) stateroot: Option<String>,
83    /// Source device specification for the root filesystem.
84    /// For example, `UUID=2e9f4241-229b-4202-8429-62d2302382e1` or `LABEL=rootfs`.
85    pub(crate) root_mount_spec: Option<String>,
86    /// Mount specification for the /boot filesystem.
87    pub(crate) boot_mount_spec: Option<String>,
88}
89
90fn merge_basic<T>(s: &mut Option<T>, o: Option<T>, _env: &EnvProperties) {
91    if let Some(o) = o {
92        *s = Some(o);
93    }
94}
95
96trait Mergeable {
97    fn merge(&mut self, other: Self, env: &EnvProperties)
98    where
99        Self: Sized;
100}
101
102impl<T> Mergeable for Option<T>
103where
104    T: Mergeable,
105{
106    fn merge(&mut self, other: Self, env: &EnvProperties)
107    where
108        Self: Sized,
109    {
110        if let Some(other) = other {
111            if let Some(s) = self.as_mut() {
112                s.merge(other, env)
113            } else {
114                *self = Some(other);
115            }
116        }
117    }
118}
119
120impl Mergeable for RootFS {
121    /// Apply any values in other, overriding any existing values in `self`.
122    fn merge(&mut self, other: Self, env: &EnvProperties) {
123        merge_basic(&mut self.fstype, other.fstype, env)
124    }
125}
126
127impl Mergeable for BasicFilesystems {
128    /// Apply any values in other, overriding any existing values in `self`.
129    fn merge(&mut self, other: Self, env: &EnvProperties) {
130        self.root.merge(other.root, env)
131    }
132}
133
134impl Mergeable for OstreeRepoOpts {
135    /// Apply any values in other, overriding any existing values in `self`.
136    fn merge(&mut self, other: Self, env: &EnvProperties) {
137        merge_basic(
138            &mut self.bls_append_except_default,
139            other.bls_append_except_default,
140            env,
141        )
142    }
143}
144
145impl Mergeable for InstallConfiguration {
146    /// Apply any values in other, overriding any existing values in `self`.
147    fn merge(&mut self, other: Self, env: &EnvProperties) {
148        // if arch is specified, only merge config if it matches the current arch
149        // if arch is not specified, merge config unconditionally
150        if other
151            .match_architectures
152            .map(|a| a.contains(&env.sys_arch))
153            .unwrap_or(true)
154        {
155            merge_basic(&mut self.root_fs_type, other.root_fs_type, env);
156            #[cfg(feature = "install-to-disk")]
157            merge_basic(&mut self.block, other.block, env);
158            self.filesystem.merge(other.filesystem, env);
159            self.ostree.merge(other.ostree, env);
160            merge_basic(&mut self.stateroot, other.stateroot, env);
161            merge_basic(&mut self.root_mount_spec, other.root_mount_spec, env);
162            merge_basic(&mut self.boot_mount_spec, other.boot_mount_spec, env);
163            if let Some(other_kargs) = other.kargs {
164                self.kargs
165                    .get_or_insert_with(Default::default)
166                    .extend(other_kargs)
167            }
168        }
169    }
170}
171
172impl InstallConfiguration {
173    /// Set defaults (e.g. `block`), and also handle fields that can be specified multiple ways
174    /// by synchronizing the values of the fields to ensure they're the same.
175    ///
176    /// - install.root-fs-type is synchronized with install.filesystems.root.type; if
177    ///   both are set, then the latter takes precedence
178    pub(crate) fn canonicalize(&mut self) {
179        // New canonical form wins.
180        if let Some(rootfs_type) = self.filesystem_root().and_then(|f| f.fstype.as_ref()) {
181            self.root_fs_type = Some(*rootfs_type)
182        } else if let Some(rootfs) = self.root_fs_type.as_ref() {
183            let fs = self.filesystem.get_or_insert_with(Default::default);
184            let root = fs.root.get_or_insert_with(Default::default);
185            root.fstype = Some(*rootfs);
186        }
187
188        #[cfg(feature = "install-to-disk")]
189        if self.block.is_none() {
190            self.block = Some(vec![BlockSetup::Direct]);
191        }
192    }
193
194    /// Convenience helper to access the root filesystem
195    pub(crate) fn filesystem_root(&self) -> Option<&RootFS> {
196        self.filesystem.as_ref().and_then(|fs| fs.root.as_ref())
197    }
198
199    // Remove all configuration which is handled by `install to-filesystem`.
200    pub(crate) fn filter_to_external(&mut self) {
201        self.kargs.take();
202    }
203
204    #[cfg(feature = "install-to-disk")]
205    pub(crate) fn get_block_setup(&self, default: Option<BlockSetup>) -> Result<BlockSetup> {
206        let valid_block_setups = self.block.as_deref().unwrap_or_default();
207        let default_block = valid_block_setups.iter().next().ok_or_else(|| {
208            anyhow::anyhow!("Empty block storage configuration in install configuration")
209        })?;
210        let block_setup = default.as_ref().unwrap_or(default_block);
211        if !valid_block_setups.contains(block_setup) {
212            anyhow::bail!("Block setup {block_setup:?} is not enabled in installation config");
213        }
214        Ok(*block_setup)
215    }
216}
217
218#[context("Loading configuration")]
219/// Load the install configuration, merging all found configuration files.
220pub(crate) fn load_config() -> Result<Option<InstallConfiguration>> {
221    let env = EnvProperties {
222        sys_arch: std::env::consts::ARCH.to_string(),
223    };
224    const SYSTEMD_CONVENTIONAL_BASES: &[&str] = &["/usr/lib", "/usr/local/lib", "/etc", "/run"];
225    let fragments = liboverdrop::scan(SYSTEMD_CONVENTIONAL_BASES, "bootc/install", &["toml"], true);
226    let mut config: Option<InstallConfiguration> = None;
227    for (_name, path) in fragments {
228        let buf = std::fs::read_to_string(&path)?;
229        let mut unused = std::collections::HashSet::new();
230        let de = toml::Deserializer::parse(&buf).with_context(|| format!("Parsing {path:?}"))?;
231        let mut c: InstallConfigurationToplevel = serde_ignored::deserialize(de, |path| {
232            unused.insert(path.to_string());
233        })
234        .with_context(|| format!("Parsing {path:?}"))?;
235        for key in unused {
236            eprintln!("warning: {path:?}: Unknown key {key}");
237        }
238        if let Some(config) = config.as_mut() {
239            if let Some(install) = c.install {
240                tracing::debug!("Merging install config: {install:?}");
241                config.merge(install, &env);
242            }
243        } else {
244            // Only set the config if it matches the current arch
245            // If no arch is specified, set the config unconditionally
246            if let Some(ref mut install) = c.install {
247                if install
248                    .match_architectures
249                    .as_ref()
250                    .map(|a| a.contains(&env.sys_arch))
251                    .unwrap_or(true)
252                {
253                    config = c.install;
254                }
255            }
256        }
257    }
258    if let Some(config) = config.as_mut() {
259        config.canonicalize();
260    }
261    Ok(config)
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    /// Verify that we can parse our default config file
270    fn test_parse_config() {
271        let env = EnvProperties {
272            sys_arch: "x86_64".to_string(),
273        };
274        let c: InstallConfigurationToplevel = toml::from_str(
275            r##"[install]
276root-fs-type = "xfs"
277"##,
278        )
279        .unwrap();
280        let mut install = c.install.unwrap();
281        assert_eq!(install.root_fs_type.unwrap(), Filesystem::Xfs);
282        let other = InstallConfigurationToplevel {
283            install: Some(InstallConfiguration {
284                root_fs_type: Some(Filesystem::Ext4),
285                ..Default::default()
286            }),
287        };
288        install.merge(other.install.unwrap(), &env);
289        assert_eq!(
290            install.root_fs_type.as_ref().copied().unwrap(),
291            Filesystem::Ext4
292        );
293        // This one shouldn't have been set
294        assert!(install.filesystem_root().is_none());
295        install.canonicalize();
296        assert_eq!(install.root_fs_type.as_ref().unwrap(), &Filesystem::Ext4);
297        assert_eq!(
298            install.filesystem_root().unwrap().fstype.unwrap(),
299            Filesystem::Ext4
300        );
301
302        let c: InstallConfigurationToplevel = toml::from_str(
303            r##"[install]
304root-fs-type = "ext4"
305kargs = ["console=ttyS0", "foo=bar"]
306"##,
307        )
308        .unwrap();
309        let mut install = c.install.unwrap();
310        assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
311        let other = InstallConfigurationToplevel {
312            install: Some(InstallConfiguration {
313                kargs: Some(
314                    ["console=tty0", "nosmt"]
315                        .into_iter()
316                        .map(ToOwned::to_owned)
317                        .collect(),
318                ),
319                ..Default::default()
320            }),
321        };
322        install.merge(other.install.unwrap(), &env);
323        assert_eq!(install.root_fs_type.unwrap(), Filesystem::Ext4);
324        assert_eq!(
325            install.kargs,
326            Some(
327                ["console=ttyS0", "foo=bar", "console=tty0", "nosmt"]
328                    .into_iter()
329                    .map(ToOwned::to_owned)
330                    .collect()
331            )
332        )
333    }
334
335    #[test]
336    fn test_parse_filesystems() {
337        let env = EnvProperties {
338            sys_arch: "x86_64".to_string(),
339        };
340        let c: InstallConfigurationToplevel = toml::from_str(
341            r##"[install.filesystem.root]
342type = "xfs"
343"##,
344        )
345        .unwrap();
346        let mut install = c.install.unwrap();
347        assert_eq!(
348            install.filesystem_root().unwrap().fstype.unwrap(),
349            Filesystem::Xfs
350        );
351        let other = InstallConfigurationToplevel {
352            install: Some(InstallConfiguration {
353                filesystem: Some(BasicFilesystems {
354                    root: Some(RootFS {
355                        fstype: Some(Filesystem::Ext4),
356                    }),
357                }),
358                ..Default::default()
359            }),
360        };
361        install.merge(other.install.unwrap(), &env);
362        assert_eq!(
363            install.filesystem_root().unwrap().fstype.unwrap(),
364            Filesystem::Ext4
365        );
366    }
367
368    #[test]
369    fn test_parse_block() {
370        let env = EnvProperties {
371            sys_arch: "x86_64".to_string(),
372        };
373        let c: InstallConfigurationToplevel = toml::from_str(
374            r##"[install.filesystem.root]
375type = "xfs"
376"##,
377        )
378        .unwrap();
379        let mut install = c.install.unwrap();
380        // Verify the default (but note canonicalization mutates)
381        {
382            let mut install = install.clone();
383            install.canonicalize();
384            assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Direct);
385        }
386        let other = InstallConfigurationToplevel {
387            install: Some(InstallConfiguration {
388                block: Some(vec![]),
389                ..Default::default()
390            }),
391        };
392        install.merge(other.install.unwrap(), &env);
393        // Should be set, but zero length
394        assert_eq!(install.block.as_ref().unwrap().len(), 0);
395        assert!(install.get_block_setup(None).is_err());
396
397        let c: InstallConfigurationToplevel = toml::from_str(
398            r##"[install]
399block = ["tpm2-luks"]"##,
400        )
401        .unwrap();
402        let mut install = c.install.unwrap();
403        install.canonicalize();
404        assert_eq!(install.block.as_ref().unwrap().len(), 1);
405        assert_eq!(install.get_block_setup(None).unwrap(), BlockSetup::Tpm2Luks);
406
407        // And verify passing a disallowed config is an error
408        assert!(install.get_block_setup(Some(BlockSetup::Direct)).is_err());
409    }
410
411    #[test]
412    /// Verify that kargs are only applied to supported architectures
413    fn test_arch() {
414        // no arch specified, kargs ensure that kargs are applied unconditionally
415        let env = EnvProperties {
416            sys_arch: "x86_64".to_string(),
417        };
418        let c: InstallConfigurationToplevel = toml::from_str(
419            r##"[install]
420root-fs-type = "xfs"
421"##,
422        )
423        .unwrap();
424        let mut install = c.install.unwrap();
425        let other = InstallConfigurationToplevel {
426            install: Some(InstallConfiguration {
427                kargs: Some(
428                    ["console=tty0", "nosmt"]
429                        .into_iter()
430                        .map(ToOwned::to_owned)
431                        .collect(),
432                ),
433                ..Default::default()
434            }),
435        };
436        install.merge(other.install.unwrap(), &env);
437        assert_eq!(
438            install.kargs,
439            Some(
440                ["console=tty0", "nosmt"]
441                    .into_iter()
442                    .map(ToOwned::to_owned)
443                    .collect()
444            )
445        );
446        let env = EnvProperties {
447            sys_arch: "aarch64".to_string(),
448        };
449        let c: InstallConfigurationToplevel = toml::from_str(
450            r##"[install]
451root-fs-type = "xfs"
452"##,
453        )
454        .unwrap();
455        let mut install = c.install.unwrap();
456        let other = InstallConfigurationToplevel {
457            install: Some(InstallConfiguration {
458                kargs: Some(
459                    ["console=tty0", "nosmt"]
460                        .into_iter()
461                        .map(ToOwned::to_owned)
462                        .collect(),
463                ),
464                ..Default::default()
465            }),
466        };
467        install.merge(other.install.unwrap(), &env);
468        assert_eq!(
469            install.kargs,
470            Some(
471                ["console=tty0", "nosmt"]
472                    .into_iter()
473                    .map(ToOwned::to_owned)
474                    .collect()
475            )
476        );
477
478        // one arch matches and one doesn't, ensure that kargs are only applied for the matching arch
479        let env = EnvProperties {
480            sys_arch: "aarch64".to_string(),
481        };
482        let c: InstallConfigurationToplevel = toml::from_str(
483            r##"[install]
484root-fs-type = "xfs"
485"##,
486        )
487        .unwrap();
488        let mut install = c.install.unwrap();
489        let other = InstallConfigurationToplevel {
490            install: Some(InstallConfiguration {
491                kargs: Some(
492                    ["console=ttyS0", "foo=bar"]
493                        .into_iter()
494                        .map(ToOwned::to_owned)
495                        .collect(),
496                ),
497                match_architectures: Some(["x86_64"].into_iter().map(ToOwned::to_owned).collect()),
498                ..Default::default()
499            }),
500        };
501        install.merge(other.install.unwrap(), &env);
502        assert_eq!(install.kargs, None);
503        let other = InstallConfigurationToplevel {
504            install: Some(InstallConfiguration {
505                kargs: Some(
506                    ["console=tty0", "nosmt"]
507                        .into_iter()
508                        .map(ToOwned::to_owned)
509                        .collect(),
510                ),
511                match_architectures: Some(["aarch64"].into_iter().map(ToOwned::to_owned).collect()),
512                ..Default::default()
513            }),
514        };
515        install.merge(other.install.unwrap(), &env);
516        assert_eq!(
517            install.kargs,
518            Some(
519                ["console=tty0", "nosmt"]
520                    .into_iter()
521                    .map(ToOwned::to_owned)
522                    .collect()
523            )
524        );
525
526        // multiple arch specified, ensure that kargs are applied to both archs
527        let env = EnvProperties {
528            sys_arch: "x86_64".to_string(),
529        };
530        let c: InstallConfigurationToplevel = toml::from_str(
531            r##"[install]
532root-fs-type = "xfs"
533"##,
534        )
535        .unwrap();
536        let mut install = c.install.unwrap();
537        let other = InstallConfigurationToplevel {
538            install: Some(InstallConfiguration {
539                kargs: Some(
540                    ["console=tty0", "nosmt"]
541                        .into_iter()
542                        .map(ToOwned::to_owned)
543                        .collect(),
544                ),
545                match_architectures: Some(
546                    ["x86_64", "aarch64"]
547                        .into_iter()
548                        .map(ToOwned::to_owned)
549                        .collect(),
550                ),
551                ..Default::default()
552            }),
553        };
554        install.merge(other.install.unwrap(), &env);
555        assert_eq!(
556            install.kargs,
557            Some(
558                ["console=tty0", "nosmt"]
559                    .into_iter()
560                    .map(ToOwned::to_owned)
561                    .collect()
562            )
563        );
564        let env = EnvProperties {
565            sys_arch: "aarch64".to_string(),
566        };
567        let c: InstallConfigurationToplevel = toml::from_str(
568            r##"[install]
569root-fs-type = "xfs"
570"##,
571        )
572        .unwrap();
573        let mut install = c.install.unwrap();
574        let other = InstallConfigurationToplevel {
575            install: Some(InstallConfiguration {
576                kargs: Some(
577                    ["console=tty0", "nosmt"]
578                        .into_iter()
579                        .map(ToOwned::to_owned)
580                        .collect(),
581                ),
582                match_architectures: Some(
583                    ["x86_64", "aarch64"]
584                        .into_iter()
585                        .map(ToOwned::to_owned)
586                        .collect(),
587                ),
588                ..Default::default()
589            }),
590        };
591        install.merge(other.install.unwrap(), &env);
592        assert_eq!(
593            install.kargs,
594            Some(
595                ["console=tty0", "nosmt"]
596                    .into_iter()
597                    .map(ToOwned::to_owned)
598                    .collect()
599            )
600        );
601    }
602
603    #[test]
604    fn test_parse_ostree() {
605        let env = EnvProperties {
606            sys_arch: "x86_64".to_string(),
607        };
608
609        // Table-driven test cases for parsing bls-append-except-default
610        let parse_cases = [
611            ("console=ttyS0", "console=ttyS0"),
612            ("console=ttyS0,115200n8", "console=ttyS0,115200n8"),
613            ("rd.lvm.lv=vg/root", "rd.lvm.lv=vg/root"),
614        ];
615        for (input, expected) in parse_cases {
616            let toml_str = format!(
617                r#"[install.ostree]
618bls-append-except-default = "{input}"
619"#
620            );
621            let c: InstallConfigurationToplevel = toml::from_str(&toml_str).unwrap();
622            assert_eq!(
623                c.install
624                    .unwrap()
625                    .ostree
626                    .unwrap()
627                    .bls_append_except_default
628                    .unwrap(),
629                expected
630            );
631        }
632
633        // Test merging: other config should override original
634        let mut install: InstallConfiguration = toml::from_str(
635            r#"[ostree]
636bls-append-except-default = "console=ttyS0"
637"#,
638        )
639        .unwrap();
640        let other = InstallConfiguration {
641            ostree: Some(OstreeRepoOpts {
642                bls_append_except_default: Some("console=tty0".to_string()),
643                ..Default::default()
644            }),
645            ..Default::default()
646        };
647        install.merge(other, &env);
648        assert_eq!(
649            install.ostree.unwrap().bls_append_except_default.unwrap(),
650            "console=tty0"
651        );
652    }
653
654    #[test]
655    fn test_parse_stateroot() {
656        let c: InstallConfigurationToplevel = toml::from_str(
657            r#"[install]
658stateroot = "custom"
659"#,
660        )
661        .unwrap();
662        assert_eq!(c.install.unwrap().stateroot.unwrap(), "custom");
663    }
664
665    #[test]
666    fn test_merge_stateroot() {
667        let env = EnvProperties {
668            sys_arch: "x86_64".to_string(),
669        };
670        let mut install: InstallConfiguration = toml::from_str(
671            r#"stateroot = "original"
672"#,
673        )
674        .unwrap();
675        let other = InstallConfiguration {
676            stateroot: Some("newroot".to_string()),
677            ..Default::default()
678        };
679        install.merge(other, &env);
680        assert_eq!(install.stateroot.unwrap(), "newroot");
681    }
682
683    #[test]
684    fn test_parse_mount_specs() {
685        let c: InstallConfigurationToplevel = toml::from_str(
686            r#"[install]
687root-mount-spec = "LABEL=rootfs"
688boot-mount-spec = "UUID=abcd-1234"
689"#,
690        )
691        .unwrap();
692        let install = c.install.unwrap();
693        assert_eq!(install.root_mount_spec.unwrap(), "LABEL=rootfs");
694        assert_eq!(install.boot_mount_spec.unwrap(), "UUID=abcd-1234");
695    }
696
697    #[test]
698    fn test_merge_mount_specs() {
699        let env = EnvProperties {
700            sys_arch: "x86_64".to_string(),
701        };
702        let mut install: InstallConfiguration = toml::from_str(
703            r#"root-mount-spec = "UUID=old"
704boot-mount-spec = "UUID=oldboot"
705"#,
706        )
707        .unwrap();
708        let other = InstallConfiguration {
709            root_mount_spec: Some("LABEL=newroot".to_string()),
710            ..Default::default()
711        };
712        install.merge(other, &env);
713        // root_mount_spec should be overridden
714        assert_eq!(install.root_mount_spec.as_deref().unwrap(), "LABEL=newroot");
715        // boot_mount_spec should remain unchanged
716        assert_eq!(install.boot_mount_spec.as_deref().unwrap(), "UUID=oldboot");
717    }
718
719    /// Empty mount specs are valid and signal to omit mount kargs entirely.
720    /// See https://github.com/bootc-dev/bootc/issues/1441
721    #[test]
722    fn test_parse_empty_mount_specs() {
723        let c: InstallConfigurationToplevel = toml::from_str(
724            r#"[install]
725root-mount-spec = ""
726boot-mount-spec = ""
727"#,
728        )
729        .unwrap();
730        let install = c.install.unwrap();
731        assert_eq!(install.root_mount_spec.as_deref().unwrap(), "");
732        assert_eq!(install.boot_mount_spec.as_deref().unwrap(), "");
733    }
734}