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.