Skip to content

Instantly share code, notes, and snippets.

@chaserhkj
Last active January 17, 2026 01:24
Show Gist options
  • Select an option

  • Save chaserhkj/ad666a558800abeae9bc51202ad0da78 to your computer and use it in GitHub Desktop.

Select an option

Save chaserhkj/ad666a558800abeae9bc51202ad0da78 to your computer and use it in GitHub Desktop.
Run garbage collection on a bootc composefs backend repo using a forked cfsctl. WARN: EXPERIMENTAL, DO NOT USE IN PRODUCTION
#!/bin/bash
# binary dependencies outside bootc, coreutils and util-linux: cfsctl jq
# use together with cfsctl from https://github.com/chaserhkj/composefs-rs (PR underway)
#
# This script cleans up bootc composefs repository, leaving only booted, stage and rollback deployments,
# pinning their images and streams running garbage collection on everything else, and finally removes
# unused BLS entires
set -e
shopt -s extglob nullglob
program=$(basename $0)
progress() {
echo "($program) $@"
}
log_and_run() {
echo "(running) $@"
"$@"
}
dry_run() {
echo "(dry run) $@"
}
[ "$(readlink /proc/self/ns/mnt)" = "$(readlink /proc/1/ns/mnt)" ] && { progress "Entering private mntns" ; exec unshare -m "$0" "$@"; }
run_prefix=dry_run
while [ $# -gt 0 ]; do
if [ $1 = -f ] || [ $1 = --force ]; then
run_prefix=log_and_run
fi
shift
done
bootc_status="$(bootc status --format json)"
for type in booted rollback staged; do
printf -v "${type}_digest" "%s" $(echo "$bootc_status" | jq -r ".status.$type.image.imageDigest // \"\" " | cut -d: -f2)
printf -v "${type}_verity" "%s" $(echo "$bootc_status" | jq -r ".status.$type.composefs.verity // \"\" ")
printf -v "${type}_boot_digest" "%s" $(echo "$bootc_status" | jq -r ".status.$type.composefs.bootDigest // \"\" ")
printf -v "${type}_boot_type" "%s" $(echo "$bootc_status" | jq -r ".status.$type.composefs.bootType // \"\" ")
printf -v "${type}_bootloader" "%s" $(echo "$bootc_status" | jq -r ".status.$type.composefs.bootloader // \"\" ")
done
progress "Resolved GC roots:"
for type in booted rollback staged; do
for item in digest verity boot_digest boot_type bootloader; do
var=${type}_${item}
echo " $var: ${!var}"
done
echo
done
efi_device=$(lsblk -o PATH -nr -Q 'PARTTYPE == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"')
gc_root_flags=
for type in booted rollback staged; do
digest_var=${type}_digest
[ -n "${!digest_var}" ] && gc_root_flags="$gc_root_flags -s oci-config-sha256:${!digest_var}"
verity_var=${type}_verity
[ -n "${!verity_var}" ] && gc_root_flags="$gc_root_flags -i ${!verity_var}"
done
progress "Remounting /sysroot RW"
$run_prefix mount --make-private /sysroot
$run_prefix mount -o remount,rw /sysroot
progress "Pruning /sysroot/state/deploy"
stale_deployments=( /sysroot/state/deploy/!($booted_verity|$rollback_verity|$staged_verity) )
$run_prefix rm -rf "${stale_deployments[@]}"
force_flag=
[ "$run_prefix" != "dry_run" ] && force_flag=--force
gc_log=/tmp/bootc-cfsctl-cleanup.gc.log
progress "Performing GC $force_flag on /sysroot/composefs and writing GC logs to $gc_log"
cfsctl_cmdline="RUST_LOG=debug cfsctl --repo /sysroot/composefs gc $gc_root_flags $force_flag >$gc_log 2>&1"
echo "(cfsctl) $cfsctl_cmdline"
eval "$cfsctl_cmdline"
echo "(cfsctl) Garbage collected objects: $(grep -E '(dry run: )?rm ' $gc_log | wc -l)"
clean_esp=yes
for type in booted rollback staged; do
var=${type}_boot_type
# when boot_type is empty string, assume the current entry is empty and skip to next entry
[ -z "${!var}" ] && continue
[ "${!var}" != "Bls" ] && clean_esp=no
var=${type}_bootloader
[ "${!var}" != "Systemd" ] && clean_esp=no
done
if [ "$clean_esp" != "yes" ]; then
progress "Unsupported bootloader/boot type is used, skip cleaning ESP"
exit 0
fi
progress "Mounting ESP"
log_and_run mount --make-private $efi_device /sysroot/boot
boot_contents=( /sysroot/boot/EFI/Linux/* )
progress "Cleaning unused kernel files"
# /sysroot/boot/EFI/Linux entires may still refer to stale deployment ids
# Try to rename them to current deployment ids
# This is necessary to make "bootc upgrade" and "bootc switch" function correctly
stale_deployment_ids=()
for path in "${stale_deployments[@]}"; do
stale_deployment_ids+=( "$(basename $path)" )
done
declare -A rename_map
for boot_content_dir in "${boot_contents[@]}"; do
boot_content_id=$(basename $boot_content_dir)
used=no
boot_digest=$(cat $boot_content_dir/vmlinuz $boot_content_dir/initrd | sha256sum | cut -d" " -f1)
echo "(ESP GC) Boot binary entry: $boot_content_id"
echo "(ESP GC) Boot binary digest: $boot_digest"
# This order decides which digest to use for rename when multiple entries share same boot binaries
# Always prioritize staged > booted > rollback
for entry_type in rollback booted staged; do
digest_var_name=${entry_type}_boot_digest
match_digest=${!digest_var_name}
if [[ "$boot_digest" == "$match_digest" ]]; then
used=yes
else
continue
fi
verity_var_name=${entry_type}_verity
match_verity=${!verity_var_name}
# Check if used boot binary is referring to stale deployment id
for stale_id in "${stale_deployment_ids[@]}"; do
[[ "$stale_id" != "$boot_content_id" ]] && continue
rename_map[$stale_id]=$match_verity
echo " mark rename, from $stale_id"
echo " to $match_verity"
done
done
[ "$used" != "yes" ] && $run_prefix rm -rf $boot_content_dir
done
progress "Renaming ESP references to stale entries"
for from in "${!rename_map[@]}"; do
to=${rename_map[$from]}
$run_prefix mv /sysroot/boot/EFI/Linux/$from /sysroot/boot/EFI/Linux/$to
$run_prefix sed -i "s/\\/$from\\//\\/$to\\//g" /sysroot/boot/loader/entries/*.conf
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment