From 9a696af012d57a0a9d473df8ae36adb8135f4e68 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 17 Aug 2026 02:37:46 -0400 Subject: [PATCH] [FIX] host defaults: apt was deciding what got built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --auto-features enabled turns every auto feature into a requirement, so installing a tool switches on code that never compiled here. Three of the fifty host packages did exactly that, and only one failed for the reason it named. expat had built clean the day before. asciidoc pulled docbook-utils in as an automatic dependency; it owns docbook2man, the third name in expat's find_program list, so docs defaulted ON twenty-nine hours before anything rebuilt. That tool is the SGML pipeline: on DocBook XML it writes nothing and still exits 0, and the mv it feeds is what reported the failure. Arch ships no xmlwf.1 either, so OFF is parity. systemd's split-bin probes the BUILD host's /usr/sbin. Ubuntu's is a real directory, so six binaries went where package() does not look -- and arch-meson's --sbindir is ignored, systemd computes its own. util-linux needed no option: poman-translate.sh greps po4a for the English "Discard" and this host answers "Rejet de". The locale belongs to the host, so it is pinned once on the makepkg line. --- FR --- --auto-features enabled fait de chaque fonction « auto » une exigence : installer un outil active donc du code jamais compilé ici. Trois des cinquante paquets hôte l'ont fait, et un seul a échoué pour la raison qu'il annonçait. expat compilait proprement la veille. asciidoc avait tiré docbook-utils en dépendance automatique ; il fournit docbook2man, troisième nom de la liste find_program d'expat, et la doc est passée à ON vingt-neuf heures avant toute reconstruction. Cet outil est le pipeline SGML : sur du DocBook XML il n'écrit rien et sort quand même 0, et le mv qu'il alimente est ce qui a signalé la panne. Arch ne livre pas xmlwf.1 non plus : OFF, c'est la parité. Le split-bin de systemd sonde le /usr/sbin de la machine de BUILD. Celui d'Ubuntu est un vrai répertoire, donc six binaires sont partis là où package() ne regarde pas — et le --sbindir d'arch-meson est ignoré, systemd calcule le sien. util-linux n'avait besoin d'aucune option : poman-translate.sh cherche le « Discard » anglais de po4a, et cet hôte répond « Rejet de ». La locale appartient à l'hôte : elle est épinglée une fois, sur la ligne makepkg. Assisted-by: Claude Opus 5 --- patches/pkgbuild/expat.sh | 74 +++++++++++++++++++++++++++++++++---- patches/pkgbuild/systemd.sh | 45 ++++++++++++++++++++++ scripts/bootstrap-pacman.sh | 25 ++++++++++++- 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/patches/pkgbuild/expat.sh b/patches/pkgbuild/expat.sh index 7f45f91..d5a760f 100755 --- a/patches/pkgbuild/expat.sh +++ b/patches/pkgbuild/expat.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# expat: bare cmake picks up the Debian multiarch libdir. +# expat: two defaults decided by the build HOST rather than by the PKGBUILD. +set -euo pipefail + +# 1. The library directory. # # The PKGBUILD calls cmake directly -- NOT arch-cmake -- so the devtools # stand-in that already passes -DCMAKE_INSTALL_LIBDIR=lib never runs. The @@ -21,13 +24,8 @@ # dependency of cmake, python, gdb and fontconfig, so this does not fail # loudly here -- it fails later, in whatever links against -lexpat. # -# The fix is to state the libdir the stand-in would have stated. Passing it on -# the command line makes it a cache entry BEFORE GNUInstallDirs runs, and -# GNUInstallDirs only fills in defaults for variables that are still unset. -# -# One configure call, so exactly one insertion: assert the count rather than -# rewriting every -D. -set -euo pipefail +# Passing it on the command line makes it a cache entry BEFORE GNUInstallDirs +# runs, and GNUInstallDirs only fills in defaults for variables still unset. python3 - <<'PY' import io s = io.open("PKGBUILD", encoding="utf-8").read() @@ -41,3 +39,63 @@ PY [ "$(grep -c 'CMAKE_INSTALL_LIBDIR=lib' PKGBUILD)" = 1 ] || { echo "expat: libdir not pinned exactly once" >&2; exit 1; } echo "expat: CMAKE_INSTALL_LIBDIR=lib (bare cmake bypasses arch-cmake)" + +# 2. EXPAT_BUILD_DOCS: pin it OFF, because its default reads the host's PATH. +# +# THE TRAP: this option has no fixed default. CMakeLists.txt line 74 does +# +# find_program(DOCBOOK_TO_MAN NAMES docbook2x-man db2x_docbook2man \ +# docbook2man docbook-to-man) +# +# and turns docs ON if ANY of those four is found. Nothing in the PKGBUILD +# asks for docs -- makedepends is (cmake git). So whether expat builds a man +# page is decided by whatever apt happens to have dragged onto the VM. +# +# Not hypothetical, and the timeline is the whole lesson. expat built clean on +# 2026-08-15 21:25 and its package carries no usr/share/man at all. At +# 2026-08-16 00:23:31 apt installed docbook-utils -- nobody asked for it; it +# arrived as an automatic dependency of asciidoc, which install_host_deps +# needs for pacman. docbook-utils owns /usr/bin/docbook2man, the THIRD name in +# that list. From that moment find_program succeeded and a target that had +# never run in this port switched itself on. expat was simply not rebuilt for +# another twenty-nine hours, so the breakage waited. +# +# THE ERROR NAMES THE WRONG CULPRIT, twice over: +# +# mv: cannot stat 'XMLWF.1': No such file or directory +# +# Nothing is wrong with the mv, and XMLWF.1 is the correct name -- +# doc/xmlwf.xml declares XMLWF, and every +# docbook man backend derives the filename from it. The real failure is one +# command earlier and silent: docbook-utils' docbook2man is a two-line shell +# script, `jw -f docbook -b man "$@"`, i.e. the SGML pipeline. doc/xmlwf.xml +# is DocBook XML 4.2, whose ISO entity sets use `&#xNNNN;` hex character +# references that SGML forbids. onsgmls emits `"X2288" is not a function name` +# two hundred times, gives up -- and jw still EXITS 0, having written only +# manpage.refs and manpage.links. Those two hundred lines sit ABOVE the +# failure in the log and read like harmless validation noise. +# +# Upstream knows this tool is unusable and rejects it BY NAME in the autotools +# path: configure.ac:454 greps `${DOCBOOK_TO_MAN} --help` for "sgmlbase" and +# aborts with "was found to work with SGML rather than XML". Our +# /usr/bin/docbook2man matches that grep. The CMake path has no such guard. +# +# WHY OFF RATHER THAN INSTALLING docbook2x: because OFF is what Arch ships. +# Arch's core expat 2.8.3 has 29 files and no usr/share/man, which proves +# Arch's builder has none of those four programs -- any one would have flipped +# the default. Pinning OFF reproduces Arch exactly; installing docbook2x would +# make s390x diverge from x86_64 and add one more host tool that silently +# decides what gets built. +python3 - <<'PY' +import io +s = io.open("PKGBUILD", encoding="utf-8").read() +assert "EXPAT_BUILD_DOCS" not in s, "expat: docs already pinned, hook ran twice" +anchor = " -D CMAKE_INSTALL_LIBDIR=lib\n" +n = s.count(anchor) +assert n == 1, "expat: expected 1 libdir line, found %d" % n +s = s.replace(anchor, anchor + " -D EXPAT_BUILD_DOCS=OFF\n", 1) +io.open("PKGBUILD", "w", encoding="utf-8").write(s) +PY +[ "$(grep -c 'EXPAT_BUILD_DOCS=OFF' PKGBUILD)" = 1 ] || { + echo "expat: docs not pinned exactly once" >&2; exit 1; } +echo "expat: EXPAT_BUILD_DOCS=OFF (default follows host PATH, not the PKGBUILD)" diff --git a/patches/pkgbuild/systemd.sh b/patches/pkgbuild/systemd.sh index 1704324..17e4bec 100755 --- a/patches/pkgbuild/systemd.sh +++ b/patches/pkgbuild/systemd.sh @@ -81,3 +81,48 @@ grep -q -- '-Dvmlinux-h=generated' PKGBUILD || { grep -q -- '-Dvmlinux-h-path' PKGBUILD && { echo "systemd: stale vmlinux-h-path left in place" >&2; exit 1; } echo "systemd: vmlinux.h generated from /sys/kernel/btf/vmlinux" + +# 3. -Dsplit-bin=auto: the question is answered by the HOST, not the target. +# +# systemd/meson.build:123 +# sbindir = prefixdir / (split_bin ? 'sbin' : 'bin') +# +# split-bin is a combo defaulting to 'auto', and 'auto' probes whether +# /usr/sbin on the BUILD MACHINE is a symlink to bin. On Arch it is, so +# split_bin is false and everything lands in /usr/bin. Here /usr/sbin is a +# real directory, and meson-log.txt says so: +# +# split bin-sbin : true +# +# THE TRAP is that arch-meson ALREADY passes --sbindir bin, and +# `meson configure` dutifully reports sbindir = bin. It is ignored: systemd +# computes its own sbindir from split_bin and never reads meson's builtin. The +# option that looks like the fix is not the fix, and the one that is mentions +# neither sbin nor a directory in its name. +# +# Nothing fails in build(). It fails much later, in package_systemd(), on the +# first line that names a path: +# +# rm: cannot remove '/usr/bin/halt': No such file or directory +# +# because halt, init, poweroff, reboot, shutdown and resolvconf all went to +# /usr/sbin -- along with mount.ddi, mount.mstack and mount.storage, which the +# PKGBUILD never mentions at all. The rm is only the first victim. Suppress it +# and the package ships /usr/sbin as a DIRECTORY, which collides on the target +# with the ["usr/sbin"]="bin" SYMLINK our own filesystem package declares. +# +# Measured on a configured copy: with split-bin=false there is no /usr/sbin in +# the install tree at all, and all six paths are where package() looks. +# +# Same species as arch-meson's --libdir -- an Ubuntu default standing in for +# an Arch one -- but it cannot live in arch-meson: split-bin is systemd's own +# option, not a meson builtin. --auto-features does not reach it either; it is +# a combo, not a feature. +test "$(grep -c -- '-Dcompat-sysv-interfaces=false' PKGBUILD)" -eq 1 || { + echo "systemd: expected exactly one _meson_options array" >&2; exit 1; } +test "$(grep -c -- '-Dsplit-bin' PKGBUILD)" -eq 0 || { + echo "systemd: PKGBUILD already sets split-bin, re-read it" >&2; exit 1; } +sed -i 's/^\( *\)-Dcompat-sysv-interfaces=false/\1-Dsplit-bin=false\n\1-Dcompat-sysv-interfaces=false/' PKGBUILD +grep -q -- '-Dsplit-bin=false' PKGBUILD || { + echo "systemd: split-bin still auto (would install into /usr/sbin)" >&2; exit 1; } +echo "systemd: split-bin=false (Ubuntu's /usr/sbin is not Arch's)" diff --git a/scripts/bootstrap-pacman.sh b/scripts/bootstrap-pacman.sh index 6afd5c1..ed8f7f1 100755 --- a/scripts/bootstrap-pacman.sh +++ b/scripts/bootstrap-pacman.sh @@ -82,7 +82,8 @@ install_host_deps() { libbpf-dev clang libapparmor-dev libfdisk-dev libkmod-dev libdw-dev \ libpwquality-dev libxkbcommon-dev libdbus-1-dev libqrencode-dev \ libfido2-dev libtss2-dev libmicrohttpd-dev libaudit-dev \ - libcryptsetup-dev libgcrypt20-dev libgnutls28-dev libpam0g-dev + libcryptsetup-dev libgcrypt20-dev libgnutls28-dev libpam0g-dev \ + python3-pefile install_host_shims } @@ -254,6 +255,28 @@ build_package() { # mkdir: build-curl-compat: File exists # grep, gnupg, pacman and curl all failed this way -- not on the # port, but on my own driver re-entering a dirty directory. + # LC_ALL=C.UTF-8, because build systems parse their tools' output. + # + # This host runs LANG=fr_FR.UTF-8. util-linux's poman-translate.sh reads + # po4a's report and skips what it says it discarded: + # + # DISCARDED_TRANSLATION=$(echo "$line" | awk '/Discard/ {print $2;}') + # + # -- an English word matched against a gettext-translated message. In + # French po4a prints "Rejet de ar/..." instead, so the skip list came out + # empty, asciidoctor was handed 852 files po4a had never written, and the + # build died on the first. Note $2 is "de" in French: even a locale-aware + # pattern would take the wrong field. + # + # It went unseen for a second reason -- the script captures po4a in + # `output=$(...)`, so none of those 852 lines reach the log at all. + # + # The locale is a property of THIS host, not of any one package, and any + # build that greps English tool output is exposed. So it is pinned here, + # once, rather than in a hook per package. C.UTF-8 and not C: the Arabic + # and Ukrainian pages must stay valid UTF-8. Arch's own build chroot runs + # in this locale, so this makes us match it rather than diverge. + LC_ALL=C.UTF-8 \ makepkg --nodeps --ignorearch --skippgpcheck --skipchecksums --nocheck \ -C -f || return 1 # An exit code is not proof. Only the artefact is.