[FIX] the build host answered questions the target should

Five packages, one shape: a default computed from this machine rather than
from Arch, and arch-meson's --auto-features enabled turning what was optional
into a requirement.

dbus taught the method. It stopped three times in a row on a different
documentation tool -- ducktype, then yelp-build, then qhelpgenerator. The
first two were installed. The third needs Qt, so the chain had no end. All
three are `auto` features promoted to mandatory; pinned off, they cost
nothing. A single missing library is a host dependency. A queue of them is a
feature that should be disabled.

pinentry wanted Qt6 for a passphrase prompt; tty and curses remain.
krb5 asked for a system ss library Ubuntu ships no headers for, and said
"cannot run test program" instead of saying so.
sqlite found tcl, then installed into share/tcltk because Ubuntu's
tclConfig.sh says so and Arch's says lib.
gettext linked the host's libselinux, silently -- the sixth package to do it,
and the first the audit caught rather than a build.

--- FR ---

Cinq paquets, une seule forme : une valeur par défaut calculée depuis cette
machine plutôt que depuis Arch, et le --auto-features enabled d'arch-meson qui
transforme l'optionnel en exigence.

dbus a enseigné la méthode. Il s'est arrêté trois fois de suite sur un outil
de documentation différent — ducktype, puis yelp-build, puis qhelpgenerator.
Les deux premiers ont été installés. Le troisième exige Qt : la chaîne ne
terminait nulle part. Les trois sont des features `auto` promues en
obligations ; épinglées à disabled, elles ne coûtent rien. Une bibliothèque
manquante est une dépendance hôte. Une file d'attente en est une
fonctionnalité à désactiver.

pinentry réclamait Qt6 pour une invite de mot de passe ; tty et curses
suffisent.
krb5 demandait une bibliothèque ss système dont Ubuntu ne livre aucun
en-tête, et annonçait « cannot run test program » plutôt que de le dire.
sqlite trouvait tcl, puis installait dans share/tcltk parce que le
tclConfig.sh d'Ubuntu le dit là où celui d'Arch dit lib.
gettext liait le libselinux de l'hôte, en silence — sixième paquet à le
faire, et le premier que l'audit a pris plutôt qu'une compilation.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-19 05:28:32 -04:00
parent 3c926f10a9
commit 09ef26798a
5 changed files with 279 additions and 0 deletions

