archlinux-s390x/scripts/bootstrap-pacman.sh
Mathieu Benoit 549b5650f5 [ADD] driver: refuse to build below 8 GiB free
A full disk does not say so. gcc's bootstrap reported

  genattrtab: cannot close file tmp-attrtab.cc: No space left on device

seventeen thousand lines into its log, behind two hundred lines of make
recursion. Everything visible pointed at gcc, and the first grep for
"error:" matched cpp_error() -- a function name in gcc's own source, the
documented trap of grepping a whole file instead of the right part.

df would have said it in one line, before the forty minutes.

This host is shared, and the free margin is not stable, so it is checked per
package rather than assumed once. 8 GiB clears gcc, the largest build here;
smaller packages never trip it. The message names the reclaim command,
because the answer is not obvious either: the src trees are regenerable,
makepkg -C wipes them anyway.

--- FR ---

Un disque plein ne le dit pas. L'amorçage de gcc a signalé

  genattrtab: cannot close file tmp-attrtab.cc: No space left on device

dix-sept mille lignes plus bas dans son journal, derrière deux cents lignes
de récursion make. Tout ce qui était visible accusait gcc, et le premier
grep sur « error: » est tombé sur cpp_error() — un nom de fonction dans les
sources de gcc, précisément le piège documenté qui consiste à grep un
fichier entier plutôt que le bon endroit.

df l'aurait dit en une ligne, avant les quarante minutes.

Cet hôte est partagé et la marge libre n'est pas stable : la vérification se
fait donc par paquet, sans rien supposer. 8 GiB suffisent à gcc, le plus
gros build ici ; les petits paquets ne la déclencheront jamais. Le message
nomme la commande de récupération, car la réponse n'est pas évidente non
plus : les arbres src se régénèrent, makepkg -C les efface de toute façon.

Assisted-by: Claude Opus 5
2026-08-17 03:14:40 -04:00

334 lines
16 KiB
Bash
Executable file

