diff --git a/patches/pkgbuild/kbd.sh b/patches/pkgbuild/kbd.sh new file mode 100755 index 0000000..0e86f2b --- /dev/null +++ b/patches/pkgbuild/kbd.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# kbd: --disable-xkb. Found by nothing except the artefact. +# +# $ readelf -d usr/bin/loadkeys | grep NEEDED +# (NEEDED) Shared library: [libxkbcommon.so.0] +# +# $ bsdtar -xOf kbd-*.pkg.tar.gz .PKGINFO | grep depend +# depend = glibc +# depend = gzip +# depend = pam +# +# THE POINT OF THIS HOOK IS THE GAP BETWEEN THOSE TWO OUTPUTS. loadkeys needs +# a library the package does not declare, so pacman's resolver is satisfied, +# the rootfs installs cleanly, and loadkeys does not start. No dependency +# check can see this -- not pacman's, and not the closure computation in this +# port, because both read declarations. Only reading the binaries finds it. +# scripts/test-chroot.sh grew a fourth check for exactly this class. +# +# WHERE IT CAME FROM, and it is worth admitting plainly: configure.ac:362 +# +# AC_ARG_ENABLE([xkb], ... [default=auto]) +# AS_IF([test "x$USE_XKB" != xno], +# [PKG_CHECK_MODULES(XKBCOMMON, xkbcommon, [USE_XKB=yes], [USE_XKB=no])]) +# +# 'auto' again -- a default read from the build host. libxkbcommon-dev is on +# this machine because it was added to install_host_deps FOR SYSTEMD. kbd, an +# unrelated package built later, probed for it, found it, and silently linked +# a library nothing in this port ships. One host package, one broken binary in +# a different package, undeclared. +# +# Arch's kbd declares neither libxkbcommon in depends nor in makedepends, so +# Arch's build chroot fails the same probe and Arch ships loadkeys without xkb +# support. Disabling converges with Arch rather than diverging from it. +# +# The alternative was building libxkbcommon, and it is not close: measured, it +# wants libxcb and xkeyboard-config behind it -- an X11 subtree, to generate +# keymaps from an XKB database, on a mainframe with no graphics adapter. +set -euo pipefail +grep -q '^\s\+--disable-tests$' PKGBUILD || { + echo "kbd: configure block not in the expected form" >&2; exit 1; } +sed -i 's|^\(\s*\)--disable-tests$|\1--disable-tests \\\n\1--disable-xkb|' PKGBUILD +[ "$(grep -c -- '--disable-xkb' PKGBUILD)" = 1 ] || { + echo "kbd: --disable-xkb not inserted exactly once" >&2; exit 1; } +echo "kbd: --disable-xkb (host libxkbcommon-dev, installed for systemd, leaked in)" diff --git a/scripts/test-chroot.sh b/scripts/test-chroot.sh index f7a9cbd..add3529 100755 --- a/scripts/test-chroot.sh +++ b/scripts/test-chroot.sh @@ -19,7 +19,14 @@ # 2. ARTEFACT -- static audit of every package for host contamination that # does not raise an error: Debian multiarch libdirs, files # under /usr/local, binaries linked to libselinux. -# 3. RUN -- chroot in and execute the binaries. The only check that +# 3. SONAME -- every library any shipped binary ASKS for, minus every +# library the repository SHIPS. This is the check that reads +# binaries instead of declarations, and it is the only one +# that can see an under-declared dependency: kbd's loadkeys +# needed libxkbcommon.so.0 while kbd declared glibc, gzip +# and pam. RESOLVE was satisfied, the rootfs installed, and +# loadkeys could not start. +# 4. RUN -- chroot in and execute the binaries. The only check that # can catch a missing ld.so, because a package whose # interpreter is absent installs perfectly. # @@ -30,6 +37,19 @@ WORK="${WORK:-$HOME/work/arch-s390x}" REPO="${REPO:-$WORK/repo/s390x}" ROOT="${ROOT:-$WORK/rootfs-test}" CONF="$WORK/pacman-test.conf" +# A cache of its own, wiped every run. +# +# pacman's default cache is /var/cache/pacman/pkg, which is HOST-WIDE and +# survives between runs. A package rebuilt at the same pkgver-pkgrel -- which +# every fix in this port does -- leaves the old file there while core.db +# records the new checksum, and the next install stops on +# +# File .../coreutils-9.11-2-s390x.pkg.tar.gz is corrupted +# (invalid or corrupted package (checksum)) +# +# which reads like a damaged build rather than a stale copy. The shared cache +# is also not this test's to empty: other work on this host uses it. +CACHE="$WORK/pacman-test.cache" # The set a rootfs needs to reach a shell prompt and manage itself. filesystem # is not optional and not obvious: its usr-merge symlinks are what create @@ -61,11 +81,11 @@ note "1. RESOLVE -- pacman's own dependency check, --nodeps OFF" # The root and its dbpath must EXIST before alpm will initialise -- pacman # reports that as "failed to resolve path ... passed to --root", which reads # like a bad argument rather than a directory it declined to create. -sudo rm -rf "$ROOT.probe"; sudo mkdir -p "$ROOT.probe/var/lib/pacman" +sudo rm -rf "$ROOT.probe" "$CACHE"; sudo mkdir -p "$ROOT.probe/var/lib/pacman" "$CACHE" # -Syp, not -Sp. A fresh dbpath has no sync database, and without -y pacman # reports every package as "target not found" -- which reads like an empty # repository rather than an unread index. -if sudo pacman --root "$ROOT.probe" --config "$CONF" \ +if sudo pacman --root "$ROOT.probe" --config "$CONF" --cachedir "$CACHE" \ --noconfirm -Syp "${PKGS[@]}" > "$WORK/resolve.txt" 2>&1; then good "resolver satisfied ($(grep -c '^file://' "$WORK/resolve.txt") packages)" else @@ -84,9 +104,45 @@ for f in "$REPO"/*.pkg.tar.*; do done [ "$n" -eq 0 ] && good "no Debian multiarch libdir anywhere" -note "3. RUN -- install for real, then chroot" -sudo rm -rf "$ROOT"; sudo mkdir -p "$ROOT/var/lib/pacman" -if ! sudo pacman --root "$ROOT" --config "$CONF" --noconfirm -Sy "${PKGS[@]}" \ +note "3. SONAME -- what binaries ask for versus what the repository ships" +# Two passes over the packages: collect the soname each shared library +# DECLARES, and every soname each binary REQUESTS. The difference is a set of +# libraries that will be missing at runtime on the target. +# +# Most entries here are the ordinary stage-1 artefact -- the host's soname +# version rather than Arch's, e.g. libgpgme.so.11 where our gpgme package +# ships .45 -- and stage 2 resolves those by rebuilding inside the chroot. +# What must not be ignored is the other kind: a library no package in the +# repository provides at any version. +_sa=$(mktemp -d) +: > "$_sa/have"; : > "$_sa/want" +for f in "$REPO"/*.pkg.tar.*; do + rm -rf "$_sa/x"; mkdir -p "$_sa/x" + bsdtar -xf "$f" -C "$_sa/x" usr 2>/dev/null || continue + _pn=$(bsdtar -xOf "$f" .PKGINFO 2>/dev/null | sed -n 's/^pkgname = //p') + while IFS= read -r b; do + readelf -d "$b" 2>/dev/null | sed -n 's/.*Library soname: \[\(.*\)\].*/\1/p' >> "$_sa/have" + readelf -d "$b" 2>/dev/null | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p' \ + | sed "s|^|$_pn |" >> "$_sa/want" + done < <(find "$_sa/x" -type f 2>/dev/null) +done +sort -u "$_sa/have" > "$_sa/have.s" +awk '{print $2}' "$_sa/want" | sort -u > "$_sa/want.s" +if [ -s "$(comm -13 "$_sa/have.s" "$_sa/want.s" > "$_sa/miss"; echo "$_sa/miss")" ]; then + bad "$(wc -l < "$_sa/miss") soname(s) requested and never shipped:" + while read -r m; do + printf ' %-24s <- %s\n' "$m" \ + "$(awk -v m="$m" '$2==m {print $1}' "$_sa/want" | sort -u | tr '\n' ' ')" + done < "$_sa/miss" +else + good "every requested soname is shipped by some package" +fi +rm -rf "$_sa" + +note "4. RUN -- install for real, then chroot" +sudo rm -rf "$ROOT" "$CACHE"; sudo mkdir -p "$ROOT/var/lib/pacman" "$CACHE" +if ! sudo pacman --root "$ROOT" --config "$CONF" --cachedir "$CACHE" \ + --noconfirm -Sy "${PKGS[@]}" \ > "$WORK/install.txt" 2>&1; then bad "install failed, see $WORK/install.txt" tail -15 "$WORK/install.txt" | sed 's/^/ /'