From e53658bc02367436ea4b8dc1b13aa72517e93e4d Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 23 Sep 2026 06:49:26 -0400 Subject: [PATCH 1/2] 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-images.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 aa41e9cfd4..fb0b2f73e2 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-images.md b/docs/src/bootc-images.md index be6c52b7c4..65cadd9616 100644 --- a/docs/src/bootc-images.md +++ b/docs/src/bootc-images.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 95920b0b2aebb3e517172d7020081b6b5cf65402 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 25 Sep 2026 16:04:18 -0400 Subject: [PATCH 2/2] tests-integration: Fix race between the install config tests "printconfig --all" writes /run/bootc/install/10-test.toml and deletes it again, while "install config" runs `bootc install print-configuration` in parallel. When the drop-in vanishes between load_config()'s directory scan and its read, the latter fails with "Loading configuration: No such file or directory". This flakes the container integration tests on random PRs and variants. Merge the two into one test that checks both outputs with the drop-in in place. Generated-by: AI --- crates/tests-integration/src/container.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs index 08716f475a..a5fa4fcf03 100644 --- a/crates/tests-integration/src/container.rs +++ b/crates/tests-integration/src/container.rs @@ -114,17 +114,11 @@ pub(crate) fn test_bootc_upgrade() -> Result<()> { Ok(()) } +/// This is a single test because it adds a drop-in to /run/bootc/install, +/// which would race with any other test loading the install configuration: +/// libtest runs tests in parallel, and a file that vanishes between the +/// directory scan and the read fails `print-configuration`. pub(crate) fn test_bootc_install_config() -> Result<()> { - let sh = &xshell::Shell::new()?; - let config = cmd!(sh, "bootc install print-configuration").read()?; - let config: serde_json::Value = - serde_json::from_str(&config).context("Parsing install config")?; - // check that it parses okay, but also ensure kargs is not available here (only via --all) - assert!(config.get("kargs").is_none()); - Ok(()) -} - -pub(crate) fn test_bootc_install_config_all() -> Result<()> { #[derive(Deserialize)] #[serde(rename_all = "kebab-case")] struct TestOstreeConfig { @@ -152,6 +146,12 @@ pub(crate) fn test_bootc_install_config_all() -> Result<()> { } let sh = &xshell::Shell::new()?; + let config = cmd!(sh, "bootc install print-configuration").read()?; + let config: serde_json::Value = + serde_json::from_str(&config).context("Parsing install config")?; + // check that it parses okay, but also ensure kargs is not available here (only via --all) + assert!(config.get("kargs").is_none()); + let config = cmd!(sh, "bootc install print-configuration --all").read()?; let config: TestInstallConfig = serde_json::from_str(&config).context("Parsing install config")?; @@ -531,7 +531,6 @@ pub(crate) fn run(testargs: libtest_mimic::Arguments) -> Result<()> { new_test("variant-base-crosscheck", test_variant_base_crosscheck), new_test("bootc upgrade", test_bootc_upgrade), new_test("install config", test_bootc_install_config), - new_test("printconfig --all", test_bootc_install_config_all), new_test("status", test_bootc_status), new_test("container inspect", test_bootc_container_inspect), new_test("system-reinstall --help", test_system_reinstall_help),