#!/usr/bin/env bash
# Bootstrap the Arch packaging toolchain (pacman, makepkg, repo-add) on an
# s390x host, then build official Arch PKGBUILDs for s390x.
#
# WHY THIS IS THE KEYSTONE
#
# The README lists "pacman, bash and GNU coreutils are not yet ported" as three
# missing pieces. They are not three tasks -- pacman is the only one that
# matters, because pacman's source tree also ships makepkg and repo-add. Once
# those three run, every remaining package stops being hand-work and becomes
# `makepkg` on the upstream PKGBUILD.
#
# NO CROSS-COMPILATION IS NEEDED HERE. This runs on native s390x hardware, so
# the whole userspace builds at full speed with the host toolchain. That drops
# the z/VM round-trip the other scripts need.
set -euo pipefail
HERE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PATCH_DIR="${PATCH_DIR:-$HERE_DIR/../patches/pkgbuild}"
WORK="${WORK:-$HOME/work/arch-s390x}"
REPO="${REPO:-$WORK/repo/s390x}"
PACMAN_GIT="https://gitlab.archlinux.org/pacman/pacman.git"
PKG_GIT_BASE="https://gitlab.archlinux.org/archlinux/packaging/packages"
log() { printf '\n== %s ==\n' "$*"; }
install_host_deps() {
log "Host build dependencies"
# Two distinct groups, and confusing them costs hours.
#
# makepkg's OWN requirements: bsdtar and fakeroot. Without them it fails
# deep inside extraction with a bare "bsdtar: command not found".
#
# The PKGBUILDs' makedepends: --nodeps tells pacman not to check them, so
# every one must be satisfied from the Ubuntu host by hand. Each name on
# the last three lines was added because a build stopped on it --
# 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 \
libarchive-tools fakeroot \
build-essential autoconf automake libtool m4 patch texinfo bison flex \
zstd xz-utils bzip2 \
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 \
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 \
python3-pefile
install_host_shims
}
# The parts of "make the host look enough like Arch" that apt cannot express.
#
# Called from install_host_deps AND from build-stage1.sh, deliberately. The
# stand-ins are only consulted through /usr/local/bin, so editing the copy in
# this repository changes NOTHING until it is installed -- and a stale
# /usr/local copy is invisible: the build succeeds and ships the wrong paths.
# Re-installing them on every stage-1 run makes the repository the source of
# truth in fact, not just in intent. Both operations are idempotent.
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() {
log "pacman + makepkg + repo-add"
mkdir -p "$WORK"
[ -d "$WORK/pacman" ] || git clone --depth 1 "$PACMAN_GIT" "$WORK/pacman"
cd "$WORK/pacman" || return 1
# /usr/local, never /usr: this host is Ubuntu and /usr belongs to dpkg.
# makepkg resolves its own libraries from the configured prefix, so the
# prefix has to be real -- running it from the build tree fails with
# "/usr/share/makepkg/*.sh: No such file or directory".
rm -rf build
meson setup build --prefix=/usr/local --buildtype=release \
-Ddoc=disabled -Ddoxygen=disabled -Di18n=false
ninja -C build -j"$(nproc)"
sudo ninja -C build install
}
configure_makepkg() {
log "makepkg configuration"
# CARCH is already detected as s390x by meson; CHOST must stay the
# auto-detected triplet -- configure scripts are matched against it, and
# inventing "s390x-pc-linux-gnu" breaks them.
sudo sed -i "s|^#\?MAKEFLAGS=.*|MAKEFLAGS=\"-j$(nproc)\"|" /etc/makepkg.conf
# pacman refuses to initialise alpm without its database directory. The
# error is only a warning during packaging, but it hides real ones.
sudo mkdir -p /var/lib/pacman
# Arch's numeric group ids must EXIST on the build host.
#
# The filesystem package creates directories with `install -g 11`, and
# GNU coreutils rejects a gid that resolves to nothing:
#
# install: invalid group: '11'
#
# gid 11 is `ftp` on Arch; Ubuntu leaves it free. This is the circular
# corner of any bootstrap -- the package that DEFINES /etc/group needs
# groups that do not exist yet -- and the way out is to give the build
# host the target's id map rather than to work around the check.
#
# Only the ids Arch uses and Ubuntu lacks are created, and only when
# missing, so an already-correct host is left alone.
if ! getent group 11 > /dev/null 2>&1; then
sudo groupadd -g 11 ftp
echo "created group ftp (gid 11), required by the filesystem package"
fi
# CHOST must be the CANONICAL triplet, not the Debian-style one.
#
# gcc -dumpmachine reports s390x-linux-gnu here, but config.sub
# canonicalises that to s390x-ibm-linux-gnu, and GCC builds its tree
# under the canonical name. Arch PKGBUILDs assume the canonical form --
# theirs is x86_64-pc-linux-gnu, with the vendor field present -- so
# gcc's own PKGBUILD looked for $CHOST/libstdc++-v3/doc and found
# nothing:
#
# make: *** s390x-linux-gnu/libstdc++-v3/doc: No such file or directory
#
# while the build had created s390x-ibm-linux-gnu/libstdc++-v3/doc.
sudo sed -i 's|^CHOST=.*|CHOST="s390x-ibm-linux-gnu"|' /etc/makepkg.conf
grep -E '^(CARCH|CHOST|MAKEFLAGS)=' /etc/makepkg.conf
}
# build_package <name> -- fetch the official PKGBUILD and build it for s390x.
#
# --ignorearch: upstream PKGBUILDs carry arch=(x86_64) and nothing else.
# --skipchecksums: GitLab regenerates .patch URLs, so their checksums drift
# from what the PKGBUILD recorded. Release tarballs still validate; only
# the generated patches are skipped.
# --nocheck: stage-1 test suites run against the HOST libraries, not Arch's,
# so their verdict says nothing about the port. acl failed its check() on a
# perfectly sound build, and the suites cost hours. Stage 2 runs them.
build_package() {
local name="$1"
log "Building $name"
# Free space, checked before the build rather than discovered inside it.
#
# A full disk does not say so. gcc's stage-2 bootstrap reported
#
# genattrtab: cannot close file tmp-attrtab.cc: No space left on device
#
# seventeen thousand lines into its log, behind two hundred lines of make
# recursion -- and the first grep for "error:" matched cpp_error(), a
# function name in gcc's own source. Every symptom pointed at gcc.
#
# This host is shared with running VMs whose disk images grow, so the
# margin is not stable and cannot be assumed. 8 GiB clears gcc, the
# largest build here; a smaller package will simply never trip it.
local free_mb
free_mb=$(df -Pm "$WORK" | awk 'NR==2 {print $4}')
if [ "${free_mb:-0}" -lt 8192 ]; then
echo "only ${free_mb} MiB free under $WORK; need 8192" >&2
echo "reclaim with: rm -rf $WORK/pkg/*/src (makepkg -C re-extracts)" >&2
return 1
fi
mkdir -p "$WORK/pkg"
# Both guards are load-bearing, and their absence cost a whole run.
#
# gitlab.archlinux.org answers a 404 by asking for credentials, so a
# package that does not exist fails as
#
# fatal: could not read Username for 'https://gitlab.archlinux.org'
#
# GIT_TERMINAL_PROMPT=0 turns that into an immediate error instead of a
# process waiting on a terminal that is not there.
#
# Then the cd. With neither guarded, a failed clone left makepkg running
# in whatever directory the PREVIOUS package used -- it rebuilt that
# package, wrote the output into THIS package's log, and copied the
# artefact back into the repository. log-libselinux.txt was 119 KB of
# util-linux. The lesson is the repository's oldest one, one level up:
# an exit code proves nothing, and neither does a log file's NAME.
if [ ! -d "$WORK/pkg/$name" ]; then
GIT_TERMINAL_PROMPT=0 \
git clone --depth 1 "$PKG_GIT_BASE/$name.git" "$WORK/pkg/$name" || {
rm -rf "$WORK/pkg/$name"
echo "clone failed: $PKG_GIT_BASE/$name.git" >&2
return 1
}
fi
cd "$WORK/pkg/$name" || return 1
# Port patches. Upstream PKGBUILDs are written for x86_64 and some carry
# flags no other architecture accepts -- glibc's --enable-sframe is the
# first. They are applied here, one hook per package, so each change is
# visible, reviewable and survives a re-clone, rather than being an edit
# someone once made by hand in a working copy.
local hook="$PATCH_DIR/$name.sh"
if [ -f "$hook" ]; then
git checkout -- PKGBUILD 2>/dev/null || true
bash "$hook" || return 1
fi
rm -f ./*.pkg.tar.*
# Explicit `|| return 1`. Relying on `set -e` here does NOT work: callers
# invoke build_package inside `if`, which disables set -e for the whole
# function body. A failing makepkg then fell through to repo-add, whose
# success became the function's exit status -- and a run reported
# "51 built, 0 failed" while glibc, gcc, coreutils and pacman had all
# failed. Measured: the repository held 23 files, not a hundred.
# -C wipes $srcdir first. Without it a re-run inherits the previous
# attempt's tree, and prepare() fails on work it already did:
# patch: ... already exists! Skipping patch.
# 1 out of 1 hunk ignored
# 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.
local produced=()
shopt -s nullglob
produced=(./*.pkg.tar.*)
shopt -u nullglob
if [ "${#produced[@]}" -eq 0 ]; then
echo "no package produced for $name" >&2
return 1
fi
mkdir -p "$REPO"
cp -f "${produced[@]}" "$REPO/" || return 1
# Only the new packages. Globbing the whole repository made repo-add
# re-index everything on every call: quadratic, and it buried the real
# lines under warnings about entries that already existed.
local names=() f
for f in "${produced[@]}"; do names+=("$(basename "$f")"); done
( cd "$REPO" && repo-add core.db.tar.gz "${names[@]}" ) || return 1
}
main() {
install_host_deps
build_pacman
configure_makepkg
for p in "$@"; do build_package "$p"; done
log "Done"
command -v pacman makepkg repo-add
}
# Only run when executed, never when sourced: build-stage1.sh reuses
# build_package and must not re-run the whole bootstrap to get it.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi