Files
computer-playbook/scripts/install/filesystem.sh
T
kevinveenbirkenbachandClaude Opus 5 3de61076ff perf(images): build the filesystem userlands in a layer the source tree cannot invalidate
The filesystem install ran in the RUN after 'COPY .', so any commit
rebuilt the zfs userland: the AUR build on Arch and, on arm64, the
source RPM rebuild on Fedora and CentOS, all of it under emulation now
that one job builds every architecture. It now runs in its own layer
right after default.env and scripts/install are copied, which only
those two paths invalidate. apt-mirrors.sh moves with it and still runs
first.

Running before the Python step, filesystem.sh no longer finds a system
that step already upgraded, so its pacman call does the full -Syu
instead of a partial -Sy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 20:04:24 +02:00

305 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bake the userlands for every filesystem the docker data root can run on into
# the distro image: ext4, btrfs and zfs. The kernel side always comes from the
# host, but the tools have to sit in the image - dockerd resolves `zfs` on PATH
# and opens /dev/zfs before it accepts a data root, and mkfs.ext4 / mkfs.btrfs
# on the host are equally useless to a container that cannot call them. A pick
# the kernel could serve still fails when the image lacks the userland.
#
# The three differ sharply in what that costs:
#
# * ext4 and btrfs are ordinary packages everywhere except the EL family,
# where RHEL dropped btrfs and btrfs-progs exists only in EPEL. A plain
# `dnf install btrfs-progs` there fails the whole transaction and takes
# e2fsprogs down with it, so EPEL goes in first. The split is by distro
# rather than by a failed first attempt, because on Fedora there is no
# epel-release to fall back to: routing a transient failure there would
# turn it into a fatal one and hide the cause behind an EPEL message.
# * The OpenZFS `zfs` RPM carries an ungated `Requires: %{name}-kmod`, so a
# plain `dnf install zfs` resolves through zfs-dkms and drags in
# kernel-devel, kernel-debug-core and a DKMS build against a kernel the
# container never boots: measured at 389 extra packages and 1.44 GB. A
# dummy package that only *provides* zfs-kmod keeps the same install at 60
# packages with no kernel package at all. Its version must equal the exact
# %{version} dnf resolves for `zfs`; a mismatch silently falls back to DKMS.
# Because that version is pinned, the repository is removed again once the
# userland is in: leaving it enabled means the next `dnf upgrade` finds a
# newer zfs whose kmod dependency the dummy no longer satisfies, and the
# whole DKMS pull returns at deploy time rather than at build time.
# zfsonlinux publishes Fedora and EL binaries for x86_64 only, and on any
# other architecture the binary repository has no metadata at all: dnf5
# skips it, dnf4 aborts every transaction that loads it. There the zfs
# source RPM from the same release is rebuilt instead, with the binary
# repository disabled and zfs-release gone before any further dnf call.
# Its build dependencies include the protected systemd, which `dnf remove`
# refuses to touch, so they are marked as dependencies and autoremoved once
# the userland holds on to whatever it still needs.
# * Debian keeps zfsutils-linux in contrib, which the base image does not
# enable. Without the extra source apt reports no installation candidate.
# * Arch has no zfs userland in core/extra. archzfs publishes a prebuilt
# zfs-utils, but only on a rolling release tag whose asset filename carries
# the version, so any pinned URL 404s on the next upstream bump. Building
# the AUR PKGBUILD (which configures --with-config=user, userland only) is
# the reproducible route and costs about six minutes. That build runs with
# --skippgpcheck rather than --skipinteg: the release tarball's sha256 and
# b2 sums stay verified, and only the detached OpenZFS signature is skipped,
# because checking it means carrying the release key in the builder's
# keyring while the same PKGBUILD already supplies both the sums and the
# list of accepted keys, so the signature adds no trust the sums do not.
set -euo pipefail
RETRIES=3
AUR_BUILDER=pkgbuild
ZFS_RELEASE_FEDORA="3-1 3-0 2-8"
ZFS_RELEASE_EL="3-0 2-8 2-3"
log() { echo "[filesystem] $*"; }
# Param: $@ command to run, retried on failure with a widening backoff
retry() {
local attempt=1
until "$@"; do
if [ "${attempt}" -ge "${RETRIES}" ]; then
log "ERROR: '$*' failed after ${attempt} attempts"
return 1
fi
log "attempt ${attempt} of '$1' failed; retrying"
sleep $((attempt * 5))
attempt=$((attempt + 1))
done
}
install_apt() {
export DEBIAN_FRONTEND=noninteractive
local apt_opts=(-o Acquire::Retries="${RETRIES}" -qq)
if [ "${ID:-}" = debian ]; then
log "enabling contrib for ${VERSION_CODENAME:?}, which is where zfsutils-linux lives"
echo "deb http://deb.debian.org/debian ${VERSION_CODENAME} contrib" \
>/etc/apt/sources.list.d/contrib.list
fi
retry apt-get "${apt_opts[@]}" update
retry apt-get "${apt_opts[@]}" install -y --no-install-recommends \
e2fsprogs btrfs-progs zfsutils-linux
rm -rf /var/lib/apt/lists/*
}
install_pacman() {
retry pacman -Syu --noconfirm --needed base-devel git e2fsprogs btrfs-progs
if ! id "${AUR_BUILDER}" >/dev/null 2>&1; then
useradd -m -s /bin/bash "${AUR_BUILDER}"
fi
install -d -m 0755 /etc/sudoers.d
printf '%s ALL=(ALL) NOPASSWD: /usr/bin/pacman\n' "${AUR_BUILDER}" \
>/etc/sudoers.d/99-"${AUR_BUILDER}"-pacman
chmod 0440 /etc/sudoers.d/99-"${AUR_BUILDER}"-pacman
local build_root=/tmp/zfs-utils-build
rm -rf "${build_root}"
install -d -o "${AUR_BUILDER}" -g "${AUR_BUILDER}" "${build_root}"
retry su "${AUR_BUILDER}" -c \
"git clone --depth 1 https://aur.archlinux.org/zfs-utils.git '${build_root}'" # nocheck: url - AUR serves this path to git clients only; a plain GET is a 404 by design
su "${AUR_BUILDER}" -c \
"cd '${build_root}' && makepkg --syncdeps --noconfirm --cleanbuild --skippgpcheck"
local pkg
pkg="$(find "${build_root}" -maxdepth 1 -type f -name 'zfs-utils-*.pkg.tar.*' \
! -name '*-debug-*' | head -n1)"
if [ -z "${pkg}" ]; then
log "ERROR: makepkg produced no zfs-utils package"
return 1
fi
pacman -U --noconfirm "${pkg}"
rm -rf "${build_root}" /var/cache/pacman/pkg
}
install_dnf_native() {
case "${ID:-}" in
fedora)
retry dnf install -y e2fsprogs btrfs-progs
return
;;
esac
log "adding EPEL, the only place the EL family carries btrfs-progs"
retry dnf install -y epel-release
retry dnf install -y e2fsprogs btrfs-progs
}
# Param: $1 path segment of the zfsonlinux repository, `fedora` or `epel`
# Param: $2 space-separated zfs-release versions to try, newest first
release_install() {
local base="$1" candidates="$2" dist version url
dist="$(rpm --eval '%{dist}')"
for version in ${candidates}; do
url="https://zfsonlinux.org/${base}/zfs-release-${version}${dist}.noarch.rpm"
if dnf install -y "${url}"; then
log "zfs-release: ${url}"
return 0
fi
log "no zfs-release ${version} for ${dist}"
done
log "ERROR: none of the zfs-release versions '${candidates}' exists for ${dist}"
return 1
}
# Param: $1 directory the zfs source RPM is downloaded into
# Output: path of the downloaded source RPM
download_zfs_source() {
local dir="$1" srpm
rm -rf "${dir}"
mkdir -p "${dir}"
if command -v dnf5 >/dev/null 2>&1; then
retry dnf install -y --disablerepo=zfs dnf5-plugins >&2
else
retry dnf install -y --disablerepo=zfs dnf-plugins-core >&2
fi
retry dnf download --source --disablerepo=zfs --enablerepo=zfs-source --destdir="${dir}" zfs >&2
srpm="$(find "${dir}" -maxdepth 1 -name 'zfs-*.src.rpm' | head -n1)"
if [ -z "${srpm}" ]; then
log "ERROR: the zfs-source repository offers no zfs source RPM" >&2
return 1
fi
printf '%s\n' "${srpm}"
}
# Param: $1 zfs source RPM to rebuild for the running architecture
# Param: $2 sorted package names installed before any zfs build tooling
install_zfs_from_source() {
local srpm="$1" before="$2" topdir=/tmp/zfs-rebuild
local -a added runtime crb=()
if [ "${ID:-}" != fedora ]; then
crb=(--enablerepo=crb)
fi
retry dnf builddep -y "${crb[@]}" "${srpm}"
mapfile -t added < <(rpm -qa --qf '%{NAME}\n' | sort | comm -13 "${before}" -)
rm -rf "${topdir}"
rpmbuild --define "_topdir ${topdir}" --rebuild "${srpm}"
mapfile -t runtime < <(find "${topdir}/RPMS" -name '*.rpm' \
! -name '*-debug*' ! -name '*-devel-*' ! -name 'zfs-test*' \
! -name 'zfs-dracut*' ! -name 'python3-pyzfs*' ! -name 'pam_zfs_key*' \
! -name 'zfs-dkms*' ! -name 'zfs-kmod*')
retry dnf install -y "${runtime[@]}"
log "dropping ${#added[@]} build dependencies of the zfs rebuild"
if command -v dnf5 >/dev/null 2>&1; then
dnf mark -y dependency "${added[@]}"
else
dnf mark remove "${added[@]}"
fi
dnf autoremove -y
rm -rf "${topdir}" "$(dirname "${srpm}")"
}
install_dnf() {
install_dnf_native
case "${ID:-}" in
fedora) retry release_install fedora "${ZFS_RELEASE_FEDORA}" ;;
*) retry release_install epel "${ZFS_RELEASE_EL}" ;;
esac
local zfs_version source_rpm="" before=/tmp/zfs-packages-before
rpm -qa --qf '%{NAME}\n' | sort >"${before}"
zfs_version="$(dnf --quiet --setopt=zfs.skip_if_unavailable=True repoquery --qf '%{version}\n' zfs | sort -V | tail -n1)"
if [ -z "${zfs_version}" ]; then
log "the zfs repository carries no $(uname -m) build; rebuilding its source RPM"
source_rpm="$(download_zfs_source /tmp/zfs-source)"
zfs_version="$(rpm -qp --qf '%{version}' "${source_rpm}")"
dnf remove -y --disablerepo=zfs zfs-release
fi
log "zfs userland version: ${zfs_version}"
if ! command -v rpmbuild >/dev/null 2>&1; then
retry dnf install -y rpm-build
fi
local topdir=/tmp/zfs-kmod-container
rm -rf "${topdir}"
mkdir -p "${topdir}/SPECS"
cat >"${topdir}/SPECS/zfs-kmod-container.spec" <<SPEC
Name: zfs-kmod-container
Version: ${zfs_version}
Release: 1
Summary: Satisfies the zfs kernel module dependency inside a container
License: CDDL
BuildArch: noarch
Provides: zfs-kmod = ${zfs_version}
%description
The container never loads a zfs module; the host supplies it and the container
reaches it through /dev/zfs. This package exists only so the zfs userland
resolves without pulling a DKMS toolchain and a kernel it will never boot.
%files
SPEC
rpmbuild --define "_topdir ${topdir}" -bb "${topdir}/SPECS/zfs-kmod-container.spec"
dnf install -y "${topdir}"/RPMS/noarch/zfs-kmod-container-*.rpm
if [ -n "${source_rpm}" ]; then
install_zfs_from_source "${source_rpm}" "${before}"
else
retry dnf install -y zfs
dnf remove -y zfs-release
fi
if rpm -qa 'kernel-devel*' 'kernel-debug*' 'zfs-dkms' | grep -q .; then
log "ERROR: the transaction pulled kernel or DKMS packages, so the dummy did not take"
rpm -qa 'kernel-devel*' 'kernel-debug*' 'zfs-dkms' >&2
return 1
fi
rm -rf "${topdir}" "${before}"
dnf clean all
}
verify() {
local tool
for tool in mkfs.ext4 mkfs.btrfs zfs zpool; do
if ! command -v "${tool}" >/dev/null 2>&1; then
log "ERROR: ${tool} is missing after the install"
return 1
fi
done
if ldd "$(command -v zpool)" | grep -q 'not found'; then
log "ERROR: zpool has unresolved shared libraries"
ldd "$(command -v zpool)" | grep 'not found' >&2
return 1
fi
if command -v dnf >/dev/null 2>&1 && dnf repolist --enabled 2>/dev/null | grep -q '^zfs'; then
log "ERROR: a zfs repository is still enabled, so an upgrade would re-resolve the kmod"
return 1
fi
log "installed mkfs.ext4, mkfs.btrfs and $(command -v zpool); all libraries resolved"
}
main() {
# shellcheck source=/dev/null
. /etc/os-release
if command -v apt-get >/dev/null 2>&1; then
install_apt
elif command -v pacman >/dev/null 2>&1; then
install_pacman
elif command -v dnf >/dev/null 2>&1; then
install_dnf
else
log "ERROR: no supported package manager (apt/pacman/dnf)"
exit 1
fi
verify
}
main "$@"