From fc289d70060691facbe7ff9949f76329607b85db Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 23 Sep 2026 06:49:26 -0400 Subject: [PATCH 1/8] lints: Don't require ostree bits on composefs-native images The baseimage-root lint insists on an /ostree -> sysroot/ostree symlink, and baseimage-composefs warns unless ostree's prepare-root.conf enables composefs. Both are meaningless for images that are only ever deployed with the composefs backend, and just force them to carry ostree cruft. Use the presence of /usr/lib/composefs/setup-root-conf.toml (even if empty) as the signal that an image is composefs-native, mirroring how prepare-root.conf signals ostree. If /ostree is present anyway it is still validated, and /sysroot is still required since both backends mount the physical root there. Closes: #2256 Generated-by: AI Signed-off-by: Colin Walters --- crates/lib/src/lints.rs | 51 ++++++++++++++++++++++++- docs/src/bootc-compatible-images.7.md | 6 ++- docs/src/man/bootc-setup-root-conf.5.md | 5 +++ 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/crates/lib/src/lints.rs b/crates/lib/src/lints.rs index f3c599d6d2..5555a36382 100644 --- a/crates/lib/src/lints.rs +++ b/crates/lib/src/lints.rs @@ -573,12 +573,17 @@ fn check_api_dirs(root: &Dir, _config: &LintExecutionConfig) -> LintResult { static LINT_COMPOSEFS: Lint = Lint::new_warning( "baseimage-composefs", indoc! { r#" -Check that composefs is enabled for ostree. More in +Check that composefs is enabled for ostree. Skipped for composefs-native +images, i.e. those that ship /usr/lib/composefs/setup-root-conf.toml. More in . "#}, check_composefs, ); fn check_composefs(dir: &Dir, _config: &LintExecutionConfig) -> LintResult { + // ostree's prepare-root.conf is irrelevant for composefs-native images. + if is_composefs_native(dir)? { + return lint_ok(); + } if let Err(e) = check_prepareroot_composefs_norecurse(dir)? { return Ok(Err(e)); } @@ -592,6 +597,20 @@ fn check_composefs(dir: &Dir, _config: &LintExecutionConfig) -> LintResult { lint_ok() } +/// The setup-root configuration, relative to the root directory. +fn setup_root_conf_path() -> &'static str { + bootc_initramfs_setup::SETUP_ROOT_CONF_PATH.trim_start_matches('/') +} + +/// Whether the image is intended to be deployed only with the composefs +/// backend, which is signaled by the presence of a setup-root configuration +/// file (even if empty). +fn is_composefs_native(root: &Dir) -> Result { + Ok(root + .symlink_metadata_optional(setup_root_conf_path())? + .is_some()) +} + /// Check for a few files and directories we expect in the base image. fn check_baseimage_root_norecurse(dir: &Dir, _config: &LintExecutionConfig) -> LintResult { // Check /sysroot @@ -604,6 +623,10 @@ fn check_baseimage_root_norecurse(dir: &Dir, _config: &LintExecutionConfig) -> L // Check /ostree -> sysroot/ostree let Some(meta) = dir.symlink_metadata_optional("ostree")? else { + // Composefs-native images don't use ostree, so they don't need it. + if is_composefs_native(dir)? { + return lint_ok(); + } return lint_err("Missing ostree -> sysroot/ostree link"); }; if !meta.is_symlink() { @@ -624,7 +647,9 @@ static LINT_BASEIMAGE_ROOT: Lint = Lint::new_fatal( "baseimage-root", indoc! { r#" Check that expected files are present in the root of the filesystem; such -as /sysroot and a composefs configuration for ostree. More in +as /sysroot and a composefs configuration for ostree. The /ostree symlink +is not required for composefs-native images, i.e. those that ship +/usr/lib/composefs/setup-root-conf.toml. More in . "#}, check_baseimage_root, @@ -1365,6 +1390,21 @@ mod tests { drop(td); let td = passing_fixture()?; check_baseimage_root(&td, config).unwrap().unwrap(); + + // Composefs-native images don't need /ostree... + td.remove_file("ostree")?; + assert!(check_baseimage_root(&td, config).unwrap().is_err()); + let conf = Utf8Path::new(setup_root_conf_path()); + td.create_dir_all(conf.parent().unwrap())?; + td.write(conf, "")?; + check_baseimage_root(&td, config).unwrap().unwrap(); + // ...but if they have it, it must still be correct + td.create_dir("ostree")?; + assert!(check_baseimage_root(&td, config).unwrap().is_err()); + td.remove_dir("ostree")?; + // ...and they still need /sysroot + td.remove_dir("sysroot")?; + assert!(check_baseimage_root(&td, config).unwrap().is_err()); Ok(()) } @@ -1388,6 +1428,13 @@ mod tests { // Now it should fail because composefs is explicitly disabled. assert!(check_composefs(&td, config).unwrap().is_err()); + // ...unless the image is composefs-native, where ostree's + // configuration doesn't matter. + let conf = Utf8Path::new(setup_root_conf_path()); + td.create_dir_all(conf.parent().unwrap())?; + td.write(conf, "")?; + check_composefs(&td, config).unwrap().unwrap(); + Ok(()) } diff --git a/docs/src/bootc-compatible-images.7.md b/docs/src/bootc-compatible-images.7.md index b98705df46..64ebf04165 100644 --- a/docs/src/bootc-compatible-images.7.md +++ b/docs/src/bootc-compatible-images.7.md @@ -27,7 +27,11 @@ For the composefs backend, the UKI must be located at `/boot/EFI/Linux/$kver.efi ### /ostree symlink and `bootc container lint` -This is [a bug](https://github.com/bootc-dev/bootc/issues/2256): currently a `/ostree -> /sysroot/ostree` symlink is required just for `bootc container lint` to pass, even though it's not required for `/sysroot/ostree` to exist. +`bootc container lint` requires a `/ostree -> sysroot/ostree` symlink, +unless the image is composefs-native, which is signaled by the presence of +`/usr/lib/composefs/setup-root-conf.toml` (it may be empty); see +[bootc-setup-root-conf.toml(5)](man/bootc-setup-root-conf.5.md). Such +images also don't need ostree's `prepare-root.conf` to enable composefs. ## composefs backend diff --git a/docs/src/man/bootc-setup-root-conf.5.md b/docs/src/man/bootc-setup-root-conf.5.md index 3e7082f06b..08d6f405c9 100644 --- a/docs/src/man/bootc-setup-root-conf.5.md +++ b/docs/src/man/bootc-setup-root-conf.5.md @@ -15,6 +15,11 @@ mounted. If the file does not exist all options take their documented defaults. +The presence of this file (even if empty) also marks the image as +composefs-native; `bootc container lint` then no longer requires the +`/ostree` symlink or the ostree `prepare-root.conf` composefs configuration +used by the ostree backend. + The `51bootc` dracut module installs this file into the initramfs automatically when it is present on the host image. Image authors can therefore ship the file at this path in their container image and rebuild the initramfs with a From 35ae44e5121e21893a580d917ac61949e22fcfc2 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 29 Sep 2026 13:11:13 -0400 Subject: [PATCH 2/8] lints: Move composefs-native detection next to the composefs code Prep for bootc install using the same signal to pick its default backend. Generated-by: AI Signed-off-by: Colin Walters --- crates/lib/src/bootc_composefs/image.rs | 19 +++++++++++++++++++ crates/lib/src/bootc_composefs/mod.rs | 1 + crates/lib/src/lints.rs | 16 ++-------------- 3 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 crates/lib/src/bootc_composefs/image.rs diff --git a/crates/lib/src/bootc_composefs/image.rs b/crates/lib/src/bootc_composefs/image.rs new file mode 100644 index 0000000000..2c7f3ac3ac --- /dev/null +++ b/crates/lib/src/bootc_composefs/image.rs @@ -0,0 +1,19 @@ +//! Detecting which backend a container image's root filesystem is built for. + +use anyhow::Result; +use cap_std_ext::cap_std::fs::Dir; +use cap_std_ext::dirext::CapStdExtDirExt as _; + +/// The setup-root configuration, relative to the root directory. +pub(crate) fn setup_root_conf_path() -> &'static str { + bootc_initramfs_setup::SETUP_ROOT_CONF_PATH.trim_start_matches('/') +} + +/// Whether the image is intended to be deployed with the composefs +/// backend, which is signaled by the presence of a setup-root configuration +/// file (even if empty). +pub(crate) fn is_composefs_native(root: &Dir) -> Result { + Ok(root + .symlink_metadata_optional(setup_root_conf_path())? + .is_some()) +} diff --git a/crates/lib/src/bootc_composefs/mod.rs b/crates/lib/src/bootc_composefs/mod.rs index 42d521150a..fe5bc9c6a5 100644 --- a/crates/lib/src/bootc_composefs/mod.rs +++ b/crates/lib/src/bootc_composefs/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod digest; pub(crate) mod export; pub(crate) mod finalize; pub(crate) mod gc; +pub(crate) mod image; pub(crate) mod progress; pub(crate) mod repo; pub(crate) mod rollback; diff --git a/crates/lib/src/lints.rs b/crates/lib/src/lints.rs index 5555a36382..e997f0ca2f 100644 --- a/crates/lib/src/lints.rs +++ b/crates/lib/src/lints.rs @@ -28,6 +28,7 @@ use ostree_ext::ostree_prepareroot; use serde::Serialize; use crate::bootc_composefs::boot::EFI_LINUX; +use crate::bootc_composefs::image::is_composefs_native; /// Create a default WalkConfiguration with noxdev enabled. /// @@ -597,20 +598,6 @@ fn check_composefs(dir: &Dir, _config: &LintExecutionConfig) -> LintResult { lint_ok() } -/// The setup-root configuration, relative to the root directory. -fn setup_root_conf_path() -> &'static str { - bootc_initramfs_setup::SETUP_ROOT_CONF_PATH.trim_start_matches('/') -} - -/// Whether the image is intended to be deployed only with the composefs -/// backend, which is signaled by the presence of a setup-root configuration -/// file (even if empty). -fn is_composefs_native(root: &Dir) -> Result { - Ok(root - .symlink_metadata_optional(setup_root_conf_path())? - .is_some()) -} - /// Check for a few files and directories we expect in the base image. fn check_baseimage_root_norecurse(dir: &Dir, _config: &LintExecutionConfig) -> LintResult { // Check /sysroot @@ -974,6 +961,7 @@ mod tests { use std::sync::LazyLock; use super::*; + use crate::bootc_composefs::image::setup_root_conf_path; static ALTROOT_LINTS: LazyLock = LazyLock::new(|| { LINTS From 643a10271a2c31e7b7ed76dd6accf162fa11741c Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 29 Sep 2026 13:11:13 -0400 Subject: [PATCH 3/8] install: Default to the composefs backend for composefs-only images Today only a UKI selects the composefs backend automatically. An image built for composefs with a traditional kernel and initramfs (BLS) is installed with ostree unless every caller passes --composefs-backend, which bootc-image-builder and Anaconda don't. The lints already treat /usr/lib/composefs/setup-root-conf.toml as the marker of a composefs-native image. Use the same marker here: when an image ships it and has no ostree prepare-root.conf, it can't be installed with ostree anyway, so default to composefs. An image with both is still installed with ostree by default, since it may be meant for either backend: bootc's own composefs CI images, for example, add setup-root-conf.toml to a stock base image and pass the flag. Both files are read from the root bootc runs in, as the install configuration and prepare-root.conf already were, including with --source-imgref: bootc-image-builder runs bootc from the image it installs, so it gets the same default. Such an image also failed with --composefs-backend, since install required prepare-root.conf regardless of the backend; only the ostree backend reads it, so it's optional for composefs now. --allow-missing-verity and --uki-addon used to require --composefs-backend at the clap level, which would reject them for an image selecting the backend by itself, so check them after the backend is decided. Generated-by: AI Signed-off-by: Colin Walters --- crates/lib/src/bootc_composefs/image.rs | 53 ++++++++ crates/lib/src/install.rs | 122 ++++++++++++++---- docs/src/bootc-experimental-composefs.7.md | 7 + docs/src/man/bootc-install-to-disk.8.md | 6 +- .../man/bootc-install-to-existing-root.8.md | 6 +- docs/src/man/bootc-install-to-filesystem.8.md | 6 +- docs/src/man/bootc-setup-root-conf.5.md | 2 + 7 files changed, 166 insertions(+), 36 deletions(-) diff --git a/crates/lib/src/bootc_composefs/image.rs b/crates/lib/src/bootc_composefs/image.rs index 2c7f3ac3ac..f6691f75e3 100644 --- a/crates/lib/src/bootc_composefs/image.rs +++ b/crates/lib/src/bootc_composefs/image.rs @@ -17,3 +17,56 @@ pub(crate) fn is_composefs_native(root: &Dir) -> Result { .symlink_metadata_optional(setup_root_conf_path())? .is_some()) } + +/// Whether `bootc install` should default to the composefs backend for this +/// root: it is composefs-native, and it has no ostree `prepare-root.conf` +/// (`has_ostree_prepareroot`, which the caller loads anyway), without which +/// it can't be installed with the ostree backend. An image that has both +/// configurations still defaults to ostree. +pub(crate) fn defaults_to_composefs_backend( + root: &Dir, + has_ostree_prepareroot: bool, +) -> Result { + Ok(!has_ostree_prepareroot && is_composefs_native(root)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use camino::Utf8Path; + use ostree_ext::ostree_prepareroot; + + const OSTREE_PREPAREROOT: &str = "usr/lib/ostree/prepare-root.conf"; + const OSTREE_PREPAREROOT_ETC: &str = "etc/ostree/prepare-root.conf"; + + #[test] + fn test_defaults_to_composefs_backend() -> Result<()> { + let setup_root = setup_root_conf_path(); + // (files present in the image) => (composefs-native, defaults to composefs) + let cases: &[(&[&str], bool, bool)] = &[ + (&[], false, false), + (&[setup_root], true, true), + (&[OSTREE_PREPAREROOT], false, false), + (&[OSTREE_PREPAREROOT_ETC], false, false), + (&[setup_root, OSTREE_PREPAREROOT], true, false), + (&[setup_root, OSTREE_PREPAREROOT_ETC], true, false), + ]; + for &(files, native, composefs) in cases { + let td = cap_std_ext::cap_tempfile::tempdir(cap_std_ext::cap_std::ambient_authority())?; + for f in files { + let f = Utf8Path::new(f); + td.create_dir_all(f.parent().unwrap())?; + // Both files are signals even if empty + td.write(f, "")?; + } + assert_eq!(is_composefs_native(&td)?, native, "{files:?}"); + let has_ostree = ostree_prepareroot::load_config_from_root(&td)?.is_some(); + assert_eq!( + defaults_to_composefs_backend(&td, has_ostree)?, + composefs, + "{files:?}" + ); + } + Ok(()) + } +} diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index b2654b867c..b0aa39f267 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -397,23 +397,49 @@ pub(crate) struct InstallConfigOpts { #[derive(Debug, Default, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallComposefsOpts { - /// If true, composefs backend is used, else ostree backend is used + /// Use the composefs backend instead of ostree. This is the default for images with a UKI, + /// and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf #[clap(long, default_value_t)] #[serde(default)] pub(crate) composefs_backend: bool, /// Make fs-verity validation optional in case the filesystem doesn't support it - #[clap(long, default_value_t, requires = "composefs_backend")] + /// (composefs backend only) + #[clap(long, default_value_t)] #[serde(default)] pub(crate) allow_missing_verity: bool, /// Name of the UKI addons to install without the ".efi.addon" suffix. - /// This option can be provided multiple times if multiple addons are to be installed. - #[clap(long, requires = "composefs_backend")] + /// This option can be provided multiple times if multiple addons are to be installed + /// (composefs backend only). + #[clap(long)] #[serde(default)] pub(crate) uki_addon: Option>, } +impl InstallComposefsOpts { + /// Check that the options fit together, once `composefs_backend` says + /// whether the composefs backend is used (passed, or selected by the image). + pub(crate) fn validate(&self, bootloader: Option<&Bootloader>) -> Result<()> { + if self.composefs_backend { + anyhow::ensure!( + !matches!(bootloader, Some(Bootloader::None)), + "Bootloader set to none is not supported with the composefs backend" + ); + } else { + anyhow::ensure!( + !self.allow_missing_verity, + "--allow-missing-verity requires the composefs backend" + ); + anyhow::ensure!( + self.uki_addon.is_none(), + "--uki-addon requires the composefs backend" + ); + } + Ok(()) + } +} + #[cfg(feature = "install-to-disk")] #[derive(Debug, Clone, clap::Parser, Serialize, Deserialize, PartialEq, Eq)] pub(crate) struct InstallToDiskOpts { @@ -635,7 +661,7 @@ pub(crate) struct State { pub(crate) target_opts: InstallTargetOpts, pub(crate) target_imgref: ostree_container::OstreeImageReference, #[allow(dead_code)] - pub(crate) prepareroot_config: HashMap, + pub(crate) ostree_prepareroot_config: HashMap, pub(crate) install_config: Option, /// The parsed contents of the authorized_keys (not the file path) pub(crate) root_ssh_authorized_keys: Option, @@ -1004,7 +1030,7 @@ async fn initialize_ostree_root(state: &State, root_setup: &RootSetup) -> Result let repo_verity_state = ostree_ext::fsverity::is_verity_enabled(&repo)?; let prepare_root_composefs = state - .prepareroot_config + .ostree_prepareroot_config .get("composefs.enabled") .map(|v| ComposefsState::from_str(&v)) .transpose()? @@ -1724,15 +1750,33 @@ async fn prepare_install( tracing::debug!("Composefs required: {composefs_required}"); - if composefs_required { - composefs_options.composefs_backend = true; - } + // ostree's prepare-root.conf is read from the running root even with + // --source-imgref, like the install configuration: tools such as + // bootc-image-builder run bootc from the image they install. Convert the + // keyfile to a hashmap because GKeyFile isnt Send for probably bad reasons. + let ostree_prepareroot_config = ostree_prepareroot::load_config_from_root(&rootfs)? + .map(|kf| -> Result> { + let mut r = HashMap::new(); + for grp in kf.groups() { + for key in kf.keys(&grp)? { + let key = key.as_str(); + let value = kf.value(&grp, key)?; + r.insert(format!("{grp}.{key}"), value.to_string()); + } + } + Ok(r) + }) + .transpose()?; - if composefs_options.composefs_backend - && matches!(config_opts.bootloader, Some(Bootloader::None)) - { - anyhow::bail!("Bootloader set to none is not supported with the composefs backend"); - } + // A UKI requires the composefs backend, and a composefs-native image + // without ostree's configuration defaults to it. + let composefs_default = crate::bootc_composefs::image::defaults_to_composefs_backend( + &rootfs, + ostree_prepareroot_config.is_some(), + )?; + tracing::debug!("Composefs default: {composefs_default}"); + composefs_options.composefs_backend |= composefs_required || composefs_default; + composefs_options.validate(config_opts.bootloader.as_ref())?; // Read the file eagerly so we error out early, and before the mount changes // below hide a file bind mounted under e.g. /tmp. We may re-exec further down @@ -1867,18 +1911,15 @@ async fn prepare_install( } } - // Convert the keyfile to a hashmap because GKeyFile isnt Send for probably bad reasons. - let prepareroot_config = { - let kf = ostree_prepareroot::require_config_from_root(&rootfs)?; - let mut r = HashMap::new(); - for grp in kf.groups() { - for key in kf.keys(&grp)? { - let key = key.as_str(); - let value = kf.value(&grp, key)?; - r.insert(format!("{grp}.{key}"), value.to_string()); - } - } - r + // Only the ostree backend uses prepare-root.conf, and composefs-native + // images needn't have one. + let ostree_prepareroot_config = match ostree_prepareroot_config { + Some(c) => c, + None if composefs_options.composefs_backend => HashMap::new(), + None => anyhow::bail!( + "Failed to find {} in /usr/lib or /etc", + ostree_prepareroot::CONF_PATH + ), }; // Create our global (read-only) state which gets wrapped in an Arc @@ -1891,7 +1932,7 @@ async fn prepare_install( target_opts, target_imgref, install_config, - prepareroot_config, + ostree_prepareroot_config, root_ssh_authorized_keys, container_root: rootfs, tempdir, @@ -3079,6 +3120,33 @@ pub(crate) async fn install_finalize(target: &Utf8Path) -> Result<()> { mod tests { use super::*; + #[test] + fn test_composefs_opts_validate() { + let addon = || Some(vec!["addon".to_string()]); + // (composefs_backend, allow_missing_verity, uki_addon, bootloader, valid) + let cases = [ + (false, false, None, None, true), + (false, false, None, Some(Bootloader::None), true), + (false, true, None, None, false), + (false, false, addon(), None, false), + (true, false, None, None, true), + (true, true, addon(), Some(Bootloader::Systemd), true), + (true, false, None, Some(Bootloader::None), false), + ]; + for (composefs_backend, allow_missing_verity, uki_addon, bootloader, valid) in cases { + let opts = InstallComposefsOpts { + composefs_backend, + allow_missing_verity, + uki_addon, + }; + assert_eq!( + opts.validate(bootloader.as_ref()).is_ok(), + valid, + "{opts:?} {bootloader:?}" + ); + } + } + #[test] #[cfg(feature = "install-to-disk")] fn install_opts_serializable() { diff --git a/docs/src/bootc-experimental-composefs.7.md b/docs/src/bootc-experimental-composefs.7.md index ee8b22004b..d99217cb0d 100644 --- a/docs/src/bootc-experimental-composefs.7.md +++ b/docs/src/bootc-experimental-composefs.7.md @@ -276,6 +276,13 @@ Composefs installs using a traditional `vmlinuz`/`initramfs.img` layout instead There is a `--composefs-backend` option for `bootc install` to explicitly select a composefs backend apart from sealed images; this is not as heavily tested yet. +An image built only for the composefs backend selects it by itself: if it ships +`/usr/lib/composefs/setup-root-conf.toml` (see [bootc-setup-root-conf.toml(5)](man/bootc-setup-root-conf.5.md); +it may be empty) and no ostree `prepare-root.conf` (in `/usr/lib/ostree` or `/etc/ostree`), +`bootc install` uses composefs without the flag. Like the install configuration, these files are +read from the root bootc runs in, also with `--source-imgref`, so this applies to tools like +bootc-image-builder that run bootc from the image they install. An image that ships both is installed with ostree. + ## Known issues The composefs backend is experimental; on-disk formats are subject to change. diff --git a/docs/src/man/bootc-install-to-disk.8.md b/docs/src/man/bootc-install-to-disk.8.md index 9c11739480..776c01ab11 100644 --- a/docs/src/man/bootc-install-to-disk.8.md +++ b/docs/src/man/bootc-install-to-disk.8.md @@ -176,19 +176,19 @@ set `discoverable-partitions = true` in their install configuration **--composefs-backend** - If true, composefs backend is used, else ostree backend is used + Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf Default: false **--allow-missing-verity** - Make fs-verity validation optional in case the filesystem doesn't support it + Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only) Default: false **--uki-addon**=*UKI_ADDON* - Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed + Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only) diff --git a/docs/src/man/bootc-install-to-existing-root.8.md b/docs/src/man/bootc-install-to-existing-root.8.md index 973c174f84..3ff11166ed 100644 --- a/docs/src/man/bootc-install-to-existing-root.8.md +++ b/docs/src/man/bootc-install-to-existing-root.8.md @@ -217,19 +217,19 @@ of migrating the fstab entries. See the "Injecting kernel arguments" section abo **--composefs-backend** - If true, composefs backend is used, else ostree backend is used + Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf Default: false **--allow-missing-verity** - Make fs-verity validation optional in case the filesystem doesn't support it + Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only) Default: false **--uki-addon**=*UKI_ADDON* - Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed + Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only) diff --git a/docs/src/man/bootc-install-to-filesystem.8.md b/docs/src/man/bootc-install-to-filesystem.8.md index 2d2b8b6f62..9549e156b2 100644 --- a/docs/src/man/bootc-install-to-filesystem.8.md +++ b/docs/src/man/bootc-install-to-filesystem.8.md @@ -126,19 +126,19 @@ is currently expected to be empty by default. **--composefs-backend** - If true, composefs backend is used, else ostree backend is used + Use the composefs backend instead of ostree. This is the default for images with a UKI, and for images with /usr/lib/composefs/setup-root-conf.toml and no ostree prepare-root.conf Default: false **--allow-missing-verity** - Make fs-verity validation optional in case the filesystem doesn't support it + Make fs-verity validation optional in case the filesystem doesn't support it (composefs backend only) Default: false **--uki-addon**=*UKI_ADDON* - Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed + Name of the UKI addons to install without the ".efi.addon" suffix. This option can be provided multiple times if multiple addons are to be installed (composefs backend only) diff --git a/docs/src/man/bootc-setup-root-conf.5.md b/docs/src/man/bootc-setup-root-conf.5.md index 08d6f405c9..d190d2f7e7 100644 --- a/docs/src/man/bootc-setup-root-conf.5.md +++ b/docs/src/man/bootc-setup-root-conf.5.md @@ -19,6 +19,8 @@ The presence of this file (even if empty) also marks the image as composefs-native; `bootc container lint` then no longer requires the `/ostree` symlink or the ostree `prepare-root.conf` composefs configuration used by the ostree backend. +If the image also has no ostree `prepare-root.conf`, `bootc install` +defaults to the composefs backend, without `--composefs-backend`. The `51bootc` dracut module installs this file into the initramfs automatically when it is present on the host image. Image authors can therefore ship the From 616048c50573eb757087ec89063c0c1f041adfc0 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 29 Sep 2026 13:11:13 -0400 Subject: [PATCH 4/8] tests: Install a composefs-native image without --composefs-backend Build a derived image that adds an empty setup-root-conf.toml and drops ostree's prepare-root.conf, and check that `bootc install to-disk` run from it installs the composefs backend without the flag, both as a self-install and with --source-imgref as bootc-image-builder runs it. An image that keeps prepare-root.conf must still get ostree. On the ostree variant, nothing else selects composefs. Generated-by: AI Signed-off-by: Colin Walters --- tmt/plans/integration.fmf | 8 ++ .../booted/test-install-composefs-native.nu | 77 +++++++++++++++++++ tmt/tests/tests.fmf | 5 ++ 3 files changed, 90 insertions(+) create mode 100644 tmt/tests/booted/test-install-composefs-native.nu diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index cf08422d9e..2042e3cdb2 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -355,4 +355,12 @@ execute: - /tmt/tests/tests/test-57-composefs-separate-boot extra-skip_if_ostree: true extra-fixme_skip_if_uki: true + +/plan-60-install-composefs-native: + summary: Test that composefs-native images default to the composefs backend + discover: + how: fmf + test: + - /tmt/tests/tests/test-60-install-composefs-native + extra-fixme_skip_if_uki: true # END GENERATED PLANS diff --git a/tmt/tests/booted/test-install-composefs-native.nu b/tmt/tests/booted/test-install-composefs-native.nu new file mode 100644 index 0000000000..07925492b0 --- /dev/null +++ b/tmt/tests/booted/test-install-composefs-native.nu @@ -0,0 +1,77 @@ +# number: 60 +# tmt: +# summary: Test that composefs-native images default to the composefs backend +# duration: 45m +# extra: +# # A UKI selects the composefs backend by itself, and a derived layer +# # wouldn't match the composefs digest embedded in it. +# fixme_skip_if_uki: true +# +# An image that ships /usr/lib/composefs/setup-root-conf.toml and no ostree +# prepare-root.conf must be installed with the composefs backend by +# `bootc install` run from that image without `--composefs-backend`, both +# as a self-install and with --source-imgref (as bootc-image-builder does). +# An image with both files is still installed with ostree. This runs on the +# ostree variant too, where nothing else selects composefs. + +use std assert +use tap.nu + +const NATIVE = "localhost/bootc-composefs-native" +const BOTH = "localhost/bootc-composefs-both" +const DISK = "/var/tmp/composefs-native.img" +const MNT = "/var/mnt/composefs-native" + +def build [image: string, extra: string] { + let td = mktemp -d + $"FROM localhost/bootc +RUN rm -rf /usr/lib/bootc/bound-images.d/* +RUN mkdir -p /usr/lib/composefs && touch /usr/lib/composefs/setup-root-conf.toml +($extra) +" | save $"($td)/Containerfile" + # Keep an OCI manifest, see https://github.com/bootc-dev/bootc/issues/1703 + podman build --format oci -t $image $td + rm -rf $td +} + +# Install `image` from itself and return the backend found on the disk +def install [image: string, ...args: string] { + truncate -s 15G $DISK + (podman run --rm --privileged --pid=host + --security-opt label=type:unconfined_t + -v /dev:/dev -v /var/lib/containers:/var/lib/containers -v /var/tmp:/var/tmp + $image + bootc install to-disk --disable-selinux --via-loopback ...$args $DISK) + + # Inspect the root partition of the installed disk + let parts = sfdisk --json $DISK | from json | get partitiontable + let root = $parts.partitions | where name == "root" | first + let offset = $root.start * ($parts.sectorsize? | default 512) + mkdir $MNT + mount -o $"ro,loop,offset=($offset)" $DISK $MNT + let composefs = ($"($MNT)/composefs" | path exists) and ((ls $"($MNT)/state/deploy" | length) == 1) + let ostree = ($"($MNT)/ostree/deploy" | path exists) + umount $MNT + rm -f $DISK + match [$composefs $ostree] { + [true false] => "composefs", + [false true] => "ostree", + _ => $"unexpected: composefs=($composefs) ostree=($ostree)", + } +} + +def main [] { + tap begin "composefs-native images default to the composefs backend" + + bootc image copy-to-storage + build $NATIVE "RUN rm -f /usr/lib/ostree/prepare-root.conf /etc/ostree/prepare-root.conf" + build $BOTH "" + + assert equal (install $NATIVE) "composefs" "composefs-native self-install" + let src = $"--source-imgref=containers-storage:($NATIVE)" + assert equal (install $NATIVE $src) "composefs" "composefs-native with --source-imgref" + assert equal (install $BOTH) "ostree" "image with both configurations" + + podman rmi $NATIVE $BOTH + tap ok +} diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index b4a1af34de..c78d42d7d0 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -224,3 +224,8 @@ check: - dosfstools - e2fsprogs test: nu booted/test-composefs-separate-boot.nu + +/test-60-install-composefs-native: + summary: Test that composefs-native images default to the composefs backend + duration: 45m + test: nu booted/test-install-composefs-native.nu From e0db806d92f0435c93ffdef33295755111b5a311 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 30 Sep 2026 08:34:49 -0400 Subject: [PATCH 5/8] docs: Describe how bootc install picks the storage backend Image authors who want their image installed with composefs by default, and only with composefs, need one place stating which rules the image has to match; so far that was only spelled out in the composefs and setup-root-conf.toml pages. Generated-by: AI Signed-off-by: Colin Walters --- docs/src/bootc-installation.7.md | 17 +++++++++++++++++ docs/src/man/bootc-setup-root-conf.5.md | 11 ++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/src/bootc-installation.7.md b/docs/src/bootc-installation.7.md index 90154c5332..3a87689c34 100644 --- a/docs/src/bootc-installation.7.md +++ b/docs/src/bootc-installation.7.md @@ -118,6 +118,23 @@ For example, a derived image can supply `50-myos.toml` with See [bootc-install-config](man/bootc-install-config.5.md) for file discovery, merge precedence, and the available configuration fields. +### The storage backend + +The storage backend is determined by the image. It is installed with the +[experimental composefs backend](bootc-experimental-composefs.7.md) when it +ships a UKI, or when it matches both of these rules: + +- it ships `/usr/lib/composefs/setup-root-conf.toml` (which may be empty; see + [bootc-setup-root-conf.toml(5)](man/bootc-setup-root-conf.5.md)); +- it has no ostree `prepare-root.conf`, in either `/usr/lib/ostree` or `/etc/ostree`. + +Any other image is installed with ostree. `bootc container lint` uses the same +`setup-root-conf.toml` marker to stop requiring the ostree-specific parts of +an image, such as the `/ostree` symlink. + +Like the install configuration, these files are read from the root `bootc` runs +in, also with `--source-imgref`. + ## Installing an "unconfigured" image The bootc project aims to support generic/general-purpose operating diff --git a/docs/src/man/bootc-setup-root-conf.5.md b/docs/src/man/bootc-setup-root-conf.5.md index d190d2f7e7..d4c3350c87 100644 --- a/docs/src/man/bootc-setup-root-conf.5.md +++ b/docs/src/man/bootc-setup-root-conf.5.md @@ -16,11 +16,12 @@ mounted. If the file does not exist all options take their documented defaults. The presence of this file (even if empty) also marks the image as -composefs-native; `bootc container lint` then no longer requires the -`/ostree` symlink or the ostree `prepare-root.conf` composefs configuration -used by the ostree backend. -If the image also has no ostree `prepare-root.conf`, `bootc install` -defaults to the composefs backend, without `--composefs-backend`. +composefs-native, which decides the storage backend `bootc install` uses: +if the image also has no ostree `prepare-root.conf`, it is installed with +the composefs backend, and otherwise with ostree (see +[bootc-installation(7)](../bootc-installation.7.md)). `bootc container lint` +then also no longer requires the `/ostree` symlink or the ostree +`prepare-root.conf` composefs configuration used by the ostree backend. The `51bootc` dracut module installs this file into the initramfs automatically when it is present on the host image. Image authors can therefore ship the From a367584a11181cae25929c251b40edc8c146625a Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 30 Sep 2026 10:01:48 -0400 Subject: [PATCH 6/8] ci: Install the composefs test images without --composefs-backend The composefs test images were only installed with composefs because every CI path passed --composefs-backend, so CI never exercised the way composefs-native images are meant to select the backend. Build them like such an image instead: ship setup-root-conf.toml (empty unless a baseconfig fills it), drop ostree's prepare-root.conf, and let install pick composefs by itself. This covers the sealed and unsealed UKI variants too; a UKI already selected composefs, and the marker doesn't hurt there. bcvk only takes --bootloader together with --composefs-backend, so the images now name their bootloader in an install configuration file. BOOTC_variant=composefs still selects the composefs plans and filesystem in run-tmt. test-upgrade keeps passing the flag, since it installs the published base image first, which isn't composefs-native. Generated-by: AI Signed-off-by: Colin Walters --- .github/workflows/ci.yml | 2 +- Dockerfile | 14 +++++ Justfile | 7 +-- contrib/packaging/inject-baseconfig | 18 +++--- crates/xtask/src/bcvk.rs | 58 +++++++++++++++++-- crates/xtask/src/tmt.rs | 3 +- crates/xtask/src/xtask.rs | 29 ++++++---- .../booted/test-install-composefs-native.nu | 11 +++- .../booted/test-install-outside-container.nu | 2 +- .../test-install-to-filesystem-var-mount.sh | 1 - 10 files changed, 111 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94601538e2..256f1a9922 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -552,7 +552,7 @@ jobs: cargo xtask run-tmt \ --env=BOOTC_variant=composefs \ --env=BOOTC_baseconfigs=${{ matrix.baseconfigs }} \ - --composefs-backend --bootloader=grub --filesystem=ext4 \ + --filesystem=ext4 \ --seal-state=unsealed --boot-type=bls \ --upgrade-image=localhost/bootc-upgrade \ localhost/bootc readonly diff --git a/Dockerfile b/Dockerfile index c6b825dbad..a36bc56388 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,6 +89,14 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \ --install system-reinstall-bootc \ --add-dir /var/add-dir/usr \ --manifest=standard /target-rootfs + + # Composefs test images signal the backend the way composefs-native images + # are meant to (see bootc-installation(7)): inject-baseconfig ships + # setup-root-conf.toml, and there must be no ostree prepare-root.conf, so + # that `bootc install` picks composefs without --composefs-backend. + if [[ "${variant}" == composefs* ]]; then + rm -vf /target-rootfs/usr/lib/ostree/prepare-root.conf /target-rootfs/etc/ostree/prepare-root.conf + fi EOF RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp < /usr/lib/bootc/install/80-composefs-bootloader.toml +fi + if [[ "${boot_type}" == "uki" ]]; then cp /run/packaging/seal-uki /usr/bin/seal-uki cp /run/packaging/finalize-uki /usr/bin/finalize-uki diff --git a/Justfile b/Justfile index 59bd8d64b8..afcb3a1984 100644 --- a/Justfile +++ b/Justfile @@ -228,8 +228,7 @@ test-composefs bootloader filesystem boot_type seal_state *ARGS: filesystem={{filesystem}} \ boot_type={{boot_type}} \ seal_state={{seal_state}} \ - test-tmt --composefs-backend \ - --bootloader={{bootloader}} \ + test-tmt \ --filesystem={{filesystem}} \ --seal-state={{seal_state}} \ --boot-type={{boot_type}} \ @@ -246,6 +245,8 @@ test-upgrade *ARGS: build _build-upgrade-source-image set -xeuo pipefail composefs_args=() if [[ "{{variant}}" = composefs ]]; then + # Unlike the composefs test images, the published base image doesn't + # select the composefs backend itself, and its bootc may predate that. composefs_args=(--composefs-backend \ --bootloader={{bootloader}} \ --filesystem={{filesystem}} \ @@ -308,8 +309,6 @@ test-tmt-baseconfig baseconfig *ARGS: --env=BOOTC_erofs_version={{erofs_version}} \ --env=BOOTC_baseconfigs={{baseconfig}} \ --upgrade-image={{upgrade_img}} \ - --composefs-backend \ - --bootloader={{bootloader}} \ --filesystem={{filesystem}} \ --boot-type={{boot_type}} \ --seal-state={{seal_state}} \ diff --git a/contrib/packaging/inject-baseconfig b/contrib/packaging/inject-baseconfig index 4d1b2cf6f1..ac6c9ca805 100755 --- a/contrib/packaging/inject-baseconfig +++ b/contrib/packaging/inject-baseconfig @@ -10,25 +10,25 @@ BASE_DIR="${1:-/}" VARIANT="${2:-}" BASECONFIGS="${3:-}" -# No-op if no baseconfigs specified -if [ -z "${BASECONFIGS}" ]; then - exit 0 -fi - # setup-root-conf.toml is composefs-specific; ostree uses prepare-root.conf # which has a different (INI) format and different option names. case "${VARIANT}" in composefs*) TARGET="${BASE_DIR}/usr/lib/composefs/setup-root-conf.toml" + # Always ship it, even empty: it marks the image as composefs-native, so + # `bootc install` picks the composefs backend without --composefs-backend. + mkdir -p "$(dirname "${TARGET}")" + touch "${TARGET}" ;; *) - echo "inject-baseconfig: baseconfigs not supported for variant '${VARIANT}'" >&2 - exit 1 + if [ -n "${BASECONFIGS}" ]; then + echo "inject-baseconfig: baseconfigs not supported for variant '${VARIANT}'" >&2 + exit 1 + fi + exit 0 ;; esac -mkdir -p "$(dirname "${TARGET}")" - # Split on commas and process each token IFS=',' read -ra TOKENS <<< "${BASECONFIGS}" for raw_token in "${TOKENS[@]}"; do diff --git a/crates/xtask/src/bcvk.rs b/crates/xtask/src/bcvk.rs index 520640ebf8..03791e6044 100644 --- a/crates/xtask/src/bcvk.rs +++ b/crates/xtask/src/bcvk.rs @@ -19,7 +19,13 @@ const DEFAULT_SB_KEYS_DIR: &str = "target/test-secureboot"; /// or populate fields directly. #[derive(Debug, Default)] pub(crate) struct BcvkInstallOpts { + /// Pass `--composefs-backend` (and `--bootloader`, which bcvk only + /// takes with it). Images that select the composefs backend themselves, + /// like bootc's composefs test images, don't need it. pub(crate) composefs_backend: bool, + /// Whether the image is installed with the composefs backend, with or + /// without `composefs_backend`. + pub(crate) composefs: bool, pub(crate) bootloader: Option, pub(crate) filesystem: Option, pub(crate) seal_state: Option, @@ -29,7 +35,8 @@ pub(crate) struct BcvkInstallOpts { impl BcvkInstallOpts { /// Build from `BOOTC_*` environment variables. /// - /// `BOOTC_variant=composefs` implies `composefs_backend = true`. + /// `BOOTC_variant=composefs` implies `composefs_backend = true`, since the + /// dev VM may fall back to a stock base image. pub(crate) fn from_env() -> Self { let composefs_backend = std::env::var("BOOTC_variant") .map(|v| v == "composefs") @@ -55,6 +62,7 @@ impl BcvkInstallOpts { Self { composefs_backend, + composefs: composefs_backend, bootloader, filesystem, seal_state, @@ -65,15 +73,19 @@ impl BcvkInstallOpts { /// Return the install-related args for `bcvk libvirt run`. /// /// This covers `--composefs-backend`, `--filesystem`, `--bootloader`, - /// and `--karg` flags. Note that `--bootloader` and `--filesystem` - /// are only valid when `--composefs-backend` is also set (bcvk - /// enforces this via a clap `requires` relationship). + /// and `--karg` flags. Note that `--bootloader` is only valid when + /// `--composefs-backend` is also set (bcvk enforces this via a clap + /// `requires` relationship). pub(crate) fn install_args(&self) -> Vec { let mut args = Vec::new(); if self.composefs_backend { args.push("--composefs-backend".into()); + } + if self.composefs_backend || self.composefs { let fs = self.filesystem.as_deref().unwrap_or("ext4"); args.push(format!("--filesystem={fs}")); + } + if self.composefs_backend { if let Some(b) = &self.bootloader { args.push(format!("--bootloader={b}")); } @@ -121,3 +133,41 @@ impl BcvkInstallOpts { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_install_args() { + // (composefs_backend, composefs) => expected args + let cases: &[(bool, bool, &[&str])] = &[ + (false, false, &[]), + // A composefs-native image selects the backend itself + (false, true, &["--filesystem=xfs"]), + ( + true, + true, + &[ + "--composefs-backend", + "--filesystem=xfs", + "--bootloader=systemd", + ], + ), + ]; + for &(composefs_backend, composefs, expected) in cases { + let opts = BcvkInstallOpts { + composefs_backend, + composefs, + bootloader: Some(Bootloader::Systemd), + filesystem: Some("xfs".into()), + ..Default::default() + }; + assert_eq!( + opts.install_args(), + expected, + "composefs_backend={composefs_backend} composefs={composefs}" + ); + } + } +} diff --git a/crates/xtask/src/tmt.rs b/crates/xtask/src/tmt.rs index 4836aea229..ed7e7fea54 100644 --- a/crates/xtask/src/tmt.rs +++ b/crates/xtask/src/tmt.rs @@ -400,6 +400,7 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { let bcvk_opts = BcvkInstallOpts { composefs_backend: args.composefs_backend, + composefs: args.composefs, bootloader: args.bootloader.clone(), filesystem: args.filesystem.clone(), seal_state: args.seal_state.clone(), @@ -459,7 +460,7 @@ pub(crate) fn run_tmt(sh: &Shell, args: &RunTmtArgs) -> Result<()> { plans.retain(|plan| filter_args.iter().any(|arg| plan.contains(arg.as_str()))); } - if args.composefs_backend { + if args.composefs { plans.retain(|plan| { !plan_metadata .iter() diff --git a/crates/xtask/src/xtask.rs b/crates/xtask/src/xtask.rs index b6ecd5f3c9..654983ea1f 100644 --- a/crates/xtask/src/xtask.rs +++ b/crates/xtask/src/xtask.rs @@ -229,8 +229,9 @@ impl Display for SealState { /// Arguments for run-tmt command. /// /// The composefs-related fields can be set via CLI flags or via the standard -/// `BOOTC_*` environment variables used by the Justfile. When `BOOTC_variant` -/// is set to `composefs`, `--composefs-backend` is implied automatically. +/// `BOOTC_*` environment variables used by the Justfile. `BOOTC_variant=composefs` +/// selects the composefs plans, but doesn't pass `--composefs-backend` to bcvk: +/// those test images select the composefs backend themselves. #[derive(Debug, Args)] pub(crate) struct RunTmtArgs { /// Image name (e.g., "localhost/bootc") @@ -278,10 +279,18 @@ pub(crate) struct RunTmtArgs { #[arg(long)] pub(crate) preserve_vm: bool, - /// Use composefs backend. Also implied when BOOTC_variant=composefs. + /// Install with `--composefs-backend` (and `--bootloader`), for images that + /// don't select the composefs backend themselves. #[arg(long)] pub(crate) composefs_backend: bool, + /// Whether the image is installed with the composefs backend: set by + /// `--composefs-backend` or `BOOTC_variant=composefs`. + #[arg(skip)] + pub(crate) composefs: bool, + + /// Only passed to bcvk with `--composefs-backend`; composefs test images + /// configure it themselves. #[arg(long, env = "BOOTC_bootloader")] pub(crate) bootloader: Option, @@ -308,15 +317,13 @@ pub(crate) struct RunTmtArgs { } impl RunTmtArgs { - /// Derive composefs_backend from BOOTC_variant if not explicitly set. + /// Derive `composefs` from `--composefs-backend` and BOOTC_variant, from + /// the environment or passed to the tests with `--env` (as the Justfile does). pub(crate) fn resolve_composefs(&mut self) { - if !self.composefs_backend { - if let Ok(v) = std::env::var("BOOTC_variant") { - if v == "composefs" { - self.composefs_backend = true; - } - } - } + const COMPOSEFS_VARIANT: &str = "BOOTC_variant=composefs"; + self.composefs = self.composefs_backend + || std::env::var("BOOTC_variant").is_ok_and(|v| v == "composefs") + || self.env.iter().any(|e| e == COMPOSEFS_VARIANT); } } diff --git a/tmt/tests/booted/test-install-composefs-native.nu b/tmt/tests/booted/test-install-composefs-native.nu index 07925492b0..2c6d9b9124 100644 --- a/tmt/tests/booted/test-install-composefs-native.nu +++ b/tmt/tests/booted/test-install-composefs-native.nu @@ -12,7 +12,12 @@ # `bootc install` run from that image without `--composefs-backend`, both # as a self-install and with --source-imgref (as bootc-image-builder does). # An image with both files is still installed with ostree. This runs on the -# ostree variant too, where nothing else selects composefs. +# ostree variant too, where nothing else selects composefs; on the composefs +# variant, every test's install already relies on this default. +# +# TODO: This doesn't depend on the booted host; move it into a dedicated +# install test suite, sharing the install-in-test code with the other install +# tests: https://github.com/cgwalters-forge/tracker/issues/249 use std assert use tap.nu @@ -65,7 +70,9 @@ def main [] { bootc image copy-to-storage build $NATIVE "RUN rm -f /usr/lib/ostree/prepare-root.conf /etc/ostree/prepare-root.conf" - build $BOTH "" + # On the composefs variant, localhost/bootc is itself composefs-native: it + # has no prepare-root.conf, and configures its composefs bootloader. + build $BOTH "RUN printf '[composefs]\\nenabled = yes\\n' > /usr/lib/ostree/prepare-root.conf && rm -f /usr/lib/bootc/install/80-composefs-bootloader.toml" assert equal (install $NATIVE) "composefs" "composefs-native self-install" let src = $"--source-imgref=containers-storage:($NATIVE)" diff --git a/tmt/tests/booted/test-install-outside-container.nu b/tmt/tests/booted/test-install-outside-container.nu index a228727f36..71ad75c8e9 100644 --- a/tmt/tests/booted/test-install-outside-container.nu +++ b/tmt/tests/booted/test-install-outside-container.nu @@ -54,7 +54,7 @@ let base_args = $"bootc install to-disk --disable-selinux --via-loopback --sourc let install_cmd = if (tap is_composefs) { let st = bootc status --json | from json let bootloader = ($st.status.booted.composefs.bootloader | str downcase) - $"($base_args) --composefs-backend --bootloader=($bootloader) --filesystem ext4 ./disk.img" + $"($base_args) --bootloader=($bootloader) --filesystem ext4 ./disk.img" } else { $"($base_args) --filesystem xfs ./disk.img" } diff --git a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh index c64d70c181..3953f6a9ee 100644 --- a/tmt/tests/booted/test-install-to-filesystem-var-mount.sh +++ b/tmt/tests/booted/test-install-to-filesystem-var-mount.sh @@ -171,7 +171,6 @@ COMPOSEFS_BACKEND_PARAMS=() KARGS=("--karg=root=UUID=$ROOT_UUID") if [[ $is_composefs != "null" ]]; then - COMPOSEFS_BACKEND_PARAMS+=("--composefs-backend") COMPOSEFS_BACKEND_PARAMS+=("--bootloader" "${bootloader}") tune2fs -O verity /dev/BL/var02 From bf3b59a2cf4499c5e1a70ec33d54b68aff2fe9e7 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 30 Sep 2026 16:28:11 -0400 Subject: [PATCH 7/8] tests: Skip the ostree install case of plan-52 without bootupd The composefs systemd-boot test images drop bootupd, which an ostree install requires, so installing the image that keeps prepare-root.conf failed there with "bootupd is required for ostree-based installs". Only run that case where bootupd is present; the composefs cases still run. Generated-by: AI Signed-off-by: Colin Walters --- tmt/tests/booted/test-install-composefs-native.nu | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tmt/tests/booted/test-install-composefs-native.nu b/tmt/tests/booted/test-install-composefs-native.nu index 2c6d9b9124..59751137bc 100644 --- a/tmt/tests/booted/test-install-composefs-native.nu +++ b/tmt/tests/booted/test-install-composefs-native.nu @@ -77,7 +77,10 @@ def main [] { assert equal (install $NATIVE) "composefs" "composefs-native self-install" let src = $"--source-imgref=containers-storage:($NATIVE)" assert equal (install $NATIVE $src) "composefs" "composefs-native with --source-imgref" - assert equal (install $BOTH) "ostree" "image with both configurations" + # The ostree backend needs bootupd, which images built for systemd-boot drop + if (which bootupctl | is-not-empty) { + assert equal (install $BOTH) "ostree" "image with both configurations" + } podman rmi $NATIVE $BOTH tap ok From 804f7d21437e6cb67ae5b158aaff9d89c1568c12 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 1 Oct 2026 13:17:59 -0400 Subject: [PATCH 8/8] tests: Enable fs-verity on the multi-device ESP test's filesystems This test formats its own ext4 and runs `bootc install to-existing-root` without a backend flag. Now that the composefs test images default to the composefs backend, bootc infers verity support from the configured fstype (ext4 counts as supported) and creates the repository in strict mode, which fails on a plain mkfs.ext4 with "Filesystem does not support fs-verity". Create the filesystems with -O verity. Generated-by: AI Signed-off-by: Colin Walters --- tmt/tests/booted/test-multi-device-esp.nu | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tmt/tests/booted/test-multi-device-esp.nu b/tmt/tests/booted/test-multi-device-esp.nu index 526a54afc1..f972b6b35d 100644 --- a/tmt/tests/booted/test-multi-device-esp.nu +++ b/tmt/tests/booted/test-multi-device-esp.nu @@ -118,7 +118,7 @@ def setup_disk_with_root [ udevadm settle mkfs.vfat -F 32 $"($loop)p1" - mkfs.ext4 -q $"($loop)p2" + mkfs.ext4 -q -O verity $"($loop)p2" $loop } @@ -206,7 +206,7 @@ def test_single_esp [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint @@ -259,7 +259,7 @@ def test_dual_esp [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint @@ -314,7 +314,7 @@ def test_three_devices_partial_esp [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint @@ -401,7 +401,7 @@ def test_no_esp_failure [] { let lv_path = $"/dev/($vg_name)/test_lv" # Create filesystem and mount - mkfs.ext4 -q $lv_path + mkfs.ext4 -q -O verity $lv_path mkdir $mountpoint mount $lv_path $mountpoint