diff --git a/crates/lib/src/lints.rs b/crates/lib/src/lints.rs
index aa41e9cfd..fb0b2f73e 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/crates/tests-integration/src/container.rs b/crates/tests-integration/src/container.rs
index 08716f475..a5fa4fcf0 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),
diff --git a/docs/src/bootc-images.md b/docs/src/bootc-images.md
index be6c52b7c..65cadd961 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 3e7082f06..08d6f405c 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