mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 20:26:33 +00:00
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>
305 lines
11 KiB
Bash
Executable File
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 "$@"
|