84
patches/pkgbuild/dbus.sh Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
# dbus: three documentation generators, made mandatory by arch-meson.
#
# The build stopped three times in a row, on a different tool each time:
#
# ERROR: Program 'ducktype' not found or not executable
# ERROR: Program 'yelp-build' not found or not executable
# ERROR: Program 'qhelpgenerator qhelpgenerator-qt5' not found
#
# Each looked like its own missing host package, and the first two were duly
# installed. THE TRAP is that chasing them one at a time is the wrong shape:
# the third needs Qt, so the chain does not terminate anywhere reasonable, and
# none of the three was ever really required.
#
# meson.build asks for them like this:
#
# doxygen = find_program('doxygen', required: get_option('doxygen_docs'))
# ducktype = find_program('ducktype', required: get_option('ducktype_docs'))
# yelpbuild = find_program('yelp-build', required: get_option('ducktype_docs'))
# qhelpgen = find_program('qhelpgenerator', ..., required: get_option('qt_help'))
#
# and all three options are features declared with no value -- 'auto'.
# arch-meson passes --auto-features enabled, which promotes every one of them
# to a hard requirement. On Arch that is harmless: the doc tools are in the
# build chroot. Here it turned optional documentation into three consecutive
# build failures. Same mechanism as util-linux's translated man pages and
# expat's docbook converter; dbus is just the clearest instance, because it
# repeated three times in one afternoon.
#
# xml_docs is deliberately LEFT ALONE: it generates the man pages through
# xsltproc, which is installed, and man pages are worth having.
#
# The dbus-docs sub-package goes with the options. Its `_pick docs
# usr/share/doc` is a mv, so with nothing generated it would fail the way
# every other _pick on an absent path in this port has failed -- after a
# successful compile, at the last step, naming a directory rather than a
# reason. Deferred, not architectural: TODO.md records the way back.
set -euo pipefail
python3 - <<'PY'
import io, re
s = io.open("PKGBUILD", encoding="utf-8").read()
# (a) pin the three features off, beside the options already being pinned
anchor = " -D apparmor=disabled\n"
assert s.count(anchor) == 1, "dbus: meson_options array not in the expected form"
s = s.replace(anchor,
anchor +
" -D doxygen_docs=disabled\n"
" -D ducktype_docs=disabled\n"
" -D qt_help=disabled\n", 1)
# (b) the sub-package that would have nothing to package
m = re.search(r"pkgname=\((.*?)\)\n", s, re.S)
assert m, "dbus: pkgname array not found"
names = [n for n in re.findall(r"[A-Za-z0-9._+-]+", m.group(1))]
assert "dbus-docs" in names, "dbus: dbus-docs not in pkgname"
kept = [n for n in names if n != "dbus-docs"]
s = s[:m.start()] + "pkgname=(\n" + "".join(" %s\n" % n for n in kept) + ")\n" + s[m.end():]
# (c) and the _pick that feeds it
old = ' _pick docs "$pkgdir"/usr/share/doc\n'
assert s.count(old) == 1, "dbus: docs _pick not in the expected form"
s = s.replace(old, "", 1)
io.open("PKGBUILD", "w", encoding="utf-8").write(s)
PY
for o in doxygen_docs ducktype_docs qt_help; do
grep -qF -- "-D $o=disabled" PKGBUILD || {
echo "dbus: $o not pinned off" >&2; exit 1; }
done
grep -qF '_pick docs' PKGBUILD && {
echo "dbus: docs _pick survived and would fail on an absent path" >&2; exit 1; }
python3 - <<'PY'
import io, re, sys
s = io.open("PKGBUILD", encoding="utf-8").read()
m = re.search(r"pkgname=\((.*?)\)\n", s, re.S)
if not m:
sys.exit("dbus: pkgname array unreadable after patching")
names = re.findall(r"[A-Za-z0-9._+-]+", m.group(1))
if "dbus-docs" in names:
sys.exit("dbus: dbus-docs still declared in pkgname")
print("dbus: pkgname -> %s" % " ".join(names))
PY
echo "dbus: doc generators pinned off (--auto-features made three of them fatal)"

36
patches/pkgbuild/gettext.sh Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
# gettext: --without-selinux. The sixth package to pick up the host's
# libselinux, and the first one nothing warned about.
#
# $ readelf -d usr/lib/libgettextlib-1.0.so | grep NEEDED
# (NEEDED) Shared library: [libselinux.so.1]
#
# Nothing failed. gettext built, passed, and went into the repository with a
# library linked against something Arch has no package for -- the artefact
# audit is the only thing that found it, exactly as it found coreutils,
# findutils, sed and tar before it.
#
# WHY IT CAME BACK. Those four were fixed with per-package hooks, and the
# reasoning at the time was that the same gnulib probe "catches findutils, sed
# and tar" -- a list of the packages then in the build. gettext was not in the
# port yet. It arrived with the dependency closure, uses gnulib too, probes
# selinux/selinux.h unconditionally, and found Ubuntu's libselinux1-dev
# sitting where it always was.
#
# So this is the fifth copy of one three-line fix, and that is the real
# finding: the per-package hook does not scale to a probe that any gnulib
# package can trip. The alternative -- exporting
# ac_cv_header_selinux_selinux_h=no once in build_package -- was considered
# and rejected when coreutils was fixed, on the grounds that a global cache
# override is invisible at the point where it acts. That trade has now been
# paid for five times. TODO.md records it as a decision to revisit rather than
# leaving the next person to rediscover the arithmetic.
#
# Arch's gettext links no libselinux, so this converges with Arch.
set -euo pipefail
grep -q '^ --without-included-libunistring$' PKGBUILD || {
echo "gettext: configure block not in the expected form" >&2; exit 1; }
sed -i 's|^\( --without-included-libunistring\)$|\1 \\\n --without-selinux|' PKGBUILD
[ "$(grep -c -- '--without-selinux' PKGBUILD)" = 1 ] || {
echo "gettext: --without-selinux not inserted exactly once" >&2; exit 1; }
echo "gettext: --without-selinux (gnulib found the host's libselinux)"

