diff --git a/patches/pkgbuild/brotli.sh b/patches/pkgbuild/brotli.sh index 86a4426..ac9c239 100755 --- a/patches/pkgbuild/brotli.sh +++ b/patches/pkgbuild/brotli.sh @@ -1,10 +1,14 @@ #!/usr/bin/env bash -# brotli: two Debian defaults, and both of them ship silently. +# brotli: a Debian default that ships silently, and a sub-package that costs +# more than it is worth. # -# Neither produces an error. makepkg exits 0 both times and the repository -# gains a package whose files are in places nothing on the target will look. -# This is the case "an exit code proves nothing, only the artefact proves" -# was written for -- the symptom arrives months later, far from the cause. +# The first produces no error. makepkg exits 0 and the repository gains a +# package whose library sits where nothing on the target will look -- the case +# "an exit code proves nothing, only the artefact proves" was written for, +# where the symptom arrives months later, far from the cause. +# +# The second is a closure decision, not a defect, and it is measured rather +# than argued. set -euo pipefail # 1. The C library, and the one that actually matters. @@ -34,52 +38,40 @@ grep -q 'DCMAKE_INSTALL_LIBDIR=lib' PKGBUILD || { echo "brotli: libdir not pinned" >&2; exit 1; } echo "brotli: cmake libdir pinned to lib (host GNUInstallDirs says multiarch)" -# 2. The python wheel, and an honest account of what this buys. +# 2. python-brotli: dropped, because it costs the entire python closure. # -# package_python-brotli() runs +# THE MEASUREMENT THAT DECIDED IT. pacman resolver named 70 missing +# dependencies. Excluding this one sub-package from the ledger took that to +# 47 and removed `python` outright -- with libffi, mpdecimal and gdbm behind +# it. Three packages of closure, for one module. # -# python -m installer --destdir="$pkgdir" dist/*.whl +# And the module could not work anyway. The wheel is built by the host python +# 3.13 and is ABI-tagged cpython-313; Arch ships 3.14. No destination makes it +# importable on the target, so stage 2 is what has to produce a real one. +# TODO.md carries that entry already. # -# and `installer` asks sysconfig where a wheel belongs. Debian and Ubuntu -# patch sysconfig to prefer the "posix_local" scheme, so the answer here is -# /usr/local/lib/python3.13/dist-packages. Verified by running the real wheel -# through the real installer into a scratch pkgdir. +# Note what is NOT the reason. The wheel also lands in +# /usr/local/lib/python3.13/dist-packages, because Debian patches sysconfig to +# prefer its posix_local scheme, and an Arch package may not ship /usr/local. +# That was worth a relocation while the sub-package existed; with the +# sub-package gone there is nothing to relocate, and keeping the relocation +# would be editing a package() function makepkg never calls. # -# BE CLEAR ABOUT WHAT THIS FIXES. It does NOT make the module importable on -# Arch. Arch ships python 3.14 and this wheel is cp313 with an ABI-tagged -# _brotli.cpython-313-*.so, so no destination makes a stage-1 wheel work on -# the target; stage 2 is what produces a usable one. What the move buys is -# that an Arch package must not ship files under /usr/local, which Arch -# reserves for the administrator. That is the whole justification, and -# writing a bigger one here would mislead the next reader. +# build() still builds the wheel -- it is one function for all three +# sub-packages -- and the result is simply not packaged. Cheap, and it keeps +# this hook to one line of real change. # -# `installer --prefix=/usr` does NOT help: posix_local is literally -# "{base}/local/lib/pythonX.Y/dist-packages", so overriding base with /usr -# still lands in /usr/local. The SCHEME would have to change, and the only -# supported way to change it is to be a different interpreter. -# -# Guarded on the directory, not the host name: under an Arch python the -# directory is absent and the block does nothing. Only purelib moves -- the -# wheel declares py_modules=["brotli"] plus the extension, no scripts and no -# data files, so nothing else of it ever leaves /usr/local. -python3 - <<'PY' +# brotli-testdata stays: nothing depends on it, it costs no closure, and +# removing it would be churn. +python3 - <<'PYB' import io s = io.open("PKGBUILD", encoding="utf-8").read() -old = ' python -m installer --destdir="$pkgdir" dist/*.whl\n' -assert old in s, "installer invocation not found" -new = old + """ # Ubuntu's python installs to /usr/local/lib/pythonX.Y/dist-packages - # (sysconfig's posix_local scheme). An Arch package may not ship /usr/local. - local _pyver=$(python -c 'import sys; print("%d.%d" % sys.version_info[:2])') - if [ -d "$pkgdir/usr/local/lib/python$_pyver/dist-packages" ]; then - install -dm 755 "$pkgdir/usr/lib/python$_pyver" - mv "$pkgdir/usr/local/lib/python$_pyver/dist-packages" \\ - "$pkgdir/usr/lib/python$_pyver/site-packages" - rm -rf "$pkgdir/usr/local" - fi -""" -s = s.replace(old, new, 1) +old = "pkgname=('brotli' 'python-brotli' 'brotli-testdata')" +assert s.count(old) == 1, "brotli: expected exactly one pkgname line" +s = s.replace(old, "pkgname=('brotli' 'brotli-testdata')", 1) io.open("PKGBUILD", "w", encoding="utf-8").write(s) -PY -grep -q "posix_local scheme" PKGBUILD || { - echo "brotli: wheel relocation not inserted" >&2; exit 1; } -echo "brotli: python wheel moved out of /usr/local" +PYB +grep -q "^pkgname=('brotli' 'brotli-testdata')$" PKGBUILD || { + echo "brotli: python-brotli not removed from pkgname" >&2; exit 1; } +grep -q "python-brotli" PKGBUILD && echo "brotli: python-brotli remains only in its own package function (never called)" +echo "brotli: python-brotli dropped (pulled the whole python closure)" diff --git a/patches/pkgbuild/libseccomp.sh b/patches/pkgbuild/libseccomp.sh new file mode 100755 index 0000000..80e5860 --- /dev/null +++ b/patches/pkgbuild/libseccomp.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# libseccomp: the python sub-package ships into /usr/local. +# +# usr/local/lib/python3.13/dist-packages/... +# +# Debian patches sysconfig to prefer its posix_local scheme, so +# `python -m installer` puts the wheel under /usr/local -- a tree an Arch +# package may not ship at all. Nothing fails: makepkg exits 0 and the audit +# is what notices. +# +# THE SAME DECISION AS python-brotli, for the same measured reason. Nothing in +# the repository depends on python-libseccomp -- checked across all 135 +# packages -- and keeping it would pull `python` back into the closure, with +# libffi, mpdecimal and gdbm behind it. That closure was deliberately removed +# when python-brotli was dropped; re-admitting it for a binding no package +# asks for would undo the measurement. +# +# And the module would not work anyway: the wheel is built by the host's +# python 3.13 and ABI-tagged cpython-313, while Arch ships 3.14. Stage 2 is +# what produces a usable one. +# +# build() still builds the wheel -- it is one function for both sub-packages +# -- and the result is simply not packaged. package_python-libseccomp() stays +# in the file, uncalled, like package_libquadmath() in gcc. +set -euo pipefail +python3 - <<'PY' +import io +s = io.open("PKGBUILD", encoding="utf-8").read() +old = "pkgname=(libseccomp python-libseccomp)" +assert s.count(old) == 1, "libseccomp: pkgname line not in the expected form" +s = s.replace(old, "pkgname=(libseccomp)", 1) +io.open("PKGBUILD", "w", encoding="utf-8").write(s) +PY +grep -q '^pkgname=(libseccomp)$' PKGBUILD || { + echo "libseccomp: python sub-package not dropped" >&2; exit 1; } +echo "libseccomp: python-libseccomp dropped (would re-admit the python closure)" diff --git a/patches/pkgbuild/systemd.sh b/patches/pkgbuild/systemd.sh index 17e4bec..a1d31f1 100755 --- a/patches/pkgbuild/systemd.sh +++ b/patches/pkgbuild/systemd.sh @@ -1,11 +1,15 @@ #!/usr/bin/env bash -# systemd: two build options Arch can hold and s390x cannot. +# systemd: four defaults Arch can hold and s390x cannot. # -# Neither is a missing host package -- both are hard errors raised by meson -# because the PKGBUILD ASKS for something that does not exist on this -# architecture, so no amount of apt-get makes them go away. The libraries -# systemd wants (libbpf, clang, libfdisk, libkmod, ...) ARE host packages and -# belong in install_host_deps, not here. +# None of them is a missing host package. Two are hard errors raised by meson +# because the PKGBUILD ASKS for something this architecture does not have; two +# are defaults computed from the BUILD MACHINE rather than the target. No +# amount of apt-get fixes any of them. The libraries systemd wants (libbpf, +# clang, libfdisk, libkmod, ...) ARE host packages and belong in +# install_host_deps, not here. +# +# Three of the four fail with a message that names something else entirely. +# Each section says which, because that is the part worth reading twice. set -euo pipefail # 1. -Dbootloader=enabled: systemd-boot is EFI, and s390x has no EFI. @@ -126,3 +130,110 @@ sed -i 's/^\( *\)-Dcompat-sysv-interfaces=false/\1-Dsplit-bin=false\n\1-Dcompat- 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)" + +# 4. -Dukify=auto: the tool cannot run on this architecture at all. +# +# ukify assembles a Unified Kernel Image, and a UKI is a PE binary -- ukify.py +# imports pefile at module level and drives it directly (pefile.PE, +# SectionStructure, IMAGE_SCN_*). It is EFI tooling, so the same reasoning as +# section 1 applies. But it is stronger than "pointless on Z", and the source +# says so out loud: +# +# EFI_ARCH_MAP = { +# 'x86_64': ['x64','ia32'], 'i[3456]86': ['ia32'], 'aarch64': ['aa64'], +# 'armv[45678]*l': ['arm'], 'loongarch32': ..., 'riscv64': ... +# } +# +# def guess_efi_arch() -> str: +# ... +# else: +# raise ValueError(f'Unsupported architecture {arch}') +# +# s390x is not in that table, so ukify RAISES on this machine. Shipping it +# would put a program in the repository that cannot start. +# +# THE TRAP is that ukify does not announce itself as EFI-only anywhere the +# build stops. meson_options.txt:560 declares it a plain feature with no +# value -- 'auto' -- and meson.build:1659 is get_option('ukify').allowed(), +# which is true for 'auto'. So it is on by default, and what it broke was +# nowhere near ukify: +# +# FAILED: src/boot/test-hwids-section.c +# ModuleNotFoundError: No module named 'pefile' +# +# That looked like a missing host package, and `apt install python3-pefile` +# made it build. It was the wrong fix: the target at src/boot/meson.build:31 +# is gated on ENABLE_UKIFY, not on ENABLE_BOOTLOADER, which is why disabling +# systemd-boot did not reach it. Disabling ukify does, and python3-pefile is +# then unnecessary -- nothing else in the tree needs it, since the only other +# importer, tools/check-efi-alignment.py, is reached from +# src/boot/meson.build:495, far below the ENABLE_BOOTLOADER subdir_done() +# guard. +# +# systemd-tests goes with it, for a different reason. It is a test suite -- +# stage 1 runs --nocheck -- and it is the sole reason five python packages +# (colorama, packaging, pexpect, psutil, pytest) would enter the closure. +# Deferred, not architectural: TODO.md records it. +# +# The two subpackages leave in three places, and the pkgname array is the +# awkward one: its LAST entry carries the closing paren, so deleting a line +# would delete the ')' with it. The array is rebuilt rather than edited. +python3 - <<'PY' +import io, re +s = io.open("PKGBUILD", encoding="utf-8").read() + +# (a) the option, next to the other EFI one so they read together +anchor = " -Dbootloader=disabled\n" +assert s.count(anchor) == 1, "expected section 1 to have set bootloader=disabled" +s = s.replace(anchor, anchor + " -Dukify=disabled\n", 1) + +# (b) the pkgname array, rebuilt to keep its syntax intact +m = re.search(r"pkgname=\((.*?)\)\n", s, re.S) +assert m, "pkgname array not found" +names = re.findall(r"'([^']+)'", m.group(1)) +drop = {"systemd-ukify", "systemd-tests"} +assert drop <= set(names), "expected both subpackages in pkgname, found %s" % names +kept = [n for n in names if n not in drop] +s = s[:m.start()] + "pkgname=(" + ("\n" + " " * 9).join("'%s'" % n for n in kept) + ")\n" + s[m.end():] + +# (c) package_systemd() moves four ukify paths out. With ukify disabled none +# of them exists, and mv fails -- after a successful compile, which is the +# expensive way to find out. +blk = re.search( + r"\n # ukify shipped in separate package\n" + r"(?:.*\n)*?" + r" mv \"\$pkgdir\"/usr/lib/kernel/install\.d/60-ukify\.install systemd-ukify/install\.d\n", + s) +assert blk, "ukify mv block not found" +s = s[:blk.start()] + "\n" + s[blk.end():] + +# (d) an optdepends on a package we no longer produce +s = re.sub(r"^.*'systemd-ukify: .*\n", "", s, flags=re.M) + +io.open("PKGBUILD", "w", encoding="utf-8").write(s) +PY +# The guard checks what MATTERS, which is not "no mention of ukify remains". +# package_systemd-ukify() still sits in the file and still names four paths -- +# harmless, because makepkg never calls a function whose name is absent from +# pkgname, exactly as with package_libquadmath() in gcc.sh. Counting mentions +# instead of checking the two real conditions made this hook exit 1 on a +# correctly patched PKGBUILD, and build_package reads that as a failed +# package: systemd would have been skipped entirely, with every edit applied. +grep -q -- '-Dukify=disabled' PKGBUILD || { + echo "systemd: ukify still enabled" >&2; exit 1; } +grep -q '# ukify shipped in separate package' PKGBUILD && { + echo "systemd: package_systemd() still moves ukify paths that cannot exist" >&2 + exit 1; } +python3 - <<'PYGUARD' +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("systemd: pkgname array unreadable after patching") +names = re.findall(r"'([^']+)'", m.group(1)) +left = sorted({"systemd-ukify", "systemd-tests"} & set(names)) +if left: + sys.exit("systemd: still declared in pkgname: %s" % left) +print("systemd: pkgname -> %s" % " ".join(names)) +PYGUARD +echo "systemd: ukify disabled (guess_efi_arch raises on s390x), tests dropped"