Stage 2 could rebuild one named package. It could not rebuild a hundred and thirty-three, for reasons that were all about the driver. The order is not re-derived: it is the closure stage 1 arrived at over four rounds of resolver output and then confirmed by 179 builds, so it moves to packages.sh and both stages read it. make_rootfs wipes the chroot every run, which is what makes it reproducible and what made stage 2 unresumable -- stage2.state outlived the packages it named. Its output is now put back. The stall guard is shared rather than copied: stage 2 needs it more, since a test suite is the likeliest thing in a build to wait forever. Build trees are removed after a package installs, never after it fails. --- FR --- L'étage 2 savait rebâtir un paquet nommé. Il ne savait pas en rebâtir cent trente-trois, pour des raisons qui tenaient toutes au pilote. L'ordre n'est pas réinventé : c'est la fermeture obtenue à l'étage 1 en quatre tours de sortie du résolveur, puis confirmée par 179 constructions. Il passe donc dans packages.sh, que les deux étages lisent. make_rootfs efface le chroot à chaque passage — ce qui le rend reproductible et rendait l'étage 2 irreprenable, stage2.state survivant aux paquets qu'il nommait. Sa production y est désormais réinstallée. La garde d'immobilité est partagée plutôt que recopiée : l'étage 2 en a plus besoin, une suite de tests étant ce qui attend le plus volontiers pour toujours. Les arbres de compilation sont effacés après installation, jamais après un échec. Assisted-by: Claude Opus 5
127 lines
5.7 KiB
Bash
Executable file
127 lines
5.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Stage 1 of the port: build a self-hosting Arch `core` for s390x.
|
|
#
|
|
# THE THREE-STAGE DISCIPLINE, AND WHY IT IS NOT OPTIONAL
|
|
#
|
|
# Stage 1 builds with the HOST toolchain (Ubuntu gcc/glibc). Every package it
|
|
# produces is therefore linked against the host's glibc, not Arch's. That is
|
|
# acceptable -- and unavoidable, since Arch's glibc needs an Arch gcc which
|
|
# needs an Arch glibc -- but it is not a port yet.
|
|
#
|
|
# Stage 2 chroots into the stage-1 result and rebuilds everything with the
|
|
# stage-1 toolchain. Stage 3 repeats it, and a port is self-hosting once
|
|
# stage 3 reproduces stage 2. Skipping this leaves host artefacts baked into
|
|
# packages that will fail months later, far from their cause.
|
|
#
|
|
# This script is stage 1 only.
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$HERE/bootstrap-pacman.sh"
|
|
|
|
WORK="${WORK:-$HOME/work/arch-s390x}"
|
|
REPO="${REPO:-$WORK/repo/s390x}"
|
|
STATE="$WORK/stage1.state"
|
|
|
|
# Build order. It follows link-time dependencies, not pacman metadata:
|
|
# --nodeps means pacman never checks, so anything a compiler actually needs
|
|
# must already exist. Within a group the order is free.
|
|
# The list and its order live in one file, read by both stages -- see the
|
|
# header of packages.sh for why stage 2 must not derive its own.
|
|
source "$HERE/packages.sh"
|
|
|
|
# Kill a build that has stopped producing output.
|
|
#
|
|
# WHY THIS EXISTS. python's PKGBUILD contains
|
|
#
|
|
# while ! xvfb-run -a -n "$servernum" /bin/true 2>/dev/null; do
|
|
# servernum=$((servernum+1)); done
|
|
#
|
|
# and xvfb-run is not installed here. It exits 127, `!` inverts that to true,
|
|
# and the loop tries the next display number. Forever -- there is no exit
|
|
# condition for "the command does not exist", and the 2>/dev/null swallows the
|
|
# one line that would have said so.
|
|
#
|
|
# Measured before anyone noticed: TEN HOURS of wall clock, four million PIDs,
|
|
# and a log frozen at `configure: creating Makefile`. Every signal I had said
|
|
# the build was healthy -- the process was alive, burning 40% CPU, sitting in
|
|
# a plausible source directory. A process list cannot tell work from spinning.
|
|
#
|
|
# A STALLED LOG NEXT TO A LIVE makepkg CAN. That is the whole idea here.
|
|
#
|
|
# The threshold is deliberately generous. Real builds do go quiet: a long link,
|
|
# a test suite that prints nothing, gcc between bootstrap stages. Forty-five
|
|
# minutes of COMPLETE silence is not one of those, and the case this was
|
|
# written for ran two hundred times longer. EL_STALL_MIN=0 disables it.
|
|
#
|
|
# `set -m` rather than setsid or a bare `&`. The whole tree has to die, because
|
|
# makepkg forks children that outlive it -- killing the parent alone leaves
|
|
# them spinning, which is how ten hours happened. Job control gives the
|
|
# background job its own process group, so `kill -- -$pid` reaches all of it.
|
|
#
|
|
# A subshell, NOT `setsid bash -c "$(declare -f ...)"`. The first attempt here
|
|
# re-declared build_package into a fresh shell, which loses $WORK, $REPO,
|
|
# $PATCH_DIR and every other function it calls -- a guard that would have
|
|
# broken the thing it was guarding.
|
|
# The stall guard moved to bootstrap-pacman.sh when stage 2 needed it too.
|
|
build_watched() { watched "$2" build_package "$1"; }
|
|
|
|
built() { grep -qxF "$1" "$STATE" 2>/dev/null; }
|
|
# Appended only once: a forced rebuild of an already-built package must not
|
|
# leave two identical lines in the state file. built() uses grep -qxF, so a
|
|
# duplicate is harmless to the logic and confusing to a reader counting lines.
|
|
mark() { built "$1" || echo "$1" >> "$STATE"; }
|
|
|
|
main() {
|
|
mkdir -p "$WORK/pkg" "$REPO"
|
|
touch "$STATE"
|
|
# The stand-ins are read from /usr/local/bin, so a stage-1 run that never
|
|
# reinstalls them silently builds with whatever was deployed weeks ago.
|
|
# Cheap, idempotent, and it makes this repository the source of truth.
|
|
install_host_shims
|
|
# Named packages rebuild on demand, the way stage 2 already works.
|
|
#
|
|
# Without this, rebuilding one package means deleting its line from
|
|
# stage1.state first -- editing the record of what was built in order to
|
|
# build something. That file is the port's memory; the opening instruction
|
|
# for this work is to regenerate it from what is REALLY in repo/s390x and
|
|
# never from a log, and hand-editing it is the same mistake in a smaller
|
|
# form. Naming a package is also how a hook gets tested: libarchive's xar
|
|
# fix needed exactly one package rebuilt, not a pass over a hundred and
|
|
# ninety.
|
|
local list=("${STAGE1_PACKAGES[@]}") forced=0
|
|
if [ "$#" -gt 0 ]; then
|
|
list=("$@")
|
|
forced=1
|
|
printf '== stage 1: rebuilding on request: %s ==\n' "$*"
|
|
fi
|
|
local ok=0 fail=0 rc=0 failed=()
|
|
for p in "${list[@]}"; do
|
|
# An explicit request outranks the state file. Asking for a package
|
|
# that is already built and being told it was skipped would make the
|
|
# command useless for the one job it exists to do.
|
|
if [ "$forced" -eq 0 ] && built "$p"; then
|
|
echo "== $p already built, skipping =="
|
|
continue
|
|
fi
|
|
# A failure must not stop the run: one missing package should not hide
|
|
# the state of the forty that follow. They are collected and reported.
|
|
if build_watched "$p" "$WORK/log-$p.txt"; then
|
|
mark "$p"; ok=$((ok + 1))
|
|
echo "OK $p"
|
|
else
|
|
rc=$?
|
|
fail=$((fail + 1)); failed+=("$p")
|
|
if [ "$rc" -eq 2 ]; then
|
|
echo "STALL $p (no output for ${EL_STALL_MIN:-45} min, killed)"
|
|
else
|
|
echo "FAIL $p (see $WORK/log-$p.txt)"
|
|
fi
|
|
fi
|
|
done
|
|
echo
|
|
echo "== stage 1: $ok built, $fail failed =="
|
|
[ "$fail" -eq 0 ] || printf ' failed: %s\n' "${failed[*]}"
|
|
}
|
|
|
|
main "$@"
|