30
patches/pkgbuild/krb5.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# krb5: --with-system-ss, and Ubuntu ships no ss headers.
#
# checking whether system ss package works... configure: error: cannot run
# test program
#
# THE TRAP is that message. "cannot run test program" reads like a broken
# cross-compile or a missing loader -- the kind of thing that sends you into
# config.log looking for a link failure on this architecture. It is neither.
# The subsystem library (ss, from e2fsprogs) has no development package on
# Ubuntu at all: libss2 is runtime-only, libss-dev does not exist, and
# e2fslibs-dev has no candidate. There is no ss.h to compile against, so the
# test program cannot be built, let alone run.
#
# Note what IS present, so the two are not confused: --with-system-et works,
# because comerr-dev ships /usr/include/et/com_err.h. Only ss is missing.
#
# krb5 carries its own copy of the subsystem library and uses it when not told
# otherwise, so dropping the flag costs nothing but a slightly larger binary.
# Building e2fsprogs to supply one header would be the other answer; it is not
# worth a package in the closure, and TODO.md records the choice.
set -euo pipefail
grep -qE '^\s+--with-system-ss \\$' PKGBUILD || {
echo "krb5: --with-system-ss line not in the expected form" >&2; exit 1; }
[ "$(grep -c -- '--with-system-ss' PKGBUILD)" = 1 ] || {
echo "krb5: expected exactly one --with-system-ss" >&2; exit 1; }
sed -i '/^\s\+--with-system-ss \\$/d' PKGBUILD
grep -q -- '--with-system-ss' PKGBUILD && {
echo "krb5: --with-system-ss survived" >&2; exit 1; }
echo "krb5: uses its bundled ss (Ubuntu has no ss development package)"

84
patches/pkgbuild/pinentry.sh Executable file
View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
# pinentry: three graphical front ends, on a machine with no display stack.
#
# configure: error:
# ***
# *** Qt6 (Qt6Core, Qt6Gui, Qt6Widgets) is required.
# ***
#
# Arch builds every front end pinentry has -- tty, curses, emacs, gnome3 and
# qt -- and declares qt6-base, gcr, kguiaddons and kwindowsystem as
# makedepends. None of that exists on this build host, and none of it belongs
# in a bootstrap: pacman needs a passphrase prompt on a terminal, nothing more.
#
# THE HONEST FRAMING. This is not architectural -- Qt runs on s390x perfectly
# well. It is deferred, and TODO.md says so: a desktop on Z would want these
# back, and the way back is to build qt6-base and the KDE pieces first. What
# is not defensible is dragging a GUI toolkit into the closure of a package
# manager's passphrase prompt.
#
# THE SECOND PLACE, and then a THIRD. libsecret and glib2 are in the top-level
# depends= and exist only to serve the front ends being switched off. --nodeps
# means makepkg never checks them, so leaving them would ship a pinentry that
# cannot install -- the same trap as gcc-libs/libhwasan, make/guile and
# gnutls/leancrypto.
#
# The third place is package(), which APPENDS more:
#
# depends+=( libglib-2.0.so libncursesw.so libsecret-1.so )
#
# The first version of this hook fixed the array at the top of the file, ran
# its guard against that array, passed, and shipped the append untouched. The
# guard checked the place I was thinking about rather than every place the
# name occurs -- which is exactly the mistake the note above warns against.
# Only libncursesw.so survives; the other two name libraries this build no
# longer links.
#
# tty, curses and fallback-curses stay, and so does emacs: it is a protocol
# over a pipe, not a toolkit, and it links nothing extra.
set -euo pipefail
python3 - <<'PY'
import io
s = io.open("PKGBUILD", encoding="utf-8").read()
for flag in ("--enable-pinentry-gnome3", "--enable-pinentry-qt", "--enable-libsecret"):
off = flag.replace("--enable-", "--disable-", 1)
assert s.count(flag) == 1, "pinentry: expected exactly one %s" % flag
s = s.replace(flag, off, 1)
old = " 'glibc' 'ncurses' 'libassuan' 'libsecret' 'glib2'\n"
assert s.count(old) == 1, "pinentry: depends block not in the expected form"
s = s.replace(old, " 'glibc' 'ncurses' 'libassuan'\n", 1)
# package() appends three sonames; two of them are gone with the front ends.
old = " depends+=(\n libglib-2.0.so\n libncursesw.so\n libsecret-1.so\n )\n"
assert s.count(old) == 1, "pinentry: package() depends+= not in the expected form"
s = s.replace(old, " depends+=(\n libncursesw.so\n )\n", 1)
# package() renames the GTK binary, which is no longer built.
old = ' # The -gtk backend has been built to be used with GTK3.\n mv "${pkgdir}/usr/bin/pinentry-gtk"{-2,}\n'
assert s.count(old) == 1, "pinentry: gtk rename not in the expected form"
s = s.replace(old, "", 1)
io.open("PKGBUILD", "w", encoding="utf-8").write(s)
PY
for f in gnome3 qt libsecret; do
grep -q -- "--disable-pinentry-$f\|--disable-$f" PKGBUILD || {
echo "pinentry: $f front end still enabled" >&2; exit 1; }
done
grep -qE "^ 'glibc' 'ncurses' 'libassuan'$" PKGBUILD || {
echo "pinentry: depends not reduced" >&2; exit 1; }
# EVERY place the names occur -- but as they are actually WRITTEN, not as bare
# substrings. The first version of this loop grepped for `libsecret` and
# matched the `--disable-libsecret` it had just inserted, so a correct patch
# reported failure. Third guard in this port to check something adjacent to
# what it meant; the lesson is that a guard needs the same care as the edit.
for n in "'libsecret'" "'glib2'" "libglib-2.0.so" "libsecret-1.so" \
"--enable-libsecret" "--enable-pinentry-qt" "--enable-pinentry-gnome3" \
'pinentry-gtk"{-2,}'; do
grep -qF -- "$n" PKGBUILD && {
echo "pinentry: $n still present" >&2; exit 1; }
done
grep -q -- "--enable-pinentry-curses" PKGBUILD || {
echo "pinentry: curses front end lost -- nothing would prompt" >&2; exit 1; }
echo "pinentry: tty/curses/emacs only (no Qt, GNOME or libsecret on this host)"

