diff --git a/patches/pkgbuild/brotli.sh b/patches/pkgbuild/brotli.sh new file mode 100755 index 0000000..86a4426 --- /dev/null +++ b/patches/pkgbuild/brotli.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# brotli: two Debian defaults, and both of them ship silently. +# +# 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. +set -euo pipefail + +# 1. The C library, and the one that actually matters. +# +# brotli's PKGBUILD calls plain `cmake`, not the arch-cmake stand-in, so it +# never sees the -DCMAKE_INSTALL_LIBDIR=lib that stand-in passes. CMake's +# GNUInstallDirs then picks its branch by sniffing the host: no +# /etc/arch-release, /etc/debian_version present, so +# +# CMAKE_INSTALL_LIBDIR = lib/s390x-linux-gnu +# +# Measured on a DESTDIR install of a real build: libbrotlicommon.so.1, +# libbrotlidec.so.1, libbrotlienc.so.1 and their .pc files all under +# /usr/lib/s390x-linux-gnu. brotli is in stage 1 only because the chroot said +# openssl and curl need libbrotli*.so -- and the filesystem package's +# /etc/ld.so.conf reads only ld.so.conf.d, so the target would never find +# them. A no-op in stage 2, where CMake takes the /etc/arch-release branch. +python3 - <<'PY' +import io +s = io.open("PKGBUILD", encoding="utf-8").read() +old = ' -DCMAKE_INSTALL_PREFIX=/usr \\\n' +assert s.count(old) == 1, "expected exactly one cmake configure, found %d" % s.count(old) +s = s.replace(old, old + ' -DCMAKE_INSTALL_LIBDIR=lib \\\n', 1) +io.open("PKGBUILD", "w", encoding="utf-8").write(s) +PY +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. +# +# package_python-brotli() runs +# +# python -m installer --destdir="$pkgdir" dist/*.whl +# +# 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. +# +# 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. +# +# `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' +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) +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" diff --git a/patches/pkgbuild/pam.sh b/patches/pkgbuild/pam.sh new file mode 100755 index 0000000..6c872f2 --- /dev/null +++ b/patches/pkgbuild/pam.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# pam: the PDF removal assumes fop, which the Ubuntu host does not have. +# +# rm: cannot remove +# '.../pkg/pam/usr/share/doc/Linux-PAM/*.pdf': No such file or directory +# +# package() ends on +# +# # remove unreproducible pdf files +# rm "${pkgdir}"/usr/share/doc/Linux-PAM/*.pdf +# +# and that line only works because Arch lists fop in makedepends. fop is a +# Java renderer; putting a JVM on the build host to produce three PDFs that +# the very next line deletes buys nothing, so the host stays without it. +# +# Without fop meson drops every PDF target in silence -- doc/adg, doc/mwg and +# doc/sag each guard theirs with `if prog_fop.found()`, and the configured +# build.ninja then holds zero .pdf rules. The DIRECTORY still exists, because +# the HTML and text manuals install beside them, so the glob is never +# expanded and rm is handed the literal string `*.pdf`. Same shape as the +# util-linux glob that started the libdir hunt: the message names a path that +# was never going to exist, and it aborts package() AFTER a successful +# compile -- so the build looks like it failed at the very last step for no +# reason at all. +# +# -f is the whole fix. With fop the PDFs are still removed; without it the +# empty glob stops being an error. +# +# Not a hook, but the other half of pam and worth finding here: libnsl and +# libtirpc are NOT optional on this host even though upstream marks NIS +# `auto`, because arch-meson passes --auto-features enabled, which promotes +# every auto feature to a hard requirement. Same mechanism turns pam's `docs` +# feature into five fatal xmlcatalog probes. All of it is host packages, and +# they are declared in install_host_deps. +set -euo pipefail +# Exactly one removal, and it must be the pdf one. Never a blanket rewrite: +# package() also runs `install` and `chmod +s` on paths under the same tree, +# and meson install itself writes there. +n=$(grep -cF 'rm "${pkgdir}"/usr/share/doc/Linux-PAM/' PKGBUILD || true) +[ "$n" = "1" ] || { echo "pam: expected 1 pdf removal, found $n" >&2; exit 1; } +sed -i 's|rm "${pkgdir}"/usr/share/doc/Linux-PAM/|rm -f "${pkgdir}"/usr/share/doc/Linux-PAM/|' PKGBUILD +grep -qF 'rm -f "${pkgdir}"/usr/share/doc/Linux-PAM/*.pdf' PKGBUILD || { + echo "pam: pdf removal not made tolerant" >&2; exit 1; } +echo "pam: pdf removal tolerates the empty glob (host has no fop)" diff --git a/patches/pkgbuild/systemd.sh b/patches/pkgbuild/systemd.sh new file mode 100755 index 0000000..1704324 --- /dev/null +++ b/patches/pkgbuild/systemd.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# systemd: two build options 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. +set -euo pipefail + +# 1. -Dbootloader=enabled: systemd-boot is EFI, and s390x has no EFI. +# +# The message you get is NOT about EFI: +# +# systemd/meson.build:1638:19: ERROR: python3 is missing modules: elftools +# +# because meson.build:1638 asks for pyelftools with +# `required : get_option('bootloader')`, and the PKGBUILD set that to enabled. +# +# THE TRAP is that this reads like a missing host package, and +# `apt install python3-pyelftools` looks like the fix. It is not. It buys four +# lines, and then meson.build:1642 says what is really wrong: +# +# ERROR: Feature bootloader cannot be enabled: unsupported EFI arch or +# EFI support is disabled +# +# (verified by planting a stub elftools module on PYTHONPATH; without it the +# EFI message is unreachable, which is why nobody ever sees it first.) +# +# meson.build:1627 maps a cpu family to an EFI machine type name -- aa64, arm, +# loongarch32/64, riscv32/64, x64, ia32 -- and s390x is not in that table, so +# efi_arch is ''. -Defi is still true; the architecture simply has no EFI. +# IBM Z boots from an IPL record, there is no ESP for systemd-boot to write +# into, and no configuration invents one. +# +# The cost is the systemd-boot artefacts. bootctl itself is still built and +# installed -- checked in the install plan -- package_systemd() names neither, +# and the arch.conf/loader.conf/splash-arch.bmp it ships are `install`ed from +# $srcdir rather than built. Packaging is untouched. Disabling also drops the +# pyelftools requirement in the same line. +sed -i 's/-Dbootloader=enabled/-Dbootloader=disabled/' PKGBUILD +grep -q -- '-Dbootloader=disabled' PKGBUILD || { + echo "systemd: bootloader option not rewritten" >&2; exit 1; } +echo "systemd: bootloader disabled (s390x has no EFI machine type)" + +# 2. -Dvmlinux-h=provided: the file Arch points at is an Arch file. +# +# src/bpf/meson.build: error('Path to provided vmlinux.h does not exist.') +# +# Arch's linux-headers ships /usr/src/linux/vmlinux.h, the path hard-coded in +# the PKGBUILD. Ubuntu's linux-headers-* ship no vmlinux.h anywhere. This +# failure only appears AFTER libbpf and clang are installed, because the whole +# block is skipped while BPF_FRAMEWORK is off -- fixing libbpf uncovers it. +# +# systemd's own fallback dumps the header out of the running kernel's BTF, +# which is what 'generated' selects: +# +# bpftool btf dump file /sys/kernel/btf/vmlinux format c +# +# Checked on this host: exit 0, 122471 lines, and enum lsm_integrity_type is +# there at line 15931, so restrict-fsaccess.bpf.c builds rather than being +# quietly dropped. +# +# THE TRAP is that 'auto' would NOT have done this for us. The auto branch +# generates only when `host_machine.cpu_family() in ['x86_64', 'aarch64']`; on +# s390x it falls through to "neither provided nor generated" and silently +# drops the BPF programs that need the header. s390x has to say it out loud. +# +# Note the asymmetry this buys: on the 'provided' branch systemd probes the +# header with cc.compiles() before deciding what to build. On 'generated' it +# sets have_lsm_integrity_type = true outright. So the build now depends on +# the BTF of the kernel RUNNING when it starts -- fine on 6.17.0-41, and on an +# older kernel it turns into a compile error with no fallback. +# +# -Dvmlinux-h-path goes with it: 'generated' ignores the value, and leaving a +# path that resolves to nothing invites the next reader to "repair" it. +sed -i 's/-Dvmlinux-h=provided/-Dvmlinux-h=generated/' PKGBUILD +sed -i '/-Dvmlinux-h-path=/d' PKGBUILD +grep -q -- '-Dvmlinux-h=generated' PKGBUILD || { + echo "systemd: vmlinux.h still read from a provided path" >&2; exit 1; } +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" diff --git a/scripts/bootstrap-pacman.sh b/scripts/bootstrap-pacman.sh index eea14f9..6afd5c1 100755 --- a/scripts/bootstrap-pacman.sh +++ b/scripts/bootstrap-pacman.sh @@ -37,6 +37,30 @@ install_host_deps() { # systemtap-sdt-dev and gmp/mpfr/mpc for glibc and gcc, autopoint and # gperf for coreutils, asciidoc for pacman, po4a for xz, gnat for gcc's # Ada front end, debuginfod and jansson for binutils. + # + # THE LIST WAS ALSO A LIE BY OMISSION. An audit of the six packages that + # follow found libreadline-dev, libncurses-dev, zlib1g-dev, python3-dev, + # doxygen, xsltproc, docbook-xsl, elinks and libcap2-bin already present + # on the build VM but installed BY HAND, never declared here. On this + # host they made the builds pass; on a fresh Ubuntu they would have + # failed at readline, at libxml2 and inside shadow's `make install`, for + # reasons that have nothing to do with s390x. They are named below now. + # The rest is per-package, and each group says which package needs it. + # + # gnupg imagemagick + fig2dev render doc/*.svg and doc/*.fig, which + # a git checkout must generate; librsvg2-bin guarantees the + # SVG coder even under --no-install-recommends. + # shadow itstool builds the translated man pages; libcap2-bin gives + # the bare `setcap` its install rule calls. + # util-linux asciidoctor -- Ruby, and NOT the `asciidoc` above, which + # is a different program added for pacman. Easy to mistake + # for coverage. + # pam libnsl/libtirpc for NIS, the DocBook 5 catalog, and elinks, + # which meson uses to dump the HTML manuals to text. + # brotli cmake, and the PEP 517 chain its python bindings build with. + # libxml2 ICU, readline, and the doc toolchain. + # systemd the widest surface of anything in stage 1; every one of + # these was checked against a real meson configure. sudo apt-get -o DPkg::Lock::Timeout=600 install -y -qq \ meson ninja-build pkg-config gettext \ libarchive-dev libcurl4-openssl-dev libgpgme-dev libssl-dev \ @@ -46,7 +70,19 @@ install_host_deps() { systemtap-sdt-dev asciidoc autopoint gperf help2man rsync \ libgmp-dev libmpfr-dev libmpc-dev python3-docutils \ libseccomp-dev libpcre2-dev \ - po4a gnat libdebuginfod-dev libjansson-dev + po4a gnat libdebuginfod-dev libjansson-dev \ + libreadline-dev libncurses-dev zlib1g-dev python3-dev \ + doxygen xsltproc docbook-xsl libcap2-bin \ + imagemagick fig2dev librsvg2-bin \ + itstool asciidoctor \ + libtirpc-dev libnsl-dev docbook5-xml docbook-xsl-ns elinks \ + cmake python3-build python3-installer python3-pkgconfig \ + python3-setuptools python3-wheel \ + libicu-dev \ + 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 install_host_shims } @@ -62,6 +98,39 @@ install_host_shims() { log "Host shims" local d="$HERE_DIR/devtools" sudo install -m755 "$d/arch-meson" "$d/arch-cmake" /usr/local/bin/ + # history.pc, which Ubuntu drops and Arch ships. + # + # GNU readline generates and installs BOTH readline.pc and history.pc; + # Debian and Ubuntu keep the first and delete the second, while the + # library, libhistory.so, is right there in libreadline-dev. libxml2 asks + # pkg-config for `history` and stops: + # + # libxml2/meson.build:353:18: ERROR: Dependency "history" not found + # + # THE TRAP: our own readline package in repo/s390x DOES ship a correct + # history.pc, so copying that one looks like the tidy answer. It is not. + # Its libdir is ${exec_prefix}/lib -- right for the target rootfs, wrong + # for a multiarch host where /usr/lib holds no libhistory. The .pc has to + # describe THIS host, so it is generated from the host's own readline.pc. + local multiarch version + multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH)" + version="$(pkg-config --modversion readline)" + sudo mkdir -p /usr/local/lib/pkgconfig + printf '%s\n' \ + "prefix=/usr" \ + "exec_prefix=\${prefix}" \ + "libdir=/usr/lib/$multiarch" \ + "includedir=\${prefix}/include" \ + "" \ + "Name: History" \ + "Description: GNU History library" \ + "Version: $version" \ + "Requires.private: tinfo" \ + "Libs: -L\${libdir} -lhistory" \ + "Cflags: -I\${includedir}" \ + | sudo tee /usr/local/lib/pkgconfig/history.pc > /dev/null + pkg-config --exists history || { + echo "history.pc still not visible to pkg-config" >&2; return 1; } } build_pacman() {