From adf88771ff0c11fb0e51ef14f129d584fb471420 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Thu, 6 Nov 2025 23:11:55 +0800 Subject: [PATCH 1/3] mkfs-util: Ignore btrfs compression when there is no dir to copy mkfs.btrfs requires that the --compress option be used together with --rootdir, as compression only makes sense in that context (because compression is not a persistent setting). Right now, If --compress is specified without --rootdir, mkfs.btrfs fails with: ERROR: --compression must be used with --rootdir This can occur when repart is configured with Compression= but the partition populate logic doesn't use the --rootdir code path (eg. when using loop device mounting to copy files after mkfs). Add a defensive check to skip compression and emit a user-friendly warning when compression is requested but no root directory is provided. The warning message references the repart directive names (Compression= and CopyFiles=) rather than low-level mkfs options to help users understand the requirement. This prevents crashes but doesn't enable compression, that requires ensuring the --rootdir code path is used, which it currently is not and will be addressed in the next patch. Fixes: https://github.com/systemd/systemd/issues/39584 --- src/shared/mkfs-util.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/shared/mkfs-util.c b/src/shared/mkfs-util.c index 629d02207c..455c627ff6 100644 --- a/src/shared/mkfs-util.c +++ b/src/shared/mkfs-util.c @@ -466,17 +466,22 @@ int make_filesystem( return log_oom(); if (compression) { - _cleanup_free_ char *c = NULL; + if (!root) + log_warning("Btrfs compression setting ignored because no files are being copied. " + "Compression= can only be applied when CopyFiles= is also specified."); + else { + _cleanup_free_ char *c = NULL; - c = strdup(compression); - if (!c) - return log_oom(); + c = strdup(compression); + if (!c) + return log_oom(); - if (compression_level && !strextend(&c, ":", compression_level)) - return log_oom(); + if (compression_level && !strextend(&c, ":", compression_level)) + return log_oom(); - if (strv_extend_many(&argv, "--compress", c) < 0) - return log_oom(); + if (strv_extend_many(&argv, "--compress", c) < 0) + return log_oom(); + } } /* mkfs.btrfs unconditionally warns about several settings changing from v5.15 onwards which From f30a29245d8ff4509c95bef817944299efbc3795 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Thu, 6 Nov 2025 23:17:01 +0800 Subject: [PATCH 2/3] repart: Force --rootdir population for btrfs with compression When a btrfs partition is configured with both Compression= and CopyFiles=, we need to ensure files are copied during filesystem creation using mkfs.btrfs --rootdir, rather than copying files afterwards via loop device mounting. This is required because mkfs.btrfs can only apply compression settings when files are provided via --rootdir during filesystem creation. If we format the filesystem first and then mount it to copy files, the compression setting is meaningless. Modify the partition_needs_populate() condition to force the --rootdir code path when the format is btrfs and compression is requested. This ensures that partition_populate_directory() runs and creates a temporary directory with the files, which is then passed to make_filesystem() as the root parameter, allowing mkfs.btrfs to create the filesystem with compression applied. Fixes: https://github.com/systemd/systemd/issues/39584 --- src/repart/repart.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/repart/repart.c b/src/repart/repart.c index dbf261bb26..456aed41bd 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -6785,7 +6785,8 @@ static int context_mkfs(Context *context) { * have to populate using the filesystem's mkfs's --root= (or equivalent) option. To do that, * we need to set up the final directory tree beforehand. */ - if (partition_needs_populate(p) && (!t->loop || fstype_is_ro(p->format))) { + if (partition_needs_populate(p) && + (!t->loop || fstype_is_ro(p->format) || (streq_ptr(p->format, "btrfs") && p->compression))) { if (!mkfs_supports_root_option(p->format)) return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "Loop device access is required to populate %s filesystems.", @@ -8533,7 +8534,7 @@ static int context_minimize(Context *context) { return r; } - if (!d || fstype_is_ro(p->format)) { + if (!d || fstype_is_ro(p->format) || (streq_ptr(p->format, "btrfs") && p->compression)) { if (!mkfs_supports_root_option(p->format)) return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "Loop device access is required to populate %s filesystems.", From 2091caddb85a0816893ea5901e6d583f93d1d7f2 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Thu, 6 Nov 2025 23:36:19 +0800 Subject: [PATCH 3/3] test: Add integration test for btrfs compression in repart Add testcase_btrfs_compression() to verify that btrfs partitions with Compression= and CopyFiles= directives work correctly. The test verifies the fix for issue #39584, where mkfs.btrfs would fail with "ERROR: --compression must be used with --rootdir" when repart tried to create compressed btrfs filesystems. The test creates a partition definition with Format=btrfs, Compression=zstd, and CopyFiles=, then validates: 1. systemd-repart output shows "Rootdir from:" and "Compress:", confirming that the --rootdir code path is used 2. mkfs.btrfs is invoked with both --compress and --rootdir options 3. The file is successfully copied to the filesystem 4. Compression is actually applied (verified via compsize output containing "zstd") --- test/units/TEST-58-REPART.sh | 70 ++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/units/TEST-58-REPART.sh b/test/units/TEST-58-REPART.sh index b752af3f4d..251c337425 100755 --- a/test/units/TEST-58-REPART.sh +++ b/test/units/TEST-58-REPART.sh @@ -1685,6 +1685,76 @@ EOF grep -q 'UUID=[0-9a-f-]* /home btrfs discard,rw,nodev,suid,exec,subvol=@home,zstd:1,noatime,lazytime 0 1' "$root"/etc/fstab } +testcase_btrfs_compression() { + local defs imgs loop output + + if ! systemd-analyze compare-versions "$(btrfs --version | head -n 1 | awk '{ print $2 }')" ge v6.13; then + echo "btrfs-progs is not installed or older than v6.13, skipping test." + return 0 + fi + + defs="$(mktemp -d)" + imgs="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '$defs' '$imgs'" RETURN + chmod 0755 "$defs" + + echo "*** testcase for btrfs compression with CopyFiles (OFFLINE=$OFFLINE) ***" + + # Must not be in tmpfs due to exclusions. It also must be large and + # compressible so that the compression check succeeds later. + src=/etc/test-source-file + dd if=/dev/zero of="$src" bs=1M count=1 2>/dev/null + + tee "$defs/btrfs-compressed.conf" <&1 | tee "$imgs/repart-output.txt" + rm "$src" + + output=$(cat "$imgs/repart-output.txt") + + assert_in "Rootdir from:" "$output" + assert_in "Compress:" "$output" + + if [[ "$OFFLINE" == "yes" ]] || systemd-detect-virt --quiet --container; then + echo "Skipping mount verification (requires loop devices)" + return 0 + fi + loop="$(losetup -P --show --find "$imgs/btrfs-compressed.img")" + # shellcheck disable=SC2064 + trap "umount '$imgs/mount' 2>/dev/null || true; losetup -d '$loop' 2>/dev/null || true; rm -rf '$defs' '$imgs'" RETURN + echo "Loop device: $loop" + udevadm wait --timeout=60 --settle "${loop:?}p1" + + mkdir -p "$imgs/mount" + mount -t btrfs "${loop:?}p1" "$imgs/mount" + + [[ -f "$imgs/mount/test-file" ]] + [[ "$(stat -c%s "$imgs/mount/test-file")" == "1048576" ]] + + if command -v compsize &>/dev/null; then + output=$(compsize "$imgs/mount/test-file" 2>&1) + assert_in "zstd" "$output" + fi + + umount "$imgs/mount" + losetup -d "$loop" +} + testcase_varlink_list_devices() { REPART="$(which systemd-repart)" varlinkctl introspect "$REPART"