45
patches/pkgbuild/sqlite.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# sqlite: the tcl extension installs where Ubuntu's tclConfig.sh says, not
# where Arch's does.
#
# mv: cannot stat '<pkgdir>/usr/lib/tcl8.6/sqlite*': No such file or directory
#
# THE TRAP is that tcl was found. The log says so, plainly:
#
# Checking for tclsh8.6.../usr/bin/tclsh8.6
# Using tclConfig.sh: /usr/lib/s390x-linux-gnu/tclConfig.sh
#
# so "install tcl-dev" looks like it worked, and the failure looks like a
# separate problem. It is the same problem one layer down: tclConfig.sh
# carries TCL_PACKAGE_PATH, and Ubuntu's says share/tcltk while Arch's says
# lib. Measured in the pkgdir:
#
# usr/share/tcltk/tcl8.6/sqlite3.53.4/libsqlite3.53.4.so
#
# and usr/lib/tcl8.6 does not exist at all. This is the multiarch libdir theme
# again -- a path the BUILD HOST decides -- but one layer out from the library
# directory itself, which is why --libdir does not reach it.
#
# The tree is relocated to Arch's path before package_sqlite() splits it out,
# so the upstream mv keeps working unchanged. Same shape as the lib64 merge in
# gcc.sh: move the tree, do not rewrite every path that names it.
set -euo pipefail
python3 - <<'PY'
import io
s = io.open("PKGBUILD", encoding="utf-8").read()
anchor = " # split out tcl extension\n"
assert s.count(anchor) == 1, "sqlite: expected one tcl split block"
fix = """ # Ubuntu's tclConfig.sh puts the tcl package tree under share/tcltk;
# Arch's puts it under lib. Move it before the split below reads it.
if [ -d "$pkgdir"/usr/share/tcltk/tcl8.6 ]; then
mkdir -p "$pkgdir"/usr/lib
mv "$pkgdir"/usr/share/tcltk/tcl8.6 "$pkgdir"/usr/lib/tcl8.6
rmdir --ignore-fail-on-non-empty "$pkgdir"/usr/share/tcltk
fi
"""
s = s.replace(anchor, fix + anchor, 1)
io.open("PKGBUILD", "w", encoding="utf-8").write(s)
PY
grep -q "tclConfig.sh puts the tcl package tree" PKGBUILD || {
echo "sqlite: tcl relocation not inserted" >&2; exit 1; }
echo "sqlite: tcl extension relocated from share/tcltk to lib (host TCL_PACKAGE_PATH)"