From 38827a70f91af1372cbaef10379ecb1fedc33654 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 14 Jul 2026 12:45:59 +0530 Subject: [PATCH 01/12] install: Use systemd-repart for partitioning If `systemd-repart` binary is present and we find one of the directories associated with systemd-repart configurations, then use systemd-repart for partitioning, else fallback to sfdisk Also, update the default ESP size to 2G for composefs installs. This only applies to sfdisk path. Closes: #2132 Signed-off-by: Pragyan Poudyal --- crates/lib/src/install/baseline.rs | 356 ++++++++++++++++++++--------- 1 file changed, 252 insertions(+), 104 deletions(-) diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 0db968b20d..c4eff4dd3f 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -9,11 +9,13 @@ use std::borrow::Cow; use std::fmt::Display; use std::fmt::Write as _; use std::io::Write; +use std::path::Path; use std::process::Command; use std::process::Stdio; use anyhow::Ok; use anyhow::{Context, Result}; +use bootc_blockdev::Device; use bootc_utils::CommandRunExt; use camino::Utf8Path; use camino::Utf8PathBuf; @@ -65,7 +67,7 @@ pub(crate) const EFIPN_SIZE_MB: u32 = 512; /// EFI Partition size for composefs installations /// We need more space than ostree as we have UKIs and UKI addons /// We might also need to store UKIs for pinned deployments -pub(crate) const CFS_EFIPN_SIZE_MB: u32 = 1024; +pub(crate) const CFS_EFIPN_SIZE_MB: u32 = 2048; #[cfg(feature = "install-to-disk")] pub(crate) const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B"; #[cfg(feature = "install-to-disk")] @@ -190,6 +192,193 @@ pub(crate) fn udev_settle() -> Result<()> { Ok(()) } +/// Partition numbers resulting from partitioning, used to look up devices after. +struct PartitionLayout { + esp_partno: Option, + boot_partno: Option, + rootpn: u32, + used_repart: bool, +} + +/// The json output for systemd-repart +#[derive(Debug, Deserialize)] +struct RepartPartition { + /// Human readable partition name + /// Ex. "esp", "root-x86_64" + #[serde(rename = "type")] + partition_type: String, + /// 0-indexed partition number + partno: u32, +} + +/// Create partitions using systemd-repart (if available) +/// Returns Ok(None) if systemd-repart or repart.d are not present +fn systemd_repart(device: &Device) -> Result> { + if Command::new("systemd-repart") + .arg("--help") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_err() + { + return Ok(None); + } + + let repart_config_dirs = [ + Path::new("/etc/repart.d"), + Path::new("/run/repart.d"), + Path::new("/usr/local/lib/repart.d"), + Path::new("/usr/lib/repart.d"), + ]; + + let has_config = repart_config_dirs.iter().any(|d| { + d.is_dir() + && d.read_dir() + .ok() + .is_some_and(|mut entries| entries.next().is_some()) + }); + + if !has_config { + return Ok(None); + } + + let output = Command::new("systemd-repart") + .arg("--dry-run=no") + .arg("--empty=allow") + .arg("--no-pager") + .arg("--json=pretty") + .arg("--root=/") + .arg(device.path()) + .output() + .context("Failed to run systemd-repart")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("systemd-repart failed: {stderr}"); + } + + let partitions: Vec = serde_json::from_slice(&output.stdout) + .context("Failed to deserialize systemd-repart output")?; + + let mut esp_partno = None; + let mut boot_partno = None; + let mut rootpn = None; + + for part in &partitions { + // repart partno is 0-indexed, lsblk/sfdisk use 1-indexed + let partno = part.partno + 1; + + match part.partition_type.as_str() { + "esp" => esp_partno = Some(partno), + "xbootldr" => boot_partno = Some(partno), + t if t.starts_with("root-") => rootpn = Some(partno), + _ => {} + } + } + + let rootpn = + rootpn.ok_or_else(|| anyhow::anyhow!("systemd-repart output missing root partition"))?; + + Ok(Some(PartitionLayout { + esp_partno, + boot_partno, + rootpn, + used_repart: true, + })) +} + +/// Use sfdisk to create partitions +fn sfdisk( + device: &Device, + root_size: Option, + composefs_backend: bool, + requires_bootpart: bool, +) -> Result { + // Generate partitioning spec as input to sfdisk + let mut partno = 0; + let mut partitioning_buf = String::new(); + writeln!(partitioning_buf, "label: gpt")?; + let random_label = uuid::Uuid::new_v4(); + writeln!(&mut partitioning_buf, "label-id: {random_label}")?; + if cfg!(target_arch = "x86_64") { + partno += 1; + writeln!( + &mut partitioning_buf, + r#"size=1MiB, bootable, type=21686148-6449-6E6F-744E-656564454649, name="BIOS-BOOT""# + )?; + } else if cfg!(target_arch = "powerpc64") { + // PowerPC-PReP-boot + partno += 1; + let label = PREPBOOT_LABEL; + let uuid = PREPBOOT_GUID; + writeln!( + &mut partitioning_buf, + r#"size=4MiB, bootable, type={uuid}, name="{label}""# + )?; + } else if cfg!(any(target_arch = "aarch64", target_arch = "s390x")) { + // No bootloader partition is necessary + } else { + anyhow::bail!("Unsupported architecture: {}", std::env::consts::ARCH); + } + + let esp_partno = if super::ARCH_USES_EFI { + let esp_guid = crate::discoverable_partition_specification::ESP; + partno += 1; + + let esp_size = if composefs_backend { + CFS_EFIPN_SIZE_MB + } else { + EFIPN_SIZE_MB + }; + + writeln!( + &mut partitioning_buf, + r#"size={esp_size}MiB, type={esp_guid}, name="EFI-SYSTEM""# + )?; + Some(partno) + } else { + None + }; + + // Initialize the /boot filesystem. Note that in the future, we may match + // what systemd/uapi-group encourages and make /boot be FAT32 as well, as + // it would aid systemd-boot. + let boot_partno = if requires_bootpart { + partno += 1; + writeln!( + &mut partitioning_buf, + r#"size={BOOTPN_SIZE_MB}MiB, name="boot""# + )?; + Some(partno) + } else { + None + }; + let rootpn = partno + 1; + let root_size = root_size + .map(|v| Cow::Owned(format!("size={v}MiB, "))) + .unwrap_or_else(|| Cow::Borrowed("")); + let rootpart_uuid = + uuid::Uuid::parse_str(crate::discoverable_partition_specification::this_arch_root())?; + writeln!( + &mut partitioning_buf, + r#"{root_size}type={rootpart_uuid}, name="root""# + )?; + tracing::debug!("Partitioning: {partitioning_buf}"); + Task::new("Initializing partitions", "sfdisk") + .arg("--wipe=always") + .arg(device.path()) + .quiet() + .run_with_stdin_buf(Some(partitioning_buf.as_bytes())) + .context("Failed to run sfdisk")?; + + Ok(PartitionLayout { + esp_partno, + boot_partno, + rootpn, + used_repart: false, + }) +} + #[context("Creating rootfs")] #[cfg(feature = "install-to-disk")] pub(crate) fn install_create_rootfs( @@ -279,82 +468,16 @@ pub(crate) fn install_create_rootfs( let bootfs = mntdir.join("boot"); std::fs::create_dir_all(bootfs)?; - // Generate partitioning spec as input to sfdisk - let mut partno = 0; - let mut partitioning_buf = String::new(); - writeln!(partitioning_buf, "label: gpt")?; - let random_label = uuid::Uuid::new_v4(); - writeln!(&mut partitioning_buf, "label-id: {random_label}")?; - if cfg!(target_arch = "x86_64") { - partno += 1; - writeln!( - &mut partitioning_buf, - r#"size=1MiB, bootable, type=21686148-6449-6E6F-744E-656564454649, name="BIOS-BOOT""# - )?; - } else if cfg!(target_arch = "powerpc64") { - // PowerPC-PReP-boot - partno += 1; - let label = PREPBOOT_LABEL; - let uuid = PREPBOOT_GUID; - writeln!( - &mut partitioning_buf, - r#"size=4MiB, bootable, type={uuid}, name="{label}""# - )?; - } else if cfg!(any(target_arch = "aarch64", target_arch = "s390x")) { - // No bootloader partition is necessary - } else { - anyhow::bail!("Unsupported architecture: {}", std::env::consts::ARCH); - } - - let esp_partno = if super::ARCH_USES_EFI { - let esp_guid = crate::discoverable_partition_specification::ESP; - partno += 1; - - let esp_size = if state.composefs_options.composefs_backend { - CFS_EFIPN_SIZE_MB - } else { - EFIPN_SIZE_MB - }; - - writeln!( - &mut partitioning_buf, - r#"size={esp_size}MiB, type={esp_guid}, name="EFI-SYSTEM""# - )?; - Some(partno) - } else { - None + let layout = match systemd_repart(&device)? { + Some(layout) => layout, + None => sfdisk( + &device, + root_size, + state.composefs_options.composefs_backend, + block_setup.requires_bootpart(), + )?, }; - // Initialize the /boot filesystem. Note that in the future, we may match - // what systemd/uapi-group encourages and make /boot be FAT32 as well, as - // it would aid systemd-boot. - let boot_partno = if block_setup.requires_bootpart() { - partno += 1; - writeln!( - &mut partitioning_buf, - r#"size={BOOTPN_SIZE_MB}MiB, name="boot""# - )?; - Some(partno) - } else { - None - }; - let rootpn = partno + 1; - let root_size = root_size - .map(|v| Cow::Owned(format!("size={v}MiB, "))) - .unwrap_or_else(|| Cow::Borrowed("")); - let rootpart_uuid = - uuid::Uuid::parse_str(crate::discoverable_partition_specification::this_arch_root())?; - writeln!( - &mut partitioning_buf, - r#"{root_size}type={rootpart_uuid}, name="root""# - )?; - tracing::debug!("Partitioning: {partitioning_buf}"); - Task::new("Initializing partitions", "sfdisk") - .arg("--wipe=always") - .arg(device.path()) - .quiet() - .run_with_stdin_buf(Some(partitioning_buf.as_bytes())) - .context("Failed to run sfdisk")?; tracing::debug!("Created partition table"); // Full udev sync; it'd obviously be better to await just the devices @@ -364,7 +487,8 @@ pub(crate) fn install_create_rootfs( // Re-read partition table to get updated children device.refresh()?; - let root_device = device.find_device_by_partno(rootpn)?; + let root_device = device.find_device_by_partno(layout.rootpn)?; + // Verify the partition type matches the DPS root partition type for this architecture let expected_parttype = crate::discoverable_partition_specification::this_arch_root(); if !root_device @@ -373,7 +497,8 @@ pub(crate) fn install_create_rootfs( .is_some_and(|pt| pt.eq_ignore_ascii_case(expected_parttype)) { anyhow::bail!( - "root partition {rootpn} has type {}; expected {expected_parttype}", + "root partition {} has type {}; expected {expected_parttype}", + layout.rootpn, root_device.parttype.as_deref().unwrap_or("") ); } @@ -417,34 +542,55 @@ pub(crate) fn install_create_rootfs( }; // Initialize the /boot filesystem - let bootdev = if let Some(bootpn) = boot_partno { + let bootdev = if let Some(bootpn) = layout.boot_partno { Some(device.find_device_by_partno(bootpn)?) } else { None }; - let boot_uuid = if let Some(bootdev) = bootdev { - Some( - mkfs(&bootdev.path(), root_filesystem, "boot", opts.wipe, []) - .context("Initializing /boot")?, - ) - } else { - None - }; - // Unconditionally enable fsverity for ext4 - let mkfs_options = match root_filesystem { - Filesystem::Ext4 => ["-O", "verity"].as_slice(), - _ => [].as_slice(), + let boot_uuid = match bootdev { + Some(bootdev) => { + let u = if layout.used_repart { + let u = bootdev + .uuid + .as_ref() + .ok_or_else(|| anyhow::anyhow!("bootdev UUID not found"))?; + + u.parse::() + .with_context(|| format!("Parsing bootdev UUID {u}"))? + } else { + mkfs(&bootdev.path(), root_filesystem, "boot", opts.wipe, []) + .context("Initializing /boot")? + }; + + Some(u) + } + None => None, }; - // Initialize rootfs - let root_uuid = mkfs( - &rootdev_path, - root_filesystem, - "root", - opts.wipe, - mkfs_options.iter().copied(), - )?; + let root_uuid = if layout.used_repart { + // systemd-repart creates filesystem, just read the UUID it assigned + let u = root_device.uuid.as_ref().ok_or_else(|| { + anyhow::anyhow!("Root device created by repart has no filesystem UUID") + })?; + + u.parse::() + .with_context(|| format!("Parsing root fs UUID {u}"))? + } else { + // Unconditionally enable fsverity for ext4 + let mkfs_options = match root_filesystem { + Filesystem::Ext4 => ["-O", "verity"].as_slice(), + _ => [].as_slice(), + }; + + mkfs( + &rootdev_path, + root_filesystem, + "root", + opts.wipe, + mkfs_options.iter().copied(), + )? + }; let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}")); let bootarg = bootsrc.as_deref().map(|bootsrc| format!("boot={bootsrc}")); let boot = bootsrc.map(|bootsrc| MountSpec { @@ -499,13 +645,15 @@ pub(crate) fn install_create_rootfs( crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?; // Create the EFI system partition, if applicable - if let Some(esp_partno) = esp_partno { + if let Some(esp_partno) = layout.esp_partno { let espdev = device.find_device_by_partno(esp_partno)?; - Task::new("Creating ESP filesystem", "mkfs.fat") - .args([&espdev.path(), "-n", "EFI-SYSTEM"]) - .verbose() - .quiet_output() - .run()?; + if !layout.used_repart { + Task::new("Creating ESP filesystem", "mkfs.fat") + .args([&espdev.path(), "-n", "EFI-SYSTEM"]) + .verbose() + .quiet_output() + .run()?; + } let efifs_path = bootfs.join(crate::bootloader::EFI_DIR); std::fs::create_dir(&efifs_path).context("Creating efi dir")?; } From 18b0b54a734bf93385511012a8d419818fd9778a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Wed, 29 Jul 2026 16:00:57 +0530 Subject: [PATCH 02/12] install: Handle missing root partition in systemd-repart config When repart.d configuration defines ESP/xbootldr but omits a root partition definition, previously we failed with a hard error. Now we perform a dry-run first to detect whether a root partition is defined. If not, it creates a temporary definitions directory containing: - ESP/xbootldr configs from the existing repart.d - A generated root partition config `Type=root` that consumes all remaining disk space or a fixed size via `--root-size` Signed-off-by: Pragyan Poudyal --- crates/lib/src/install/baseline.rs | 136 +++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index c4eff4dd3f..3226b436ed 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -193,10 +193,12 @@ pub(crate) fn udev_settle() -> Result<()> { } /// Partition numbers resulting from partitioning, used to look up devices after. +#[derive(Debug)] struct PartitionLayout { esp_partno: Option, boot_partno: Option, rootpn: u32, + /// Whether systemd-repart created the ESP/boot partitions (skip mkfs for those) used_repart: bool, } @@ -209,11 +211,18 @@ struct RepartPartition { partition_type: String, /// 0-indexed partition number partno: u32, + /// Path to the repart.d definition file that created this partition + #[serde(default)] + file: Option, } /// Create partitions using systemd-repart (if available) /// Returns Ok(None) if systemd-repart or repart.d are not present -fn systemd_repart(device: &Device) -> Result> { +fn systemd_repart( + device: &Device, + root_size: Option, + rootfs: Option, +) -> Result> { if Command::new("systemd-repart") .arg("--help") .stdout(Stdio::null()) @@ -242,29 +251,115 @@ fn systemd_repart(device: &Device) -> Result> { return Ok(None); } - let output = Command::new("systemd-repart") - .arg("--dry-run=no") - .arg("--empty=allow") - .arg("--no-pager") - .arg("--json=pretty") - .arg("--root=/") - .arg(device.path()) - .output() - .context("Failed to run systemd-repart")?; + // Dry-run to check what partitions would be created + let dry_partitions = systemd_repart_run(device, None, true)?; + + if dry_partitions.is_empty() { + return Ok(None); + } + + let has_root = dry_partitions + .iter() + .any(|p| p.partition_type.starts_with("root-")); + + if has_root { + // Root partition is defined in repart.d config, run for real + let partitions = systemd_repart_run(device, None, false)?; + let layout = parse_repart_layout(&partitions)?; + return Ok(Some(layout)); + } + + // Root partition is not defined, create defintion for the root part + let tmp_dir = tempfile::tempdir().context("Creating temp dir for repart definitions")?; + + for part in &dry_partitions { + let Some(ref file) = part.file else { + continue; + }; + if matches!(part.partition_type.as_str(), "esp" | "xbootldr") { + let src = Path::new(file); + if let Some(name) = src.file_name() { + std::fs::copy(src, tmp_dir.path().join(name)) + .with_context(|| format!("Copying repart config {file}"))?; + } + } + } + + let mut root_conf = String::from("[Partition]\nType=root\n"); + if let Some(size_mib) = root_size { + writeln!(root_conf, "SizeMinBytes={size_mib}M")?; + writeln!(root_conf, "SizeMaxBytes={size_mib}M")?; + } + match rootfs { + Some(fs) => writeln!(root_conf, "Format={fs}")?, + // Default to xfs, same as sfdisk + None => writeln!(root_conf, "Format=xfs")?, + } + + std::fs::write(tmp_dir.path().join("50-root.conf"), &root_conf) + .context("Writing root repart config")?; + + let partitions = systemd_repart_run(device, Some(tmp_dir.path()), false)?; + let layout = parse_repart_layout(&partitions)?; + Ok(Some(layout)) +} + +/// Run systemd-repart on the device and return the parsed JSON output. +/// `dry_run`: if true, no changes are written to disk. +/// `definitions`: if set, uses `--definitions=` and `--empty=allow`; +/// otherwise uses the default config search paths with `--empty=force`. +fn systemd_repart_run( + device: &Device, + definitions: Option<&Path>, + dry_run: bool, +) -> Result> { + let mut cmd = Command::new("systemd-repart"); + + // Enable fsverity for ext4 + // btrfs has fsverity enabled out of the box + cmd.env("SYSTEMD_REPART_MKFS_OPTIONS_EXT4", "-O verity"); + + let dry_run_arg = if dry_run { + "--dry-run=yes" + } else { + "--dry-run=no" + }; + cmd.args([ + dry_run_arg, + "--no-pager", + "--json=pretty", + "--empty=force", + "--root=/", + ]); + + if let Some(defs) = definitions { + cmd.arg(format!("--definitions={}", defs.display())); + } + + cmd.arg(device.path()); + + let output = cmd.output().context("Failed to run systemd-repart")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); anyhow::bail!("systemd-repart failed: {stderr}"); } - let partitions: Vec = serde_json::from_slice(&output.stdout) - .context("Failed to deserialize systemd-repart output")?; + serde_json::from_slice(&output.stdout).with_context(|| { + format!( + "Failed to deserialize systemd-repart output: {}", + String::from_utf8_lossy(&output.stdout) + ) + }) +} +/// Parse partition layout from systemd-repart JSON output. +fn parse_repart_layout(partitions: &[RepartPartition]) -> Result { let mut esp_partno = None; let mut boot_partno = None; let mut rootpn = None; - for part in &partitions { + for part in partitions { // repart partno is 0-indexed, lsblk/sfdisk use 1-indexed let partno = part.partno + 1; @@ -279,12 +374,12 @@ fn systemd_repart(device: &Device) -> Result> { let rootpn = rootpn.ok_or_else(|| anyhow::anyhow!("systemd-repart output missing root partition"))?; - Ok(Some(PartitionLayout { + Ok(PartitionLayout { esp_partno, boot_partno, rootpn, used_repart: true, - })) + }) } /// Use sfdisk to create partitions @@ -468,7 +563,7 @@ pub(crate) fn install_create_rootfs( let bootfs = mntdir.join("boot"); std::fs::create_dir_all(bootfs)?; - let layout = match systemd_repart(&device)? { + let layout = match systemd_repart(&device, root_size, opts.filesystem)? { Some(layout) => layout, None => sfdisk( &device, @@ -478,7 +573,14 @@ pub(crate) fn install_create_rootfs( )?, }; - tracing::debug!("Created partition table"); + tracing::debug!( + "Created partition table using {}", + if layout.used_repart { + "systemd-repart" + } else { + "sfdisk" + } + ); // Full udev sync; it'd obviously be better to await just the devices // we're targeting, but this is a simple coarse hammer. From a0b54db229e87596d3e2b7c9b7302fa2f3d225b5 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 3 Aug 2026 09:34:29 +0530 Subject: [PATCH 03/12] install: Handle root filesystem If systemd-repart definition for root partition is present in the image, we don't need the `--filesystem` CLI option to be present. So, we ignore it until we have enough information from the repart definitions. This has the ufortunate effect of us not being able to outright detect if the filesystem will support fs-verity or not during composefs installs. Now if we have a rootfs that doesn't support fs-verity, but the composefs repository does not have fs-verity as optional, we will throw an error during installation instead of throwing an error while preparing for an install. Signed-off-by: Pragyan Poudyal --- crates/lib/src/install.rs | 77 ++++++++++++++----------- crates/lib/src/install/baseline.rs | 90 +++++++++++++++++++----------- 2 files changed, 101 insertions(+), 66 deletions(-) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 934a10ee54..0441a5aa95 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1770,13 +1770,12 @@ async fn prepare_install( println!("Digest: {digest}"); } - let root_filesystem = target_fs - .or(install_config - .as_ref() - .and_then(|c| c.filesystem_root()) - .and_then(|r| r.fstype)) - .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?; - let composefs_fsverity_supported = root_filesystem.supports_fsverity(); + // Don't error out if a filesystem is not passed in via cli as we could have + // repart.d definitions available + let root_filesystem = target_fs.or(install_config + .as_ref() + .and_then(|c| c.filesystem_root()) + .and_then(|r| r.fstype)); let mut is_uki = false; @@ -1787,39 +1786,46 @@ async fn prepare_install( // we hard require it in that particular case // // NOTE: This isn't really 100% accurate 100% of the time as the cmdline can be in an addon - match kernel { - Some(k) => match k.k_type { - crate::kernel::KernelType::Uki { cmdline, .. } => { - let allow_missing_fsverity = if let Some(cmdline) = cmdline { - ComposefsCmdline::find_in_cmdline(&cmdline)? - .is_some_and(|cfs_cmdline| cfs_cmdline.allow_missing_fsverity) - } else { - false - }; - - if !allow_missing_fsverity { - anyhow::ensure!( - root_filesystem.supports_fsverity(), - "Specified filesystem {root_filesystem} does not support fs-verity" - ); - } + if let Some(root_filesystem) = root_filesystem { + match kernel { + Some(k) => match k.k_type { + crate::kernel::KernelType::Uki { cmdline, .. } => { + let allow_missing_fsverity = if let Some(cmdline) = cmdline { + ComposefsCmdline::find_in_cmdline(&cmdline)? + .is_some_and(|cfs_cmdline| cfs_cmdline.allow_missing_fsverity) + } else { + false + }; + // >>>>>>> 626befdf (install: Handle root filesystem) + + if !allow_missing_fsverity { + anyhow::ensure!( + root_filesystem.supports_fsverity(), + "Specified filesystem {root_filesystem} does not support fs-verity" + ); + } - composefs_options.allow_missing_verity = allow_missing_fsverity; - is_uki = true; - } + composefs_options.allow_missing_verity = allow_missing_fsverity; + is_uki = true; + } - crate::kernel::KernelType::Vmlinuz { .. } => {} - }, + crate::kernel::KernelType::Vmlinuz { .. } => {} + }, - None => {} - } + None => {} + } - // If `--allow-missing-verity` is already passed via CLI, don't modify - if composefs_options.composefs_backend && !composefs_options.allow_missing_verity && !is_uki { - composefs_options.allow_missing_verity = !root_filesystem.supports_fsverity(); + // If `--allow-missing-verity` is already passed via CLI, don't modify + if composefs_options.composefs_backend && !composefs_options.allow_missing_verity && !is_uki + { + composefs_options.allow_missing_verity = !root_filesystem.supports_fsverity(); + } } tracing::info!( + root_filesystem = root_filesystem + .map(|f| f.to_string()) + .unwrap_or("None".into()), allow_missing_fsverity = composefs_options.allow_missing_verity, uki = is_uki, "ComposeFS install prep", @@ -1870,7 +1876,10 @@ async fn prepare_install( host_is_container, composefs_required, composefs_options, - composefs_fsverity_supported, + // assume fs-verity is supported as that's the safer option + composefs_fsverity_supported: root_filesystem + .map(|fs| fs.supports_fsverity()) + .unwrap_or(true), allow_missing_verity_explicit, }); diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 3226b436ed..4e2eaef748 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -214,15 +214,11 @@ struct RepartPartition { /// Path to the repart.d definition file that created this partition #[serde(default)] file: Option, + #[allow(dead_code)] + fs: Option, } -/// Create partitions using systemd-repart (if available) -/// Returns Ok(None) if systemd-repart or repart.d are not present -fn systemd_repart( - device: &Device, - root_size: Option, - rootfs: Option, -) -> Result> { +fn can_use_systemd_repart() -> bool { if Command::new("systemd-repart") .arg("--help") .stdout(Stdio::null()) @@ -230,7 +226,7 @@ fn systemd_repart( .status() .is_err() { - return Ok(None); + return false; } let repart_config_dirs = [ @@ -247,15 +243,22 @@ fn systemd_repart( .is_some_and(|mut entries| entries.next().is_some()) }); - if !has_config { - return Ok(None); - } + return has_config; +} +/// Create partitions using systemd-repart +/// Assumes we have systemd-repart definitions +#[context("Running systemd-repart")] +fn systemd_repart( + device: &Device, + root_size: Option, + rootfs: Option, +) -> Result { // Dry-run to check what partitions would be created let dry_partitions = systemd_repart_run(device, None, true)?; if dry_partitions.is_empty() { - return Ok(None); + anyhow::bail!("systemd-repart returned empty partitions"); } let has_root = dry_partitions @@ -266,7 +269,7 @@ fn systemd_repart( // Root partition is defined in repart.d config, run for real let partitions = systemd_repart_run(device, None, false)?; let layout = parse_repart_layout(&partitions)?; - return Ok(Some(layout)); + return Ok(layout); } // Root partition is not defined, create defintion for the root part @@ -292,8 +295,9 @@ fn systemd_repart( } match rootfs { Some(fs) => writeln!(root_conf, "Format={fs}")?, - // Default to xfs, same as sfdisk - None => writeln!(root_conf, "Format=xfs")?, + None => { + anyhow::bail!("Rootfs not specified") + } } std::fs::write(tmp_dir.path().join("50-root.conf"), &root_conf) @@ -301,7 +305,7 @@ fn systemd_repart( let partitions = systemd_repart_run(device, Some(tmp_dir.path()), false)?; let layout = parse_repart_layout(&partitions)?; - Ok(Some(layout)) + Ok(layout) } /// Run systemd-repart on the device and return the parsed JSON output. @@ -383,6 +387,7 @@ fn parse_repart_layout(partitions: &[RepartPartition]) -> Result, @@ -482,13 +487,6 @@ pub(crate) fn install_create_rootfs( ) -> Result { let install_config = state.install_config.as_ref(); let luks_name = "root"; - // Ensure we have a root filesystem upfront - let root_filesystem = opts - .filesystem - .or(install_config - .and_then(|c| c.filesystem_root()) - .and_then(|r| r.fstype)) - .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?; // Verify that the target is empty (if not already wiped in particular, but it's // also good to verify that the wipe worked) let mut device = bootc_blockdev::list_dev(&opts.device)?; @@ -521,10 +519,12 @@ pub(crate) fn install_create_rootfs( std::fs::remove_dir_all(&mntdir)?; } + let use_systemd_repart = can_use_systemd_repart(); + // Use the install configuration to find the block setup, if we have one let block_setup = if let Some(config) = install_config { config.get_block_setup(opts.block_setup.as_ref().copied())? - } else if opts.filesystem.is_some() { + } else if opts.filesystem.is_some() || use_systemd_repart { // Otherwise, if a filesystem is specified then we default to whatever was // specified via --block-setup, or the default opts.block_setup.unwrap_or_default() @@ -563,14 +563,15 @@ pub(crate) fn install_create_rootfs( let bootfs = mntdir.join("boot"); std::fs::create_dir_all(bootfs)?; - let layout = match systemd_repart(&device, root_size, opts.filesystem)? { - Some(layout) => layout, - None => sfdisk( + let layout = if use_systemd_repart { + systemd_repart(&device, root_size, opts.filesystem)? + } else { + sfdisk( &device, root_size, state.composefs_options.composefs_backend, block_setup.requires_bootpart(), - )?, + )? }; tracing::debug!( @@ -589,6 +590,23 @@ pub(crate) fn install_create_rootfs( // Re-read partition table to get updated children device.refresh()?; + // Ensure we have a root filesystem + let root_filesystem = if layout.used_repart { + let root = device.find_device_by_partno(layout.rootpn)?; + root.fstype + .as_ref() + .ok_or_else(|| anyhow::anyhow!("repart: Root filesystem type not defined"))? + } else { + let root_filesystem = opts + .filesystem + .or(install_config + .and_then(|c| c.filesystem_root()) + .and_then(|r| r.fstype)) + .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?; + + &root_filesystem.to_string() + }; + let root_device = device.find_device_by_partno(layout.rootpn)?; // Verify the partition type matches the DPS root partition type for this architecture @@ -661,8 +679,14 @@ pub(crate) fn install_create_rootfs( u.parse::() .with_context(|| format!("Parsing bootdev UUID {u}"))? } else { - mkfs(&bootdev.path(), root_filesystem, "boot", opts.wipe, []) - .context("Initializing /boot")? + mkfs( + &bootdev.path(), + root_filesystem.as_str().try_into()?, + "boot", + opts.wipe, + [], + ) + .context("Initializing /boot")? }; Some(u) @@ -679,15 +703,17 @@ pub(crate) fn install_create_rootfs( u.parse::() .with_context(|| format!("Parsing root fs UUID {u}"))? } else { + let rootfs: Filesystem = root_filesystem.as_str().try_into()?; + // Unconditionally enable fsverity for ext4 - let mkfs_options = match root_filesystem { + let mkfs_options = match rootfs { Filesystem::Ext4 => ["-O", "verity"].as_slice(), _ => [].as_slice(), }; mkfs( &rootdev_path, - root_filesystem, + rootfs, "root", opts.wipe, mkfs_options.iter().copied(), From e12937270c079c0e09a4f0864b2ed5fb01336aa0 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 4 Aug 2026 12:39:45 +0530 Subject: [PATCH 04/12] repart: Run all definitions Do not filter the definitions that are supposed to run after boot as we mount `/sysroot` ro which causes systemd's growfs service to throw an error as it tries to repartition Signed-off-by: Pragyan Poudyal --- crates/lib/src/install/baseline.rs | 37 +++++------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 4e2eaef748..010047f725 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -211,9 +211,6 @@ struct RepartPartition { partition_type: String, /// 0-indexed partition number partno: u32, - /// Path to the repart.d definition file that created this partition - #[serde(default)] - file: Option, #[allow(dead_code)] fs: Option, } @@ -255,7 +252,7 @@ fn systemd_repart( rootfs: Option, ) -> Result { // Dry-run to check what partitions would be created - let dry_partitions = systemd_repart_run(device, None, true)?; + let dry_partitions = systemd_repart_run(device, true)?; if dry_partitions.is_empty() { anyhow::bail!("systemd-repart returned empty partitions"); @@ -267,27 +264,12 @@ fn systemd_repart( if has_root { // Root partition is defined in repart.d config, run for real - let partitions = systemd_repart_run(device, None, false)?; + let partitions = systemd_repart_run(device, false)?; let layout = parse_repart_layout(&partitions)?; return Ok(layout); } // Root partition is not defined, create defintion for the root part - let tmp_dir = tempfile::tempdir().context("Creating temp dir for repart definitions")?; - - for part in &dry_partitions { - let Some(ref file) = part.file else { - continue; - }; - if matches!(part.partition_type.as_str(), "esp" | "xbootldr") { - let src = Path::new(file); - if let Some(name) = src.file_name() { - std::fs::copy(src, tmp_dir.path().join(name)) - .with_context(|| format!("Copying repart config {file}"))?; - } - } - } - let mut root_conf = String::from("[Partition]\nType=root\n"); if let Some(size_mib) = root_size { writeln!(root_conf, "SizeMinBytes={size_mib}M")?; @@ -300,10 +282,11 @@ fn systemd_repart( } } - std::fs::write(tmp_dir.path().join("50-root.conf"), &root_conf) + std::fs::create_dir_all("/run/repart.d").context("Creating /run/repart.d")?; + std::fs::write("/run/repart.d/50-root.conf", &root_conf) .context("Writing root repart config")?; - let partitions = systemd_repart_run(device, Some(tmp_dir.path()), false)?; + let partitions = systemd_repart_run(device, false)?; let layout = parse_repart_layout(&partitions)?; Ok(layout) } @@ -312,11 +295,7 @@ fn systemd_repart( /// `dry_run`: if true, no changes are written to disk. /// `definitions`: if set, uses `--definitions=` and `--empty=allow`; /// otherwise uses the default config search paths with `--empty=force`. -fn systemd_repart_run( - device: &Device, - definitions: Option<&Path>, - dry_run: bool, -) -> Result> { +fn systemd_repart_run(device: &Device, dry_run: bool) -> Result> { let mut cmd = Command::new("systemd-repart"); // Enable fsverity for ext4 @@ -336,10 +315,6 @@ fn systemd_repart_run( "--root=/", ]); - if let Some(defs) = definitions { - cmd.arg(format!("--definitions={}", defs.display())); - } - cmd.arg(device.path()); let output = cmd.output().context("Failed to run systemd-repart")?; From 05e21f5ec66317c4798e0a3ec27f2badbb818911 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 3 Aug 2026 09:34:02 +0530 Subject: [PATCH 05/12] tmt: Add test for systemd-repart Signed-off-by: Pragyan Poudyal --- tmt/plans/integration.fmf | 7 + tmt/tests/booted/test-install-repart.nu | 177 ++++++++++++++++++++++++ tmt/tests/tests.fmf | 5 + 3 files changed, 189 insertions(+) create mode 100644 tmt/tests/booted/test-install-repart.nu diff --git a/tmt/plans/integration.fmf b/tmt/plans/integration.fmf index d9868c4f82..f0a9fee02a 100644 --- a/tmt/plans/integration.fmf +++ b/tmt/plans/integration.fmf @@ -339,4 +339,11 @@ execute: test: - /tmt/tests/tests/test-50-switch-zstd-chunked extra-skip_if_ostree: true + +/plan-52-install-repart: + summary: Test bootc install to-disk with systemd-repart partitioning + discover: + how: fmf + test: + - /tmt/tests/tests/test-52-install-repart # END GENERATED PLANS diff --git a/tmt/tests/booted/test-install-repart.nu b/tmt/tests/booted/test-install-repart.nu new file mode 100644 index 0000000000..41d1510508 --- /dev/null +++ b/tmt/tests/booted/test-install-repart.nu @@ -0,0 +1,177 @@ +# number: 52 +# tmt: +# summary: Test bootc install to-disk with systemd-repart partitioning +# duration: 30m + +use std assert +use tap.nu + +let st = bootc status --json | from json + +let bootloader = if ($st.status.booted.composefs? != null) { + $st.status.booted.composefs.bootloader | str downcase +} else { + "grub" +} + +# We need this for grub installation +let bios = if $bootloader == "grub" { + " +RUN < /usr/lib/repart.d/00-bios.conf +[Partition] +Type=21686148-6449-6e6f-744e-656564454649 +Label=BIOS-BOOT +SizeMinBytes=1M +SizeMaxBytes=1M +EOF + " + } else { + "" + } + +def run_install_to_disk [ + target_image: string + extra_bootc_args: list +] { + let composefs_args = if (tap is_composefs) { + ["--composefs-backend", "--bootloader", $bootloader] + } else { + "" + } + + let volume = $"-v /dev:/dev -v /run/udev:/run/udev -v /var/disk.img:/disk.img" + let base = $"podman run --rm --privileged ($volume) --pid=host --security-opt label=type:unconfined_t --env BOOTC_BOOTLOADER_DEBUG=1 ($target_image)" + let args = $"($composefs_args | str join ' ') ($extra_bootc_args | str join ' ')" + let bootc = $"bootc install to-disk ($args) --disable-selinux --via-loopback --source-imgref containers-storage:($target_image) /disk.img" + + tap run_install $"($base) ($bootc)" +} + +def test_repart_full [] { + tap begin "install with systemd-repart (ESP + root defined)" + + bootc image copy-to-storage + + let dockerfile = $"FROM localhost/bootc as base +RUN rm -rf /etc/repart.d /usr/lib/repart.d /usr/local/lib/repart.d /run/repart.d +RUN rm -rf /usr/lib/bootc/bound-images.d/* + +RUN mkdir -p /usr/lib/repart.d +($bios) +RUN <<'EOF' cat > /usr/lib/repart.d/00-esp.conf +[Partition] +Type=esp +Format=vfat +SizeMinBytes=1024M +SizeMaxBytes=1024M +EOF +RUN <<'EOF' cat > /usr/lib/repart.d/10-root.conf +[Partition] +Type=root +Format=ext4 +EOF +" + (tap make_uki_containerfile $dockerfile) | podman build -t localhost/bootc-repart . -f - + + truncate -s 10G /var/disk.img + setenforce 0 + + run_install_to_disk localhost/bootc-repart [] + + # Verify partition layout + let loop = (losetup -f --show /var/disk.img | str trim) + try { + partx -u $loop + udevadm settle + let parts = (lsblk -J -o name,parttype,partuuid $loop | from json) + let children = ($parts.blockdevices.0.children) + let part_types = ($children | get parttype) + + # ESP GUID + let esp_guid = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" + assert ($part_types | any {|t| ($t | str downcase) == $esp_guid }) "ESP partition not found" + + # Root partition (architecture-specific, just check it exists beyond ESP) + assert (($children | length) >= 2) "Expected at least 2 partitions (ESP + root)" + + print "PASS: repart created ESP + root partitions" + } catch { |e| + losetup -d $loop + error make { msg: $"Verification failed: ($e.msg)" } + } + + losetup -d $loop + rm -rvf /var/disk.img +} + +def test_repart_no_root [] { + tap begin "install with systemd-repart (ESP only, root generated by bootc)" + + # Image has ESP + home in repart.d, but no root partition definition + let dockerfile = $"FROM localhost/bootc as base +RUN rm -rf /etc/repart.d /usr/lib/repart.d /usr/local/lib/repart.d /run/repart.d +RUN rm -rf /usr/lib/bootc/bound-images.d/* + +RUN mkdir -p /usr/lib/repart.d +($bios) +RUN <<'EOF' cat > /usr/lib/repart.d/00-esp.conf +[Partition] +Type=esp +Format=vfat +SizeMinBytes=512M +SizeMaxBytes=512M +EOF + +RUN <<'EOF' cat > /usr/lib/repart.d/20-home.conf +[Partition] +Type=home +Format=ext4 +SizeMinBytes=512M +SizeMaxBytes=512M +EOF +" + (tap make_uki_containerfile $dockerfile) | podman build -t localhost/bootc-repart-noroot . -f - + + truncate -s 10G /var/disk.img + setenforce 0 + + run_install_to_disk localhost/bootc-repart-noroot ["--filesystem" "ext4" "--root-size" "7G"] + + # Verify partition layout + let loop = (losetup -f --show /var/disk.img | str trim) + try { + partx -u $loop + udevadm settle + let parts = (lsblk -J --bytes -o name,parttype,partuuid,size $loop | from json) + print $parts + let children = ($parts.blockdevices.0.children) + let part_types = ($children | get parttype) + + # ESP GUID + let esp_guid = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" + assert ($part_types | any {|t| ($t | str downcase) == $esp_guid }) "ESP partition not found" + + # Should have ESP + root + home (and Bios for grub) + assert (($children | length) >= 3) "Expected at least 3 partitions" + + # Verify root is not using all disk space (--root-size 5G was specified) + let root_part = ($children | last) + let root_size_bytes = ($root_part.size | into int) + let seven_gb = (7 * 1024 * 1024 * 1024) + assert ($root_size_bytes <= $seven_gb) $"Root partition should be ~7G, got ($root_part.size)" + + print "PASS: repart created ESP, bootc generated root with correct size" + } catch { |e| + losetup -d $loop + error make { msg: $"Verification failed: ($e.msg)" } + } + + losetup -d $loop + rm -rvf /var/disk.img +} + +def main [] { + test_repart_full + test_repart_no_root + tap ok +} diff --git a/tmt/tests/tests.fmf b/tmt/tests/tests.fmf index f9f73990d3..2139a2c576 100644 --- a/tmt/tests/tests.fmf +++ b/tmt/tests/tests.fmf @@ -211,3 +211,8 @@ check: summary: Switch to an image with zstd:chunked compressed layers duration: 30m test: nu booted/test-switch-zstd-chunked.nu + +/test-52-install-repart: + summary: Test bootc install to-disk with systemd-repart partitioning + duration: 30m + test: nu booted/test-install-repart.nu From d638d41804021c84e46ccd0e2407e18c7e59f158 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 4 Aug 2026 13:26:56 +0530 Subject: [PATCH 06/12] docs: Add docs and examples for systemd-repart Generated by ClaudeCode Signed-off-by: Pragyan Poudyal --- docs/src/bootc-installation.7.md | 108 +++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/docs/src/bootc-installation.7.md b/docs/src/bootc-installation.7.md index e5a051542c..90154c5332 100644 --- a/docs/src/bootc-installation.7.md +++ b/docs/src/bootc-installation.7.md @@ -434,6 +434,114 @@ If you're building tooling that uses `bootc install to-filesystem`, you should: over `/etc/fstab` for root mount options, as this works better with composefs and DPS auto-discovery. +## systemd-repart + +When `systemd-repart` is available and repart.d definitions are present in the +container image, `bootc install to-disk` uses systemd-repart instead of sfdisk +to partition the target disk. + +### How definitions are discovered + +Definitions are searched in the standard systemd-repart locations relative +to the container root: + +- `/etc/repart.d/*.conf` +- `/run/repart.d/*.conf` +- `/usr/local/lib/repart.d/*.conf` +- `/usr/lib/repart.d/*.conf` + +If `systemd-repart` is not installed or no `.conf` files exist in any of these +directories, bootc falls back to its built-in sfdisk partitioning. + +### Root partition handling + +All repart.d definitions are passed to systemd-repart together so that it can +plan a correct layout with proper space allocation across all partitions. + +If the image's repart.d definitions do **not** include a root partition +(`Type=root`), bootc automatically injects one into `/run/repart.d/` before +invoking systemd-repart. The generated root partition definition: + +- Uses `Type=root` (architecture-specific DPS GUID is resolved by systemd-repart) +- Applies `Format=` from the configured root filesystem type +- Honours `--root-size` if specified (via `SizeMinBytes`/`SizeMaxBytes`) +- Otherwise takes all remaining space on the disk + +This means images can ship repart.d definitions for additional partitions +(e.g. `/home`, swap, `/var`) without needing to also define root. +All definitions run at install time so that systemd-repart can plan the +layout with correct space allocation across all partitions. This is +important because the root filesystem is mounted read-only on bootc +systems (`/sysroot` is ro), so systemd-repart cannot resize it after +installation. + +**Important:** A root filesystem type is always required. It can come from +any of these sources (checked in order): + +1. `--filesystem` CLI argument +2. `install.filesystem.root.type` in the install configuration +3. `Format=` in the repart.d root partition definition + +If none of these provide a filesystem type, the installation will fail. + +### Firstboot definitions + +Images may include repart.d definitions for partitions beyond root, ESP, +and xbootldr, for example `/home`, swap, or `/var`. Because all definitions +run at install time, systemd-repart allocates space for all of them during +installation. + +### DPS auto-mount and symlinked directories + +`systemd-gpt-auto-generator` maps DPS partition types to fixed mount points +(e.g. `Type=home` mounts at `/home`). If the mount point is a symlink +as is common in ostree-based systems where `/home -> /var/home`, the +auto-generated mount unit will fail. + +For this to work, the container image must ensure that `/home` is a real +directory, not a symlink. Alternatively, use `Type=linux-generic` with a +partition label and mount it explicitly via a `systemd.mount-extra` kernel +argument: + +``` +systemd.mount-extra=PARTLABEL=home:/var/home:ext4 +``` + +### Filesystem creation + +When systemd-repart creates partitions, it also creates filesystems according +to the `Format=` directive in each definition. In this case bootc reads +the filesystem UUIDs assigned by systemd-repart rather than running `mkfs` +itself. + +### LUKS (tpm2-luks) + +systemd-repart integration is not supported with `--block-setup tpm2-luks`. +When LUKS is configured, bootc always falls back to sfdisk partitioning. +This is because LUKS needs to format the root partition with `cryptsetup +luksFormat` after partitioning, which conflicts with systemd-repart having +already created a filesystem on that partition. + +### Examples + +For the full definition file format, see +[repart.d(5)](https://www.freedesktop.org/software/systemd/man/latest/repart.d.html). + +#### Minimal: let bootc handle root + +An image that only wants a custom ESP size: + +```ini +# /usr/lib/repart.d/00-esp.conf +[Partition] +Type=esp +Format=vfat +SizeMinBytes=2G +SizeMaxBytes=2G +``` + +bootc will inject a root partition definition automatically. + ## Finding and configuring the physical root filesystem On a bootc system, the "physical root" is different from From 41e48cae22d09783120e77ea6bd11386186e9b31 Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Fri, 21 Aug 2026 10:11:44 +0530 Subject: [PATCH 07/12] ostree/usr-overlay: Check if an overlay already exists Similar to what we have in the composefs path, check if there already exists an overlay at `/usr`. This fixes the error in `test-install-repart.nu` where a second install was failing for ostree due to an overlay already existing on `/usr` Signed-off-by: Pragyan Poudyal --- crates/lib/src/bootc_composefs/state.rs | 4 ++-- crates/lib/src/bootc_composefs/status.rs | 4 ++-- crates/lib/src/cli.rs | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 9f15d31fca..dd15d7c2f8 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -329,7 +329,7 @@ pub(crate) async fn write_composefs_state( } pub(crate) fn composefs_usr_overlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> { - let status = get_composefs_usr_overlay_status()?; + let status = get_usr_overlay_status()?; if status.is_some() { println!("An overlayfs is already mounted on /usr"); return Ok(()); @@ -351,7 +351,7 @@ pub(crate) fn composefs_usr_overlay(access_mode: FilesystemOverlayAccessMode) -> Ok(()) } -pub(crate) fn get_composefs_usr_overlay_status() -> Result> { +pub(crate) fn get_usr_overlay_status() -> Result> { let usr = Dir::open_ambient_dir("/usr", ambient_authority()).context("Opening /usr")?; let is_usr_mounted = usr .is_mountpoint(".") diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 512608d824..a5368b2172 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -18,7 +18,7 @@ use crate::{ bootc_composefs::{ boot::BootType, selinux::are_selinux_policies_compatible, - state::{get_composefs_usr_overlay_status, read_origin}, + state::{get_usr_overlay_status, read_origin}, utils::{compute_store_boot_digest_for_uki, get_uki_cmdline}, }, composefs_consts::{ @@ -1142,7 +1142,7 @@ async fn composefs_deployment_status_from( host.spec.boot_order = BootOrder::Rollback }; - host.status.usr_overlay = get_composefs_usr_overlay_status().ok().flatten(); + host.status.usr_overlay = get_usr_overlay_status().ok().flatten(); set_soft_reboot_capability(storage, &mut host, sorted_bls_config, cmdline)?; diff --git a/crates/lib/src/cli.rs b/crates/lib/src/cli.rs index 2af3b8a09f..ee390bc384 100644 --- a/crates/lib/src/cli.rs +++ b/crates/lib/src/cli.rs @@ -42,6 +42,7 @@ use serde::{Deserialize, Serialize}; use crate::bootc_composefs::delete::delete_composefs_deployment; use crate::bootc_composefs::gc::{GCOpts, composefs_gc}; use crate::bootc_composefs::soft_reboot::{prepare_soft_reboot_composefs, reset_soft_reboot}; +use crate::bootc_composefs::state::get_usr_overlay_status; use crate::bootc_composefs::{ digest::{compute_composefs_digest, new_temp_composefs_repo}, finalize::{composefs_backend_finalize, get_etc_diff}, @@ -1844,6 +1845,12 @@ async fn edit(opts: EditOpts) -> Result<()> { /// Implementation of `bootc usroverlay` async fn usroverlay(access_mode: FilesystemOverlayAccessMode) -> Result<()> { + let status = get_usr_overlay_status()?; + if status.is_some() { + println!("An overlayfs is already mounted on /usr"); + return Ok(()); + } + // This is just a pass-through today. At some point we may make this a libostree API // or even oxidize it. let args = match access_mode { From 5dba22e353c4c84dd0930d2ff6eff3bc9760c3bc Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Mon, 24 Aug 2026 13:57:03 +0530 Subject: [PATCH 08/12] tmt/test/repart: Remove usr-overlay state for ostree Before trying to run `bootc install to-disk` manually remove `/run/ostree/deployment-state/...` as ostree checks a file in this directory to test whether the current deployment is unlocked. 5b522162d2bdf2932086eb54d9ceec36513f869a adds a check in the ostree path to early return if an overlay already exists on top of `/usr`, but the way `systemd-run` works with `Mount=slave` makes it so that no overlay exists on top of `/usr` but the file in `/run/ostree/deployment-state/..` remains intact which causes ostree to throw an error. Signed-off-by: Pragyan Poudyal --- tmt/tests/booted/test-install-repart.nu | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tmt/tests/booted/test-install-repart.nu b/tmt/tests/booted/test-install-repart.nu index 41d1510508..dee1d602eb 100644 --- a/tmt/tests/booted/test-install-repart.nu +++ b/tmt/tests/booted/test-install-repart.nu @@ -33,6 +33,12 @@ def run_install_to_disk [ target_image: string extra_bootc_args: list ] { + # Remove usr overlay state for ostree + # We still need this even though we now have usr-overlay check in ostree branch + # as well, because ostree checks for a file inside /run/ostree/deployment-state/... + # for usr-overlay status + rm -rvf /run/ostree/deployment-state + let composefs_args = if (tap is_composefs) { ["--composefs-backend", "--bootloader", $bootloader] } else { From 912e5052f56bbc76e36c0de16555148d55742c1a Mon Sep 17 00:00:00 2001 From: Pragyan Poudyal Date: Tue, 25 Aug 2026 12:15:27 +0530 Subject: [PATCH 09/12] Handle older systemd-repart without partno output in JSON Older systemd-repart versions (ex. the one in c9s) do not include the partno field in the JSON output. Make the field optional and fall back to the array index when absent Signed-off-by: Pragyan Poudyal --- crates/lib/src/install/baseline.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 010047f725..20fbf3aa83 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -210,7 +210,10 @@ struct RepartPartition { #[serde(rename = "type")] partition_type: String, /// 0-indexed partition number - partno: u32, + /// absent in older systemd-repart + /// versions, in c9s + #[serde(default)] + partno: Option, #[allow(dead_code)] fs: Option, } @@ -338,9 +341,11 @@ fn parse_repart_layout(partitions: &[RepartPartition]) -> Result esp_partno = Some(partno), From cdb3f83a6419c68df4d4080d9dcdf3acd3fb6271 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Wed, 23 Sep 2026 14:37:45 +0530 Subject: [PATCH 10/12] repart: Computing root partn size with generic image If `--generic_image` is passed, we won't actually be sure about what to do with non root and non boot partitions, like var,home,swap etc. So, in that case, take their sizes into account when computing the size for root partition, only if an actual root partition is not defined as a systemd repart definition. The root size computation is as follows: Total Disk Size - (Sum of all partition sizes + padding) - 64 Mib The final 64Mib deduction acts as a partition headroom so that we account for GPT headers Signed-off-by: Johan-Liebert1 --- .../discoverable_partition_specification.rs | 3 + crates/lib/src/install/baseline.rs | 83 +++++++++++++++++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/crates/lib/src/discoverable_partition_specification.rs b/crates/lib/src/discoverable_partition_specification.rs index a743d17cb3..5aa0d36111 100644 --- a/crates/lib/src/discoverable_partition_specification.rs +++ b/crates/lib/src/discoverable_partition_specification.rs @@ -499,6 +499,9 @@ pub const TMP: &str = "7ec6f557-3bc5-4aca-b293-16ef5df639d1"; /// Generic Linux filesystem data partition pub const LINUX_DATA: &str = "0fc63daf-8483-4772-8e79-3d69d8477de4"; +/// BIOS boot partition +pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649"; + // ============================================================================ // ARCHITECTURE-SPECIFIC HELPERS // ============================================================================ diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 20fbf3aa83..865493de4c 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -214,6 +214,10 @@ struct RepartPartition { /// versions, in c9s #[serde(default)] partno: Option, + #[serde(default)] + raw_size: u64, + #[serde(default)] + raw_padding: u64, #[allow(dead_code)] fs: Option, } @@ -253,9 +257,12 @@ fn systemd_repart( device: &Device, root_size: Option, rootfs: Option, + generic_image: bool, ) -> Result { // Dry-run to check what partitions would be created - let dry_partitions = systemd_repart_run(device, true)?; + // Send `generic_image` as false so that we can see ALL defined + // partitions + let dry_partitions = systemd_repart_run(device, false, true)?; if dry_partitions.is_empty() { anyhow::bail!("systemd-repart returned empty partitions"); @@ -267,17 +274,45 @@ fn systemd_repart( if has_root { // Root partition is defined in repart.d config, run for real - let partitions = systemd_repart_run(device, false)?; + let partitions = systemd_repart_run(device, generic_image, false)?; let layout = parse_repart_layout(&partitions)?; return Ok(layout); } // Root partition is not defined, create defintion for the root part let mut root_conf = String::from("[Partition]\nType=root\n"); - if let Some(size_mib) = root_size { - writeln!(root_conf, "SizeMinBytes={size_mib}M")?; - writeln!(root_conf, "SizeMaxBytes={size_mib}M")?; - } + + match root_size { + Some(size_mib) => { + writeln!(root_conf, "SizeMinBytes={size_mib}M")?; + writeln!(root_conf, "SizeMaxBytes={size_mib}M")?; + } + None => { + let mb = 1024 * 1024; + // Save 64 MB as partition headroom for GPT headers + let partition_headroom = 64 * mb; + + // Installing to a disk, compute the root ptn size + // by taking all other partitions into account + // + // We're doing this to accomodate for partitions that are + // supposed to be crated on first boot, like home,var,swap etc + let space_taken = dry_partitions + .iter() + .fold(0u64, |acc, x| acc + x.raw_size + x.raw_padding); + let root_size_mib = (device.size - space_taken - partition_headroom) / mb; + + tracing::debug!( + "space_taken: {} M, device.size: {} M, Root Size: {root_size_mib} M", + space_taken / mb, + device.size / mb + ); + + writeln!(root_conf, "SizeMinBytes={root_size_mib}M")?; + writeln!(root_conf, "SizeMaxBytes={root_size_mib}M")?; + } + }; + match rootfs { Some(fs) => writeln!(root_conf, "Format={fs}")?, None => { @@ -289,8 +324,9 @@ fn systemd_repart( std::fs::write("/run/repart.d/50-root.conf", &root_conf) .context("Writing root repart config")?; - let partitions = systemd_repart_run(device, false)?; + let partitions = systemd_repart_run(device, generic_image, false)?; let layout = parse_repart_layout(&partitions)?; + Ok(layout) } @@ -298,7 +334,11 @@ fn systemd_repart( /// `dry_run`: if true, no changes are written to disk. /// `definitions`: if set, uses `--definitions=` and `--empty=allow`; /// otherwise uses the default config search paths with `--empty=force`. -fn systemd_repart_run(device: &Device, dry_run: bool) -> Result> { +fn systemd_repart_run( + device: &Device, + generic_image: bool, + dry_run: bool, +) -> Result> { let mut cmd = Command::new("systemd-repart"); // Enable fsverity for ext4 @@ -318,6 +358,18 @@ fn systemd_repart_run(device: &Device, dry_run: bool) -> Result Result Date: Wed, 23 Sep 2026 15:11:00 +0530 Subject: [PATCH 11/12] tmt: Simulate first boot partition creation Simulate it by running systemd-repart again on the disk and test whether all partitions are created properly or not Signed-off-by: Johan-Liebert1 --- tmt/tests/booted/test-install-repart.nu | 95 ++++++++++++++++++++----- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/tmt/tests/booted/test-install-repart.nu b/tmt/tests/booted/test-install-repart.nu index dee1d602eb..f82bf49785 100644 --- a/tmt/tests/booted/test-install-repart.nu +++ b/tmt/tests/booted/test-install-repart.nu @@ -14,8 +14,10 @@ let bootloader = if ($st.status.booted.composefs? != null) { "grub" } -# We need this for grub installation -let bios = if $bootloader == "grub" { +# We need this for grub and grub-cc installation as grub-cc +# installation currently is just a frankeinstined version of grub +# install so we need BIOS partition +let bios = if $bootloader == "grub" or $bootloader == "grub-cc" { " RUN < /usr/lib/repart.d/00-bios.conf [Partition] @@ -48,7 +50,7 @@ def run_install_to_disk [ let volume = $"-v /dev:/dev -v /run/udev:/run/udev -v /var/disk.img:/disk.img" let base = $"podman run --rm --privileged ($volume) --pid=host --security-opt label=type:unconfined_t --env BOOTC_BOOTLOADER_DEBUG=1 ($target_image)" let args = $"($composefs_args | str join ' ') ($extra_bootc_args | str join ' ')" - let bootc = $"bootc install to-disk ($args) --disable-selinux --via-loopback --source-imgref containers-storage:($target_image) /disk.img" + let bootc = $"bootc install to-disk ($args) --disable-selinux --via-loopback --wipe --source-imgref containers-storage:($target_image) /disk.img" tap run_install $"($base) ($bootc)" } @@ -110,6 +112,39 @@ EOF rm -rvf /var/disk.img } +def verify_part_layout_second_boot [] { + # Verify partition layout + let loop = (losetup -f --show /var/disk.img | str trim) + try { + partx -u $loop + udevadm settle + let parts = (lsblk -J --bytes -o name,parttype,partuuid,size $loop | from json) + print $parts + let children = ($parts.blockdevices.0.children) + let part_types = ($children | get parttype) + + # ESP GUID + let esp_guid = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" + assert ($part_types | any {|t| ($t | str downcase) == $esp_guid }) "ESP partition not found" + + # Should have ESP + root (and Bios for grub) + assert (($children | length) >= 2) "Expected at least 2 partitions" + + # Verify root is not using all disk space (--root-size 7G was specified) + let root_part = ($children | last) + let root_size_bytes = ($root_part.size | into int) + let seven_gb = (7 * 1024 * 1024 * 1024) + assert ($root_size_bytes <= $seven_gb) $"Root partition should be ~7G, got ($root_part.size)" + + print "PASS: repart created ESP, bootc generated root with correct size" + } catch { |e| + losetup -d $loop + error make { msg: $"Verification failed: ($e.msg)" } + } + + losetup -d $loop +} + def test_repart_no_root [] { tap begin "install with systemd-repart (ESP only, root generated by bootc)" @@ -124,16 +159,24 @@ RUN <<'EOF' cat > /usr/lib/repart.d/00-esp.conf [Partition] Type=esp Format=vfat -SizeMinBytes=512M -SizeMaxBytes=512M +SizeMinBytes=1024M +SizeMaxBytes=1024M EOF RUN <<'EOF' cat > /usr/lib/repart.d/20-home.conf [Partition] Type=home Format=ext4 -SizeMinBytes=512M -SizeMaxBytes=512M +SizeMinBytes=1024M +SizeMaxBytes=1024M +EOF + +RUN <<'EOF' cat > /usr/lib/repart.d/30-swap.conf +[Partition] +Type=swap +Format=swap +SizeMinBytes=1024M +SizeMaxBytes=1024M EOF " (tap make_uki_containerfile $dockerfile) | podman build -t localhost/bootc-repart-noroot . -f - @@ -144,6 +187,24 @@ EOF run_install_to_disk localhost/bootc-repart-noroot ["--filesystem" "ext4" "--root-size" "7G"] # Verify partition layout + verify_part_layout_second_boot + + # Install again without passing in root-size make sure it works + run_install_to_disk localhost/bootc-repart-noroot ["--filesystem" "ext4"] + + # Verify partition layout (again) + verify_part_layout_second_boot + + # Now run systemd-repart on the disk again to simulate what would happen on first boot + print "Runnin systemd-repart to simulate first boot" + ( + podman run --privileged --rm + -v /dev:/dev + -v /var/disk.img:/output/disk.img + localhost/bootc-repart-noroot + systemd-repart --dry-run=no --json=pretty --no-pager /output/disk.img + ) + let loop = (losetup -f --show /var/disk.img | str trim) try { partx -u $loop @@ -153,23 +214,19 @@ EOF let children = ($parts.blockdevices.0.children) let part_types = ($children | get parttype) - # ESP GUID - let esp_guid = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" - assert ($part_types | any {|t| ($t | str downcase) == $esp_guid }) "ESP partition not found" + # Should have ESP + root + home + swap (and Bios for grub) + assert (($children | length) >= 4) "Expected at least 4 partitions" - # Should have ESP + root + home (and Bios for grub) - assert (($children | length) >= 3) "Expected at least 3 partitions" + let home_guid = "933ac7e1-2eb4-4f13-b844-0e14e2aef915" + assert ($part_types | any {|t| ($t | str downcase) == $home_guid }) "home partition not found" - # Verify root is not using all disk space (--root-size 5G was specified) - let root_part = ($children | last) - let root_size_bytes = ($root_part.size | into int) - let seven_gb = (7 * 1024 * 1024 * 1024) - assert ($root_size_bytes <= $seven_gb) $"Root partition should be ~7G, got ($root_part.size)" + let swap_guid = "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f" + assert ($part_types | any {|t| ($t | str downcase) == $swap_guid }) "swap partition not found" - print "PASS: repart created ESP, bootc generated root with correct size" + print "PASS: repart first boot verified" } catch { |e| losetup -d $loop - error make { msg: $"Verification failed: ($e.msg)" } + error make { msg: $"repart first boot verification failed: ($e.msg)" } } losetup -d $loop From 8d6d76e952918bd0feee7c8e711752485adc9749 Mon Sep 17 00:00:00 2001 From: Johan-Liebert1 Date: Fri, 25 Sep 2026 12:25:11 +0530 Subject: [PATCH 12/12] repart: Support image installs on older systemd CentOS 9 ships systemd 252, which predates two capabilities we relied on viz `--include-partitions` and automatic enablement of fs-verity on ext4 - For restricting a generic image to only make the partitions required for a first boot (root, ESP/BOOT), fallback to the older method of copying only those repart definitions to a particular directory and passing `--definitions=` to systemd-repart - Manually enable fs-verity using tune2fs on ext4 systems on older systemd-repart versions Signed-off-by: Johan-Liebert1 --- crates/lib/src/bootloader.rs | 14 +-- crates/lib/src/install/baseline.rs | 125 +++++++++++++++++++++++++-- crates/lib/src/parsers/bls_config.rs | 4 +- 3 files changed, 129 insertions(+), 14 deletions(-) diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index 8b44c27b18..dd96976ca7 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -290,7 +290,7 @@ pub(crate) fn install_systemd_boot( if configopts.generic_image { bootctl_args.push("--no-variables"); // `--random-seed` was only added to `bootctl install` in systemd 257. - let systemd_version = bootctl_systemd_version()?; + let systemd_version = systemd_version()?; if systemd_version >= BOOTCTL_RANDOM_SEED_MIN_VERSION { bootctl_args.extend(["--random-seed", "no"]); } else { @@ -361,15 +361,19 @@ pub(crate) fn install_systemd_boot( Ok(()) } -#[context("Querying bootctl version")] -pub(crate) fn bootctl_systemd_version() -> Result { +/// Query the major version of systemd via `systemctl --version`, caching the +/// result so it can be shared across callers (bootctl, systemd-repart, etc.). +#[context("Querying systemd version")] +pub(crate) fn systemd_version() -> Result { static VERSION: OnceLock = OnceLock::new(); if let Some(v) = VERSION.get() { return Ok(*v); }; - let out = Command::new("bootctl").arg("--version").run_get_string()?; + let out = Command::new("systemctl") + .arg("--version") + .run_get_string()?; let v = parse_systemd_version(&out).context("Failed to parse version to integer")?; let version = VERSION.get_or_init(|| v); @@ -379,7 +383,7 @@ pub(crate) fn bootctl_systemd_version() -> Result { /// Parse the systemd major version from `bootctl --version` output, whose first /// line looks like `systemd 259 (259.5-0ubuntu3)`. -fn parse_systemd_version(output: &str) -> Result { +pub(crate) fn parse_systemd_version(output: &str) -> Result { output .split_whitespace() .nth(1) diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 865493de4c..86061dffbe 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -31,6 +31,8 @@ use super::RW_KARG; use super::RootSetup; use super::State; use super::config::Filesystem; +use crate::bootloader::systemd_version; +use crate::discoverable_partition_specification::BIOS_BOOT; use crate::task::Task; #[cfg(feature = "install-to-disk")] use bootc_mount::is_mounted_in_pid1_mountns; @@ -220,6 +222,8 @@ struct RepartPartition { raw_padding: u64, #[allow(dead_code)] fs: Option, + /// The file used to generate this partition + file: String, } fn can_use_systemd_repart() -> bool { @@ -250,6 +254,83 @@ fn can_use_systemd_repart() -> bool { return has_config; } +/// The first systemd version that supports `systemd-repart --include-partitions=`. +const REPART_INCLUDE_PARTITIONS_MIN_VERSION: u32 = 253; + +/// The first systemd version whose systemd-repart honors the +/// `SYSTEMD_REPART_MKFS_OPTIONS_` environment variable (used to enable +/// ext4 fs-verity). Older versions silently ignore it. +const REPART_MKFS_OPTIONS_MIN_VERSION: u32 = 254; + +/// Directory we assemble the filtered set of repart.d definitions into when +/// emulating `--include-partitions=` on systemd older than +/// [`REPART_INCLUDE_PARTITIONS_MIN_VERSION`]. +const REPART_FILTERED_DEFINITIONS_DIR: &str = "/tmp/repart.d"; + +/// The repart.d configuration search directories, in descending priority. A +/// definition present in a higher-priority directory masks a same-named one +/// below it (matching systemd's own semantics). +const REPART_CONFIG_DIRS: &[&str] = &[ + "/etc/repart.d", + "/run/repart.d", + "/usr/local/lib/repart.d", + "/usr/lib/repart.d", +]; + +/// Whether we must emulate `--include-partitions=` by pre-filtering the +/// definitions ourselves +fn need_filtered_definitions(generic_image: bool) -> Result { + Ok(generic_image && systemd_version()? < REPART_INCLUDE_PARTITIONS_MIN_VERSION) +} + +/// Whether a partition (as reported by systemd-repart) is one we must create +/// even for a generic image: root, ESP or BIOS boot. +fn repart_partition_is_required(part: &RepartPartition) -> bool { + let ptype = part.partition_type.as_str(); + ptype.starts_with("root") + || ptype.starts_with("esp") + || ptype == "bios" + || ptype.eq_ignore_ascii_case(BIOS_BOOT) +} + +/// Collect the repart.d definitions for the partitions we require (root, ESP +/// and BIOS boot) into [`REPART_FILTERED_DEFINITIONS_DIR`]. +/// +/// systemd < 253 does not support the `--include-partitions=` option, so instead +/// we point systemd-repart at a directory containing only the definitions we want +/// it to act on +fn collect_repart_definitions(dry_partitions: &[RepartPartition]) -> Result<()> { + let dest_dir = Path::new(REPART_FILTERED_DEFINITIONS_DIR); + // Start from a clean directory so stale definitions from a previous run + // don't leak in. + if dest_dir.exists() { + std::fs::remove_dir_all(dest_dir) + .with_context(|| format!("Removing {REPART_FILTERED_DEFINITIONS_DIR}"))?; + } + + std::fs::create_dir_all(dest_dir) + .with_context(|| format!("Creating {REPART_FILTERED_DEFINITIONS_DIR}"))?; + + for part in dry_partitions { + if !repart_partition_is_required(part) { + continue; + } + + // Older version of systemd-repart does not provide the full path to the file + // so we need to search one by one + let src = REPART_CONFIG_DIRS + .iter() + .map(|d| Path::new(d).join(&part.file)) + .find(|p| p.exists()) + .ok_or_else(|| anyhow::anyhow!("Could not find repart.d definition {}", part.file))?; + + std::fs::copy(&src, dest_dir.join(&part.file)) + .with_context(|| format!("Copying repart.d definition {}", src.display()))?; + } + + Ok(()) +} + /// Create partitions using systemd-repart /// Assumes we have systemd-repart definitions #[context("Running systemd-repart")] @@ -274,6 +355,10 @@ fn systemd_repart( if has_root { // Root partition is defined in repart.d config, run for real + if need_filtered_definitions(generic_image)? { + collect_repart_definitions(&dry_partitions)?; + } + let partitions = systemd_repart_run(device, generic_image, false)?; let layout = parse_repart_layout(&partitions)?; return Ok(layout); @@ -320,9 +405,19 @@ fn systemd_repart( } } - std::fs::create_dir_all("/run/repart.d").context("Creating /run/repart.d")?; - std::fs::write("/run/repart.d/50-root.conf", &root_conf) - .context("Writing root repart config")?; + if need_filtered_definitions(generic_image)? { + collect_repart_definitions(&dry_partitions)?; + + std::fs::write( + Path::new(REPART_FILTERED_DEFINITIONS_DIR).join("50-root.conf"), + &root_conf, + ) + .context("Writing root repart config to filtered definitions")?; + } else { + std::fs::create_dir_all("/run/repart.d").context("Creating /run/repart.d")?; + std::fs::write("/run/repart.d/50-root.conf", &root_conf) + .context("Writing root repart config")?; + } let partitions = systemd_repart_run(device, generic_image, false)?; let layout = parse_repart_layout(&partitions)?; @@ -343,6 +438,8 @@ fn systemd_repart_run( // Enable fsverity for ext4 // btrfs has fsverity enabled out of the box + // + // NOTE: systemd < v254 doesn't support this cmd.env("SYSTEMD_REPART_MKFS_OPTIONS_EXT4", "-O verity"); let dry_run_arg = if dry_run { @@ -364,10 +461,13 @@ fn systemd_repart_run( // or if this disk would be used to create an AMI/VHD and a separate disk // would be used for the other partitions if generic_image { - cmd.arg(format!( - "--include-partitions=root,esp,{}", - crate::discoverable_partition_specification::BIOS_BOOT - )); + if need_filtered_definitions(generic_image)? { + // Older systemd lacks --include-partitions; act only on the + // pre-filtered definitions collected by collect_repart_definitions. + cmd.arg(format!("--definitions={REPART_FILTERED_DEFINITIONS_DIR}")); + } else { + cmd.arg(format!("--include-partitions=root,esp,{BIOS_BOOT}")); + } } cmd.arg(device.path()); @@ -745,6 +845,17 @@ pub(crate) fn install_create_rootfs( anyhow::anyhow!("Root device created by repart has no filesystem UUID") })?; + // systemd < v254 (e.g. c9s) doesn't honor SYSTEMD_REPART_MKFS_OPTIONS_EXT4 + // so enable verity ourselves + if systemd_version()? < REPART_MKFS_OPTIONS_MIN_VERSION && root_filesystem == "ext4" { + tracing::debug!("Manually enabling fs-verity on root partition"); + + Command::new("tune2fs") + .args(["-O", "verity", root_device.path().as_str()]) + .run_inherited() + .context("Running tune2fs enabling fs-verity")?; + } + u.parse::() .with_context(|| format!("Parsing root fs UUID {u}"))? } else { diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 157e0f3632..c299a7b663 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -13,7 +13,7 @@ use std::fmt::Display; use uapi_version::Version; use crate::bootc_composefs::status::ComposefsCmdline; -use crate::bootloader::bootctl_systemd_version; +use crate::bootloader::systemd_version; use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX}; use crate::spec::Bootloader; @@ -40,7 +40,7 @@ impl EFIKey { EFIKey::Efi(path) } else { // Check systemd version for non-GrubCC bootloaders - match bootctl_systemd_version() { + match systemd_version() { Ok(version) if version >= SYSTEMD_UKI_MIN_VERSION => EFIKey::Uki(path), _ => EFIKey::Efi(path), }