archlinux-s390x/scripts/build-stage2.sh

441 lines
18 KiB
Bash
Raw Normal View History

[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
#!/usr/bin/env bash
# Stage 2: rebuild the repository inside the repository.
#
# WHAT STAGE 2 IS FOR
#
# Every package stage 1 produced was compiled against UBUNTU's libraries. That
# is not a defect -- Arch's glibc needs an Arch gcc which needs an Arch glibc,
# so the first pass has nowhere else to start -- but it leaves host artefacts
# baked in. scripts/test-chroot.sh names nine of them precisely: binaries that
# ask for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more, at the
# host's soname versions, while the repository ships Arch's. Stage 2 dissolves
# all nine by rebuilding each package against what the repository actually has.
#
# THE CONSTRAINT THAT SHAPES THIS SCRIPT
#
# pacman cannot run inside the stage-1 rootfs. libalpm was linked against the
# host's gpgme, so the binary is there and does not start:
#
# pacman: error while loading shared libraries: libgpgme.so.11
#
# So the rootfs is populated from OUTSIDE, with the host's pacman and --root,
# the way scripts/test-chroot.sh does. The chroot is used only to BUILD. That
# is not a workaround, it is the order the problem has: stage 2's own output
# is the first pacman that will run on the target.
#
# makepkg, by contrast, is a shell script, and it works.
set -uo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK="${WORK:-$HOME/work/arch-s390x}"
REPO1="${REPO1:-$WORK/repo/s390x}"
REPO2="${REPO2:-$WORK/repo2/s390x}"
ROOT="${ROOT:-$WORK/rootfs-stage2}"
CONF="$WORK/pacman-stage2.conf"
CACHE="$WORK/pacman-stage2.cache"
[ADD] stage 2: the rebuild loop, and git to feed it The loop applies each hook on the HOST -- /build is the same directory from both sides, so makepkg in the chroot reads the patched PKGBUILD and nothing is duplicated inside. It drops --nocheck: stage 1 skipped the test suites because they ran against the host's libraries, and here they test what was built. Each rebuilt package is installed into the chroot before the next one, with the host's pacman and --root, because the chroot's own pacman will not start until stage 2 has rebuilt it. Two hooks must NOT run there, and both for the same satisfying reason: the condition they work around does not exist in the chroot. libgcrypt.sh points at a host prefix holding our libgpg-error, which the chroot has installed properly. git.sh drops ZLIB_NG=1 because Ubuntu ships no zlib-ng headers, while our own zlib-ng package ships them. git is here because STAGE 2 needs it, not the repository: 51 of the 159 PKGBUILDs take their sources from git+https, and makepkg validates that clone even under --noextract. Nothing depends on git. Its three -- perl-error, perl-mailtools with perl-timedate, zlib-ng -- were read from the depends array rather than from my own tool, which had reported `zsh` as a dependency of git. It is not; the tool's regex was catching a neighbouring array. --- FR --- La boucle applique chaque crochet sur l'HÔTE — /build est le même répertoire des deux côtés, donc makepkg dans le chroot lit le PKGBUILD corrigé et rien n'est dupliqué dedans. Elle abandonne --nocheck : l'étage 1 sautait les suites de tests parce qu'elles s'exécutaient contre les bibliothèques de l'hôte ; ici elles éprouvent ce qui a été bâti. Chaque paquet reconstruit est installé dans le chroot avant le suivant, avec le pacman de l'hôte et --root, celui du chroot ne démarrant pas avant que l'étage 2 ne l'ait reconstruit. Deux crochets ne doivent PAS y tourner, et pour la même raison satisfaisante : la condition qu'ils contournent n'existe pas dans le chroot. libgcrypt.sh pointe sur un préfixe hôte contenant notre libgpg-error, que le chroot a installé correctement. git.sh retire ZLIB_NG=1 parce qu'Ubuntu ne livre pas les en-têtes zlib-ng, alors que notre propre paquet zlib-ng les livre. git est là parce que l'ÉTAGE 2 en a besoin, pas le dépôt : 51 des 159 PKGBUILD prennent leurs sources en git+https, et makepkg valide ce clone même sous --noextract. Rien ne dépend de git. Ses trois dépendances — perl-error, perl-mailtools avec perl-timedate, zlib-ng — ont été lues dans le tableau depends plutôt que dans mon propre outil, qui annonçait `zsh` comme dépendance de git. Elle ne l'est pas : la regex de l'outil attrapait un tableau voisin. Assisted-by: Claude Opus 5
2026-08-19 08:56:41 -04:00
CONF2="$WORK/pacman-stage2-both.conf"
STATE2="$WORK/stage2.state"
PATCH_DIR="${PATCH_DIR:-$HERE/../patches/pkgbuild}"
[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
BUILDER="${BUILDER:-$(id -un)}"
BUILD_UID="$(id -u)"
BUILD_GID="$(id -g)"
# The chroot's contents. Two groups, and the second is the one the dependency
# resolver could never have told us about: nothing in the repository DEPENDS on
# bison or fakeroot, they are simply what a build needs to happen.
CHROOT_PKGS=(
# A system that reaches a shell and can read a package.
filesystem glibc bash coreutils sed grep gawk findutils file which
tar gzip xz bzip2 zstd libarchive diffutils patch
# The toolchain.
gcc binutils make m4 autoconf automake libtool pkgconf
bison flex texinfo groff gettext
# makepkg itself, and the one thing it cannot do without.
pacman fakeroot
# libxcrypt-compat, for a reason no declaration expresses. perl declares
# `libxcrypt` and `libcrypt.so`, both satisfied by libxcrypt, which ships
# libcrypt.so.2. But perl's BINARY was linked on the host against Ubuntu's
# libcrypt.so.1, so it does not start:
#
# /usr/bin/perl: error while loading shared libraries: libcrypt.so.1
#
# The repository does ship that soname -- in libxcrypt-compat, a separate
# sub-package -- so this is neither a missing package nor a soname the
# audit should have flagged. It is a third thing: the dependency
# declarations cannot pull it in, because they name the unversioned soname
# that the newer library also provides. Listed explicitly, because nothing
# will deduce it.
libxcrypt-compat
[ADD] stage 2: the rebuild loop, and git to feed it The loop applies each hook on the HOST -- /build is the same directory from both sides, so makepkg in the chroot reads the patched PKGBUILD and nothing is duplicated inside. It drops --nocheck: stage 1 skipped the test suites because they ran against the host's libraries, and here they test what was built. Each rebuilt package is installed into the chroot before the next one, with the host's pacman and --root, because the chroot's own pacman will not start until stage 2 has rebuilt it. Two hooks must NOT run there, and both for the same satisfying reason: the condition they work around does not exist in the chroot. libgcrypt.sh points at a host prefix holding our libgpg-error, which the chroot has installed properly. git.sh drops ZLIB_NG=1 because Ubuntu ships no zlib-ng headers, while our own zlib-ng package ships them. git is here because STAGE 2 needs it, not the repository: 51 of the 159 PKGBUILDs take their sources from git+https, and makepkg validates that clone even under --noextract. Nothing depends on git. Its three -- perl-error, perl-mailtools with perl-timedate, zlib-ng -- were read from the depends array rather than from my own tool, which had reported `zsh` as a dependency of git. It is not; the tool's regex was catching a neighbouring array. --- FR --- La boucle applique chaque crochet sur l'HÔTE — /build est le même répertoire des deux côtés, donc makepkg dans le chroot lit le PKGBUILD corrigé et rien n'est dupliqué dedans. Elle abandonne --nocheck : l'étage 1 sautait les suites de tests parce qu'elles s'exécutaient contre les bibliothèques de l'hôte ; ici elles éprouvent ce qui a été bâti. Chaque paquet reconstruit est installé dans le chroot avant le suivant, avec le pacman de l'hôte et --root, celui du chroot ne démarrant pas avant que l'étage 2 ne l'ait reconstruit. Deux crochets ne doivent PAS y tourner, et pour la même raison satisfaisante : la condition qu'ils contournent n'existe pas dans le chroot. libgcrypt.sh pointe sur un préfixe hôte contenant notre libgpg-error, que le chroot a installé correctement. git.sh retire ZLIB_NG=1 parce qu'Ubuntu ne livre pas les en-têtes zlib-ng, alors que notre propre paquet zlib-ng les livre. git est là parce que l'ÉTAGE 2 en a besoin, pas le dépôt : 51 des 159 PKGBUILD prennent leurs sources en git+https, et makepkg valide ce clone même sous --noextract. Rien ne dépend de git. Ses trois dépendances — perl-error, perl-mailtools avec perl-timedate, zlib-ng — ont été lues dans le tableau depends plutôt que dans mon propre outil, qui annonçait `zsh` comme dépendance de git. Elle ne l'est pas : la regex de l'outil attrapait un tableau voisin. Assisted-by: Claude Opus 5
2026-08-19 08:56:41 -04:00
# git: 51 PKGBUILDs use git sources, and makepkg validates the clone even
# under --noextract.
git
[ADD] the build systems stage 2 rebuilds with Ten of the 159 PKGBUILDs call arch-meson and four call cmake, so a chroot without them could rebuild most of the repository and then stop. Neither is a dependency of anything in the repository, which is why no closure round ever named them -- the same shape as fakeroot and bison, one layer up. python returns with meson, and that is not the earlier decision being reversed. Dropping python at stage 1 was about what the REPOSITORY must supply: it was wanted only by two wheels Arch's own python could not load. This is about what the BUILD ENVIRONMENT must contain, and meson is written in Python. Different question, different answer. python needed two fixes of its own. Its xvfb loop is in build() AND in check(); stage 1 never ran the second because of --nocheck, but stage 2 drops --nocheck on purpose, so it would have spun there too. And Python 3.13 removed the vendored libmpdec, so --with-system-libmpdec is the only way to build and needs the host headers -- reported nine hundred lines in as a missing make rule for a file that used to be vendored. cmake wanted rhash, then jsoncpp, then cppdap, one at a time. That is a queue, and the rule in install_host_deps says a queue is a feature to disable. Not applied here, deliberately: each exists as an Ubuntu package, so the queue ends. The rule is for queues that do not, like dbus reaching a documentation tool that needed Qt. Its Qt GUI is dropped -- a dialog box on a headless mainframe. --- FR --- Dix des 159 PKGBUILD appellent arch-meson et quatre appellent cmake : un chroot sans eux pourrait reconstruire l'essentiel du dépôt puis s'arrêter. Aucun des deux n'est une dépendance de quoi que ce soit dans le dépôt, ce qui explique qu'aucun tour de fermeture ne les ait nommés — même forme que fakeroot et bison, une couche plus haut. python revient avec meson, et ce n'est pas un revirement. L'écarter à l'étage 1 portait sur ce que le DÉPÔT doit fournir : il n'était voulu que par deux roues que le python d'Arch ne pouvait pas charger. Ici il s'agit de ce que l'ENVIRONNEMENT DE BUILD doit contenir, et meson est écrit en Python. Autre question, autre réponse. python a demandé deux correctifs propres. Sa boucle xvfb est dans build() ET dans check() ; l'étage 1 n'a jamais exécuté la seconde grâce à --nocheck, mais l'étage 2 l'abandonne exprès — elle y aurait tourné aussi. Et Python 3.13 a retiré le libmpdec embarqué : --with-system-libmpdec est la seule voie et réclame les en-têtes de l'hôte, signalé neuf cents lignes plus loin comme une règle make manquante pour un fichier autrefois embarqué. cmake a réclamé rhash, puis jsoncpp, puis cppdap, un par un. C'est une file, et la règle d'install_host_deps dit qu'une file est une fonctionnalité à désactiver. Non appliquée ici, délibérément : chacun existe en paquet Ubuntu, donc la file se termine. La règle vise celles qui ne terminent pas, comme dbus atteignant un outil de documentation qui exigeait Qt. Son interface Qt est retirée — une boîte de dialogue sur un mainframe sans écran. Assisted-by: Claude Opus 5
2026-08-19 19:53:46 -04:00
# meson and cmake, with the python they are written in. Ten PKGBUILDs
# call arch-meson and four call cmake, so without these stage 2 could
# rebuild most of the repository and then stop.
python meson ninja cmake
[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
)
log() { printf '\n== %s ==\n' "$*"; }
die() { printf 'stage2: %s\n' "$*" >&2; exit 1; }
require_space() {
local free_mb
free_mb=$(df -Pm "$WORK" | awk 'NR==2 {print $4}')
[ "${free_mb:-0}" -ge 8192 ] || die "only ${free_mb} MiB free under $WORK; need 8192"
}
make_rootfs() {
log "Populating the stage-2 rootfs from stage 1"
cat > "$CONF" <<EOF
[options]
Architecture = s390x
SigLevel = Never
[core]
Server = file://$REPO1
EOF
sudo rm -rf "$ROOT" "$CACHE"
sudo mkdir -p "$ROOT/var/lib/pacman" "$CACHE"
sudo pacman --root "$ROOT" --config "$CONF" --cachedir "$CACHE" \
--noconfirm -Sy "${CHROOT_PKGS[@]}" > "$WORK/stage2-install.txt" 2>&1 \
|| { tail -20 "$WORK/stage2-install.txt" >&2; die "populate failed"; }
printf ' %s packages installed\n' "$(sudo ls "$ROOT/var/lib/pacman/local" | wc -l)"
}
configure_chroot() {
log "Configuring the chroot"
# CARCH and CHOST, for the same reason they had to be set on the host --
# except here the wrong value arrives from OUR OWN pacman package, which
# ships Arch's /etc/makepkg.conf verbatim:
#
# CARCH="x86_64"
# CHOST="x86_64-pc-linux-gnu"
#
# A stage-2 build with those would configure every source for x86_64 on an
# s390x machine. CHOST must be the canonical triplet, not the Debian one:
# config.sub turns s390x-linux-gnu into s390x-ibm-linux-gnu and GCC builds
# its tree under the canonical name, which is what broke gcc's own
# packaging on the host.
sudo sed -i 's|^CARCH=.*|CARCH="s390x"|; s|^CHOST=.*|CHOST="s390x-ibm-linux-gnu"|' \
"$ROOT/etc/makepkg.conf"
sudo sed -i "s|^#\?MAKEFLAGS=.*|MAKEFLAGS=\"-j$(nproc)\"|" "$ROOT/etc/makepkg.conf"
# !debug and !lto, matching what stage 1 used. Arch's defaults enable both;
# turning them on here would change what is being compared between the two
# stages, and comparing them is the whole point.
sudo sed -i 's|^OPTIONS=.*|OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge !debug !lto)|' \
"$ROOT/etc/makepkg.conf"
[FIX] stage 2: x86_64 compiler flags, and a .pc naming nothing Two failures three packages apart, both from the same place: our pacman package ships Arch's configuration verbatim, and Arch's is for x86_64. libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler `cc ... -march=x86-64 -mtune=generic ...` configure_chroot fixed CARCH, CHOST, MAKEFLAGS and OPTIONS and left CFLAGS alone. They are emptied rather than translated, because that is what stage 1 used -- the host's makepkg.conf has no CFLAGS line at all -- and 179 working packages are the evidence. s390x tuning is a deliberate later choice. Emptied by APPENDING, not commenting. The first attempt put a # in front of each assignment, and CFLAGS spans several lines: commenting the first left the continuations active and the quote unbalanced, so makepkg would not start. Then readline. Its .pc says `Requires.private: termcap` and this repository ships tinfo.pc; nothing provides termcap.pc. Nothing failed at build time -- a .pc is data, and pkg-config only follows Requires.private when a consumer asks. The first consumer to ask was libxml2, in the chroot, one stage and three packages from the cause. --- FR --- Deux échecs à trois paquets d'écart, de la même origine : notre paquet pacman livre la configuration d'Arch telle quelle, et celle d'Arch vise x86_64. libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler `cc ... -march=x86-64 -mtune=generic ...` configure_chroot corrigeait CARCH, CHOST, MAKEFLAGS et OPTIONS, et laissait CFLAGS. Ils sont vidés plutôt que traduits, car c'est ce qu'a utilisé l'étage 1 — le makepkg.conf de l'hôte n'a aucune ligne CFLAGS — et 179 paquets fonctionnels en sont la preuve. Le réglage pour s390x est un choix ultérieur délibéré. Vidés par AJOUT, non par commentaire. La première tentative mettait un # devant chaque affectation, et CFLAGS s'étend sur plusieurs lignes : commenter la première laissait les continuations actives et le guillemet déséquilibré, si bien que makepkg ne démarrait plus. Puis readline. Son .pc dit « Requires.private: termcap » alors que ce dépôt livre tinfo.pc ; personne ne fournit termcap.pc. Rien n'échouait à la compilation — un .pc est une donnée, et pkg-config ne suit Requires.private que si un consommateur le demande. Le premier à demander fut libxml2, dans le chroot, à une étape et trois paquets de la cause. Assisted-by: Claude Opus 5
2026-08-19 20:19:37 -04:00
# CFLAGS and friends, which arrive from the same place and are just as
# wrong. Our pacman package ships Arch's makepkg.conf verbatim, so the
# chroot inherits
#
# CFLAGS="-march=x86-64 -mtune=generic -O2 ... -fcf-protection ..."
#
# On s390x cc rejects that, and the failure surfaces nowhere near the
# cause: meson simply cannot start.
#
# libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler
# `cc -Wl,--version ... -march=x86-64 -mtune=generic ...`
#
# They are emptied rather than translated, because "no flags" is what stage
# 1 used -- the host's makepkg.conf carries no CFLAGS line at all -- and
# 179 working packages are the evidence that it builds. Choosing s390x
# tuning (-march=z13, and only the hardening flags that exist on Z) is a
# deliberate later step, not something to guess at inside a bootstrap.
# TODO.md records it.
#
# APPENDED, not commented. The first attempt here put a # in front of each
# assignment, and CFLAGS is a MULTI-LINE assignment: commenting its first
# line left the continuations active and the quote unbalanced, so makepkg
# would not start at all --
#
# /etc/makepkg.conf: line 109: unexpected EOF while looking for matching `"'
#
# An override at the end of the file needs no parsing of what came before:
# the last assignment is the one that counts.
sudo tee -a "$ROOT/etc/makepkg.conf" > /dev/null <<'EOC'
# --- stage 2: the shipped values are Arch's x86_64 ones ---
CFLAGS=""
CXXFLAGS=""
LDFLAGS=""
LTOFLAGS=""
RUSTFLAGS=""
DEBUG_CFLAGS=""
DEBUG_CXXFLAGS=""
EOC
[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
sudo grep -E '^(CARCH|CHOST|MAKEFLAGS|OPTIONS)=' "$ROOT/etc/makepkg.conf" | sed 's/^/ /'
[FIX] stage 2: x86_64 compiler flags, and a .pc naming nothing Two failures three packages apart, both from the same place: our pacman package ships Arch's configuration verbatim, and Arch's is for x86_64. libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler `cc ... -march=x86-64 -mtune=generic ...` configure_chroot fixed CARCH, CHOST, MAKEFLAGS and OPTIONS and left CFLAGS alone. They are emptied rather than translated, because that is what stage 1 used -- the host's makepkg.conf has no CFLAGS line at all -- and 179 working packages are the evidence. s390x tuning is a deliberate later choice. Emptied by APPENDING, not commenting. The first attempt put a # in front of each assignment, and CFLAGS spans several lines: commenting the first left the continuations active and the quote unbalanced, so makepkg would not start. Then readline. Its .pc says `Requires.private: termcap` and this repository ships tinfo.pc; nothing provides termcap.pc. Nothing failed at build time -- a .pc is data, and pkg-config only follows Requires.private when a consumer asks. The first consumer to ask was libxml2, in the chroot, one stage and three packages from the cause. --- FR --- Deux échecs à trois paquets d'écart, de la même origine : notre paquet pacman livre la configuration d'Arch telle quelle, et celle d'Arch vise x86_64. libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler `cc ... -march=x86-64 -mtune=generic ...` configure_chroot corrigeait CARCH, CHOST, MAKEFLAGS et OPTIONS, et laissait CFLAGS. Ils sont vidés plutôt que traduits, car c'est ce qu'a utilisé l'étage 1 — le makepkg.conf de l'hôte n'a aucune ligne CFLAGS — et 179 paquets fonctionnels en sont la preuve. Le réglage pour s390x est un choix ultérieur délibéré. Vidés par AJOUT, non par commentaire. La première tentative mettait un # devant chaque affectation, et CFLAGS s'étend sur plusieurs lignes : commenter la première laissait les continuations actives et le guillemet déséquilibré, si bien que makepkg ne démarrait plus. Puis readline. Son .pc dit « Requires.private: termcap » alors que ce dépôt livre tinfo.pc ; personne ne fournit termcap.pc. Rien n'échouait à la compilation — un .pc est une donnée, et pkg-config ne suit Requires.private que si un consommateur le demande. Le premier à demander fut libxml2, dans le chroot, à une étape et trois paquets de la cause. Assisted-by: Claude Opus 5
2026-08-19 20:19:37 -04:00
printf ' compiler flags: commented out (stage 1 used none)\n'
[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
# makepkg refuses to run as root, so the chroot needs the SAME uid as the
# user who owns the bind-mounted sources. A bind mount carries the host's
# numeric owner across, so a different uid inside would see them as
# somebody else's and fail to write $srcdir.
sudo tee -a "$ROOT/etc/passwd" > /dev/null <<EOF
$BUILDER:x:$BUILD_UID:$BUILD_GID::/build:/usr/bin/bash
EOF
sudo tee -a "$ROOT/etc/group" > /dev/null <<EOF
$BUILDER:x:$BUILD_GID:
EOF
sudo mkdir -p "$ROOT/build" "$ROOT/repo2"
sudo chown "$BUILD_UID:$BUILD_GID" "$ROOT/build" "$ROOT/repo2"
# The stage-1 repository, so makepkg's --nodeps builds can still read the
# packages if anything wants to, and so repo-add has somewhere to write.
mkdir -p "$REPO2"
}
mount_chroot() {
log "Mounting"
# /dev/pts is not optional: without it any build step that opens a pty --
# and gcc's testsuite driver does -- fails in a way that names the pty and
# not the missing mount.
for m in proc sys dev dev/pts; do
sudo mkdir -p "$ROOT/$m"
done
mountpoint -q "$ROOT/proc" || sudo mount -t proc proc "$ROOT/proc"
mountpoint -q "$ROOT/sys" || sudo mount -t sysfs sys "$ROOT/sys"
mountpoint -q "$ROOT/dev" || sudo mount --bind /dev "$ROOT/dev"
mountpoint -q "$ROOT/dev/pts" || sudo mount -t devpts devpts "$ROOT/dev/pts"
# Sources and PKGBUILDs, already fetched by stage 1. Bind-mounting them
# means the chroot needs no network at all, which is worth having: this
# host cannot reach dev.gnupg.org, and a build that silently re-fetches
# would be a different build.
mountpoint -q "$ROOT/build" || sudo mount --bind "$WORK/pkg" "$ROOT/build"
mountpoint -q "$ROOT/repo2" || sudo mount --bind "$REPO2" "$ROOT/repo2"
}
umount_chroot() {
for m in repo2 build dev/pts dev sys proc; do
mountpoint -q "$ROOT/$m" && sudo umount -l "$ROOT/$m"
done
return 0
}
# in_chroot <command...> -- run as the builder, with a sane environment.
in_chroot() {
sudo chroot --userspec="$BUILD_UID:$BUILD_GID" "$ROOT" \
/usr/bin/env -i \
HOME=/build PATH=/usr/bin \
LC_ALL=C.UTF-8 \
/usr/bin/bash -lc "$*"
}
smoke_test() {
log "Smoke test: does the chroot build anything at all?"
# NO PIPELINES IN THESE CHECKS. The first version ran `makeinfo --version |
# head -1`, and $? came from head, so a perl that could not start was
# reported as ok with its own error message as the version string. Same
# shape as the `if build_package` bug that once reported "51 built, 0
# failed" while four packages had failed. Each check runs one command and
# its status is the command's.
# HARD versus KNOWN-DRIFT, because they mean different things. A hard
# check failing means the chroot cannot build and stage 2 must not start.
# A drift check failing means a stage-1 package carries a host version
# mismatch that STAGE 2 ITSELF repairs, by rebuilding that package before
# the ones that need it. Treating the second as fatal would refuse to run
# the very thing that fixes it.
local drift="makeinfo"
local ok=0 fail=0 noted=0
while read -r desc cmd; do
[ -n "$desc" ] || continue
local out rc
out=$(in_chroot "$cmd" 2>&1); rc=$?
if [ "$rc" -eq 0 ]; then
printf ' ok %-12s %s\n' "$desc" "${out%%$'\n'*}"; ok=$((ok+1))
elif [[ " $drift " == *" $desc "* ]]; then
printf ' note %-12s %s\n' "$desc" "${out%%$'\n'*}"; noted=$((noted+1))
else
printf ' FAIL %-12s rc=%s %s\n' "$desc" "$rc" "${out%%$'\n'*}"; fail=$((fail+1))
fi
done <<'CHECKS'
bash bash --version
gcc gcc --version
ld ld --version
make make --version
makepkg makepkg --version
fakeroot fakeroot -- /usr/bin/id -u
bison bison --version
flex flex --version
perl perl -e 'print "perl $]\n"'
makeinfo makeinfo --version
compile cd /build && mkdir -p .stage2-smoke && cd .stage2-smoke && printf 'int main(void){return 0;}' > t.c && gcc t.c -o t && ./t && echo compiled-and-ran
CHECKS
printf '\n %s ok, %s failed, %s known drift\n' "$ok" "$fail" "$noted"
if [ "$noted" -gt 0 ]; then
cat <<'NOTE'
makeinfo is the one expected failure, and it is what stage 2 exists for:
texinfo was built against the HOST's perl 5.40 and our perl package is 5.42,
so its XS module refuses to load ("Perl API version ... does not match").
Rebuilding texinfo inside this chroot fixes it -- which is why texinfo has to
come EARLY in the rebuild order, before gcc, glibc and binutils, all of which
call makeinfo.
NOTE
fi
[ "$fail" -eq 0 ]
}
[ADD] stage 2: the rebuild loop, and git to feed it The loop applies each hook on the HOST -- /build is the same directory from both sides, so makepkg in the chroot reads the patched PKGBUILD and nothing is duplicated inside. It drops --nocheck: stage 1 skipped the test suites because they ran against the host's libraries, and here they test what was built. Each rebuilt package is installed into the chroot before the next one, with the host's pacman and --root, because the chroot's own pacman will not start until stage 2 has rebuilt it. Two hooks must NOT run there, and both for the same satisfying reason: the condition they work around does not exist in the chroot. libgcrypt.sh points at a host prefix holding our libgpg-error, which the chroot has installed properly. git.sh drops ZLIB_NG=1 because Ubuntu ships no zlib-ng headers, while our own zlib-ng package ships them. git is here because STAGE 2 needs it, not the repository: 51 of the 159 PKGBUILDs take their sources from git+https, and makepkg validates that clone even under --noextract. Nothing depends on git. Its three -- perl-error, perl-mailtools with perl-timedate, zlib-ng -- were read from the depends array rather than from my own tool, which had reported `zsh` as a dependency of git. It is not; the tool's regex was catching a neighbouring array. --- FR --- La boucle applique chaque crochet sur l'HÔTE — /build est le même répertoire des deux côtés, donc makepkg dans le chroot lit le PKGBUILD corrigé et rien n'est dupliqué dedans. Elle abandonne --nocheck : l'étage 1 sautait les suites de tests parce qu'elles s'exécutaient contre les bibliothèques de l'hôte ; ici elles éprouvent ce qui a été bâti. Chaque paquet reconstruit est installé dans le chroot avant le suivant, avec le pacman de l'hôte et --root, celui du chroot ne démarrant pas avant que l'étage 2 ne l'ait reconstruit. Deux crochets ne doivent PAS y tourner, et pour la même raison satisfaisante : la condition qu'ils contournent n'existe pas dans le chroot. libgcrypt.sh pointe sur un préfixe hôte contenant notre libgpg-error, que le chroot a installé correctement. git.sh retire ZLIB_NG=1 parce qu'Ubuntu ne livre pas les en-têtes zlib-ng, alors que notre propre paquet zlib-ng les livre. git est là parce que l'ÉTAGE 2 en a besoin, pas le dépôt : 51 des 159 PKGBUILD prennent leurs sources en git+https, et makepkg valide ce clone même sous --noextract. Rien ne dépend de git. Ses trois dépendances — perl-error, perl-mailtools avec perl-timedate, zlib-ng — ont été lues dans le tableau depends plutôt que dans mon propre outil, qui annonçait `zsh` comme dépendance de git. Elle ne l'est pas : la regex de l'outil attrapait un tableau voisin. Assisted-by: Claude Opus 5
2026-08-19 08:56:41 -04:00
# Hooks that must NOT run in stage 2.
#
# Most stage-1 hooks are still right inside the chroot: the architectural ones
# (systemd's EFI, glibc's SFrame, gcc's multilib) describe s390x, and the
# host-absence ones (pam's fop, gnutls's leancrypto, krb5's ss) describe a
# build environment that has not changed. A few are no-ops here and harmless
# -- the libdir hooks insert a value meson would already have chosen.
#
# This list is for the ones that would actively BREAK. libgcrypt.sh extracts
# our libgpg-error into $WORK/stage1-prefix and injects that absolute host path
# into build(); the path does not exist in the chroot. It is also unnecessary
# there, because the chroot HAS our libgpg-error 1.61 installed, so
# /usr/bin/gpgrt-config is already the new one. That is stage 2 working as
# intended: the reason for the hook disappears.
# git.sh joins it for the same reason: it drops ZLIB_NG=1 because Ubuntu has no
# zlib-ng headers, and our own zlib-ng package ships them, so inside the chroot
# the flag is correct and the hook would be a downgrade.
[FIX] stage 2: x86_64 compiler flags, and a .pc naming nothing Two failures three packages apart, both from the same place: our pacman package ships Arch's configuration verbatim, and Arch's is for x86_64. libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler `cc ... -march=x86-64 -mtune=generic ...` configure_chroot fixed CARCH, CHOST, MAKEFLAGS and OPTIONS and left CFLAGS alone. They are emptied rather than translated, because that is what stage 1 used -- the host's makepkg.conf has no CFLAGS line at all -- and 179 working packages are the evidence. s390x tuning is a deliberate later choice. Emptied by APPENDING, not commenting. The first attempt put a # in front of each assignment, and CFLAGS spans several lines: commenting the first left the continuations active and the quote unbalanced, so makepkg would not start. Then readline. Its .pc says `Requires.private: termcap` and this repository ships tinfo.pc; nothing provides termcap.pc. Nothing failed at build time -- a .pc is data, and pkg-config only follows Requires.private when a consumer asks. The first consumer to ask was libxml2, in the chroot, one stage and three packages from the cause. --- FR --- Deux échecs à trois paquets d'écart, de la même origine : notre paquet pacman livre la configuration d'Arch telle quelle, et celle d'Arch vise x86_64. libxml2/meson.build:1:0: ERROR: Unable to detect linker for compiler `cc ... -march=x86-64 -mtune=generic ...` configure_chroot corrigeait CARCH, CHOST, MAKEFLAGS et OPTIONS, et laissait CFLAGS. Ils sont vidés plutôt que traduits, car c'est ce qu'a utilisé l'étage 1 — le makepkg.conf de l'hôte n'a aucune ligne CFLAGS — et 179 paquets fonctionnels en sont la preuve. Le réglage pour s390x est un choix ultérieur délibéré. Vidés par AJOUT, non par commentaire. La première tentative mettait un # devant chaque affectation, et CFLAGS s'étend sur plusieurs lignes : commenter la première laissait les continuations actives et le guillemet déséquilibré, si bien que makepkg ne démarrait plus. Puis readline. Son .pc dit « Requires.private: termcap » alors que ce dépôt livre tinfo.pc ; personne ne fournit termcap.pc. Rien n'échouait à la compilation — un .pc est une donnée, et pkg-config ne suit Requires.private que si un consommateur le demande. Le premier à demander fut libxml2, dans le chroot, à une étape et trois paquets de la cause. Assisted-by: Claude Opus 5
2026-08-19 20:19:37 -04:00
# meson.sh joins them: it moves a wheel out of /usr/local, which only the
# host's Debian-patched python puts there.
STAGE2_SKIP_HOOKS=(libgcrypt git meson)
[ADD] stage 2: the rebuild loop, and git to feed it The loop applies each hook on the HOST -- /build is the same directory from both sides, so makepkg in the chroot reads the patched PKGBUILD and nothing is duplicated inside. It drops --nocheck: stage 1 skipped the test suites because they ran against the host's libraries, and here they test what was built. Each rebuilt package is installed into the chroot before the next one, with the host's pacman and --root, because the chroot's own pacman will not start until stage 2 has rebuilt it. Two hooks must NOT run there, and both for the same satisfying reason: the condition they work around does not exist in the chroot. libgcrypt.sh points at a host prefix holding our libgpg-error, which the chroot has installed properly. git.sh drops ZLIB_NG=1 because Ubuntu ships no zlib-ng headers, while our own zlib-ng package ships them. git is here because STAGE 2 needs it, not the repository: 51 of the 159 PKGBUILDs take their sources from git+https, and makepkg validates that clone even under --noextract. Nothing depends on git. Its three -- perl-error, perl-mailtools with perl-timedate, zlib-ng -- were read from the depends array rather than from my own tool, which had reported `zsh` as a dependency of git. It is not; the tool's regex was catching a neighbouring array. --- FR --- La boucle applique chaque crochet sur l'HÔTE — /build est le même répertoire des deux côtés, donc makepkg dans le chroot lit le PKGBUILD corrigé et rien n'est dupliqué dedans. Elle abandonne --nocheck : l'étage 1 sautait les suites de tests parce qu'elles s'exécutaient contre les bibliothèques de l'hôte ; ici elles éprouvent ce qui a été bâti. Chaque paquet reconstruit est installé dans le chroot avant le suivant, avec le pacman de l'hôte et --root, celui du chroot ne démarrant pas avant que l'étage 2 ne l'ait reconstruit. Deux crochets ne doivent PAS y tourner, et pour la même raison satisfaisante : la condition qu'ils contournent n'existe pas dans le chroot. libgcrypt.sh pointe sur un préfixe hôte contenant notre libgpg-error, que le chroot a installé correctement. git.sh retire ZLIB_NG=1 parce qu'Ubuntu ne livre pas les en-têtes zlib-ng, alors que notre propre paquet zlib-ng les livre. git est là parce que l'ÉTAGE 2 en a besoin, pas le dépôt : 51 des 159 PKGBUILD prennent leurs sources en git+https, et makepkg valide ce clone même sous --noextract. Rien ne dépend de git. Ses trois dépendances — perl-error, perl-mailtools avec perl-timedate, zlib-ng — ont été lues dans le tableau depends plutôt que dans mon propre outil, qui annonçait `zsh` comme dépendance de git. Elle ne l'est pas : la regex de l'outil attrapait un tableau voisin. Assisted-by: Claude Opus 5
2026-08-19 08:56:41 -04:00
# Packages whose sources must be extracted on the HOST, because the chroot
# cannot extract anything until they are rebuilt.
#
# THE CYCLE. makepkg extracts with bsdtar. bsdtar is libarchive, libarchive
# links libxml2 for xar support, and the stage-1 libxml2 was linked against the
# HOST's ICU 76 while our icu package ships ICU 78:
#
# bsdtar: error while loading shared libraries: libicuuc.so.76
#
# So nothing unpacks in the chroot until libxml2 is rebuilt, and libxml2 cannot
# unpack in the chroot. One of the nine soname drifts, turned into a bootstrap
# cycle by the one tool that has to work first.
#
# Extraction is not compilation, so doing it outside is less of an impurity
# than it looks -- the host's bsdtar unpacks a tarball byte for byte. What DOES
# leak is prepare(), which `makepkg -o` also runs: mostly patching, but where
# it runs autoreconf the generated configure carries the host's autotools.
# That is why this is a LIST and not the default. Once libxml2 is rebuilt,
# bsdtar works and everything after it extracts in the chroot.
STAGE2_HOST_EXTRACT=(libxml2)
host_extract() {
local n="$1" h
for h in "${STAGE2_HOST_EXTRACT[@]}"; do [ "$n" = "$h" ] && return 0; done
return 1
}
skip_hook() {
local n="$1" h
for h in "${STAGE2_SKIP_HOOKS[@]}"; do [ "$n" = "$h" ] && return 0; done
return 1
}
# stage2_build <name> -- rebuild one package inside the chroot and install it.
#
# The hook is applied on the HOST, not in the chroot: hooks are seds over the
# PKGBUILD, and /build is the same directory seen from both sides, so the
# patched file is what makepkg reads. Nothing needs to be duplicated inside.
stage2_build() {
local name="$1"
local dir="$WORK/pkg/$name"
[ -d "$dir" ] || { echo " no checkout for $name" >&2; return 1; }
( cd "$dir" && git checkout -- PKGBUILD 2>/dev/null ) || true
if [ -f "$PATCH_DIR/$name.sh" ]; then
if skip_hook "$name"; then
echo " hook skipped (stage-1 only)"
else
( cd "$dir" && bash "$PATCH_DIR/$name.sh" ) || {
echo " hook failed" >&2; return 1; }
fi
fi
( cd "$dir" && rm -f ./*.pkg.tar.* ) || true
# NO --nocheck. Stage 1 skipped the test suites because they ran against
# the host's libraries and their verdict said nothing about the port. Here
# they test what was actually built, which is the whole point of stage 2.
local mkflags="--nodeps --ignorearch --skippgpcheck --skipchecksums"
if host_extract "$name"; then
echo " extracting on the host (chroot bsdtar not usable yet)"
( cd "$dir" && LC_ALL=C.UTF-8 makepkg $mkflags -o -C -f ) || return 1
# -e: build in the tree already there. -C would wipe it again.
in_chroot "cd /build/$name && makepkg $mkflags -e -f" || return 1
else
in_chroot "cd /build/$name && makepkg $mkflags -C -f" || return 1
fi
# An exit code is not proof. Only the artefact is.
local produced=()
shopt -s nullglob; produced=("$dir"/*.pkg.tar.*); shopt -u nullglob
[ "${#produced[@]}" -gt 0 ] || { echo " no package produced" >&2; return 1; }
cp -f "${produced[@]}" "$REPO2/" || return 1
local names=() f
for f in "${produced[@]}"; do names+=("$(basename "$f")"); done
( cd "$REPO2" && repo-add core.db.tar.gz "${names[@]}" ) > /dev/null || return 1
# Install into the chroot so the NEXT package builds against it. With the
# host's pacman and --root, because the chroot's own pacman does not start
# until stage 2 has rebuilt it.
sudo pacman --root "$ROOT" --config "$CONF2" --cachedir "$CACHE" \
--noconfirm -U "${produced[@]}" > "$WORK/stage2-inst-$name.txt" 2>&1 || {
tail -10 "$WORK/stage2-inst-$name.txt" >&2; return 1; }
printf ' installed %s package(s)\n' "${#produced[@]}"
}
# A pacman.conf that sees BOTH repositories: stage 2's output first, so a
# rebuilt package wins, and stage 1 behind it for everything not yet redone.
write_conf2() {
cat > "$CONF2" <<EOF
[options]
Architecture = s390x
SigLevel = Never
[stage2]
Server = file://$REPO2
[core]
Server = file://$REPO1
EOF
}
rebuild() {
write_conf2
mkdir -p "$REPO2"
local ok=0 fail=0 failed=()
for p in "$@"; do
if grep -qxF "$p" "$STATE2" 2>/dev/null; then
echo "== $p already rebuilt, skipping =="; continue
fi
log "stage 2: $p"
if stage2_build "$p" > "$WORK/stage2-log-$p.txt" 2>&1; then
echo "$p" >> "$STATE2"; ok=$((ok + 1)); echo "OK $p"
else
fail=$((fail + 1)); failed+=("$p")
echo "FAIL $p (see $WORK/stage2-log-$p.txt)"
tail -5 "$WORK/stage2-log-$p.txt" | sed 's/^/ /'
fi
done
echo
echo "== stage 2: $ok rebuilt, $fail failed =="
[ "$fail" -eq 0 ] || printf ' failed: %s\n' "${failed[*]}"
}
[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
main() {
require_space
[ -f "$REPO1/core.db.tar.gz" ] || die "no stage-1 repository at $REPO1"
trap umount_chroot EXIT
make_rootfs
configure_chroot
mount_chroot
smoke_test || die "the chroot cannot build; stage 2 stops here"
log "Chroot ready"
[ADD] stage 2: the rebuild loop, and git to feed it The loop applies each hook on the HOST -- /build is the same directory from both sides, so makepkg in the chroot reads the patched PKGBUILD and nothing is duplicated inside. It drops --nocheck: stage 1 skipped the test suites because they ran against the host's libraries, and here they test what was built. Each rebuilt package is installed into the chroot before the next one, with the host's pacman and --root, because the chroot's own pacman will not start until stage 2 has rebuilt it. Two hooks must NOT run there, and both for the same satisfying reason: the condition they work around does not exist in the chroot. libgcrypt.sh points at a host prefix holding our libgpg-error, which the chroot has installed properly. git.sh drops ZLIB_NG=1 because Ubuntu ships no zlib-ng headers, while our own zlib-ng package ships them. git is here because STAGE 2 needs it, not the repository: 51 of the 159 PKGBUILDs take their sources from git+https, and makepkg validates that clone even under --noextract. Nothing depends on git. Its three -- perl-error, perl-mailtools with perl-timedate, zlib-ng -- were read from the depends array rather than from my own tool, which had reported `zsh` as a dependency of git. It is not; the tool's regex was catching a neighbouring array. --- FR --- La boucle applique chaque crochet sur l'HÔTE — /build est le même répertoire des deux côtés, donc makepkg dans le chroot lit le PKGBUILD corrigé et rien n'est dupliqué dedans. Elle abandonne --nocheck : l'étage 1 sautait les suites de tests parce qu'elles s'exécutaient contre les bibliothèques de l'hôte ; ici elles éprouvent ce qui a été bâti. Chaque paquet reconstruit est installé dans le chroot avant le suivant, avec le pacman de l'hôte et --root, celui du chroot ne démarrant pas avant que l'étage 2 ne l'ait reconstruit. Deux crochets ne doivent PAS y tourner, et pour la même raison satisfaisante : la condition qu'ils contournent n'existe pas dans le chroot. libgcrypt.sh pointe sur un préfixe hôte contenant notre libgpg-error, que le chroot a installé correctement. git.sh retire ZLIB_NG=1 parce qu'Ubuntu ne livre pas les en-têtes zlib-ng, alors que notre propre paquet zlib-ng les livre. git est là parce que l'ÉTAGE 2 en a besoin, pas le dépôt : 51 des 159 PKGBUILD prennent leurs sources en git+https, et makepkg valide ce clone même sous --noextract. Rien ne dépend de git. Ses trois dépendances — perl-error, perl-mailtools avec perl-timedate, zlib-ng — ont été lues dans le tableau depends plutôt que dans mon propre outil, qui annonçait `zsh` comme dépendance de git. Elle ne l'est pas : la regex de l'outil attrapait un tableau voisin. Assisted-by: Claude Opus 5
2026-08-19 08:56:41 -04:00
if [ "$#" -eq 0 ]; then
echo " enter it with:"
echo " sudo chroot --userspec=$BUILD_UID:$BUILD_GID $ROOT /usr/bin/bash -l"
echo " or rebuild packages:"
echo " bash $0 texinfo perl m4 autoconf ..."
return 0
fi
touch "$STATE2"
rebuild "$@"
[ADD] stage 2: a chroot that builds Stage 1 is done and its output is provably wrong in nine places: binaries asking for libgpgme.so.11, libnettle.so.8, libicuuc.so.76 and six more at the HOST's soname versions. Stage 2 dissolves all nine by rebuilding each package against what the repository actually ships. The constraint that shapes it: pacman cannot run inside the stage-1 rootfs, because libalpm was linked against the host's gpgme. So the rootfs is populated from OUTSIDE, with the host's pacman and --root, and the chroot is used only to build. That is not a workaround, it is the order the problem has -- stage 2's own output is the first pacman that will run on the target. Five packages were added to stage 1 for this, and the resolver could never have named them: nothing DEPENDS on fakeroot or bison, they are simply what a build needs to happen. makepkg refuses to run as root, so the chroot carries a user with the host's uid -- a bind mount keeps the numeric owner, and a different uid inside could not write $srcdir. The smoke test separates hard failures from known drift. makeinfo fails because texinfo was built against the host's perl; that is what stage 2 repairs, so refusing to start over it would refuse to run the fix. --- FR --- L'étage 1 est terminé et sa sortie est démontrablement fausse en neuf points : des binaires réclamant libgpgme.so.11, libnettle.so.8, libicuuc.so.76 et six autres, aux versions de soname de l'HÔTE. L'étage 2 les dissout tous les neuf en reconstruisant chaque paquet contre ce que le dépôt livre réellement. La contrainte qui le façonne : pacman ne peut pas tourner dans le rootfs d'étage 1, libalpm ayant été lié contre le gpgme de l'hôte. Le rootfs est donc peuplé depuis l'EXTÉRIEUR, avec le pacman de l'hôte et --root, et le chroot ne sert qu'à bâtir. Ce n'est pas un contournement mais l'ordre qu'a le problème : la sortie de l'étage 2 est le premier pacman qui tournera sur la cible. Cinq paquets ont rejoint l'étage 1 pour cela, et le résolveur n'aurait jamais pu les nommer : rien ne DÉPEND de fakeroot ni de bison, ils sont simplement ce qu'il faut pour qu'une compilation ait lieu. makepkg refuse de tourner en root, le chroot porte donc un utilisateur avec l'uid de l'hôte — un bind mount conserve le propriétaire numérique, et un uid différent ne pourrait pas écrire $srcdir. Le smoke test sépare les échecs durs des dérives connues. makeinfo échoue parce que texinfo a été bâti contre le perl de l'hôte ; c'est précisément ce que l'étage 2 répare, donc refuser de démarrer pour cela serait refuser de lancer le correctif. Assisted-by: Claude Opus 5
2026-08-19 08:30:49 -04:00
}
main "$@"