ci: cross-compile with crafter-build instead of pmbootstrap chroots
All checks were successful
package / package (push) Successful in 1m50s

The pmbootstrap approach needed the privileged pmos runner, which only
fp6-img can reach. This repo already had the right flow: make-sysroot.sh
+ crafter-build --target=aarch64-alpine-linux-musl + APKBUILD.binary —
unprivileged, so it runs in an alpine:edge container on the ordinary
arch-latest runner, and minutes instead of qemu-chroot hours. The test
suite runs natively; the aarch64 binaries are further exercised by
fp6-img's chroot 'make check' whenever an image builds.

APKBUILD.binary gains the pmOS integration payload the canonical aport
ships (systemd subpackage, enable preset, wants symlink, config-gated
drop-in, kde-modem-daemon skel override) — required before this pipeline
may publish a version users upgrade to, or the upgrade would strip those
files. pkgver now follows implementations/main.cpp (sed'd by the CI, and
bumped to 0.3.1 in the file).
This commit is contained in:
Jorijn van der Graaf 2026-09-01 23:31:27 +02:00
commit 87376ccd9c
3 changed files with 147 additions and 110 deletions

View file

@ -1,115 +1,111 @@
#!/bin/sh -eu
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
# CI package build: produce the imsd apk(s) for aarch64 from THIS checkout,
# using packaging/aport/ (the canonical aport) and pmbootstrap's cross
# chroots. Runs in CI inside an Alpine container on the privileged "pmos"
# runner (qemu-user binfmt on the host for the aarch64 chroots); also
# runnable in any Alpine environment with the same privileges
# (IMSD_NO_CROSSDIRECT=1 for hosts where crossdirect's /native bridge
# breaks — the build then runs qemu-only, slower, identical output).
# CI package build: cross-compile imsd for aarch64 with crafter-build (the
# README's "Cross-compiling" flow), run the test suite natively, and package
# the result via packaging/APKBUILD.binary. Expects an x86_64 Alpine
# environment with root — the workflow runs it in an alpine:edge job
# container on an ordinary runner. Root only installs packages and hands off
# to a scratch user: the sysroot is built with apk.static --usermode (which
# refuses root) and abuild wants a user too.
#
# Built packages land in ~build/.local/var/pmbootstrap/packages/*/aarch64/;
# the workflow's publish step uploads the imsd*.apk ones to the Forgejo
# Alpine registry.
# Built packages land in /home/build/.local/share/abuild/*/aarch64/imsd*.apk;
# the workflow's publish step uploads them to the Forgejo Alpine registry.
set -eu
PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git
# The musl build of crafter-build (Crafter.Build CI's release-musl job):
# this container is Alpine, and the glibc launcher cannot run on musl. v2 =
# SSE4.2 baseline: the CI box is an Intel N5105 (no AVX). Overridable for
# local rehearsals (file:// works).
CRAFTER_URL=${CRAFTER_URL:-https://forgejo.catcrafts.net/Catcrafts/Crafter.Build/releases/download/latest/crafter-build-linux-x86_64-musl-v2.tar.gz}
SRC=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
# pmbootstrap refuses to run as root: install deps, then re-exec as a build
# user with passwordless sudo (pmbootstrap escalates itself where needed).
# clang cross-targets aarch64 natively and the target's libc++/glib come from
# the sysroot; llvm-runtimes/libc++-dev/glib-dev here serve the NATIVE
# test-suite run. build-base = Alpine's standard build environment (the one
# abuild implies): binutils' ld/ar for clang's default link driver, gcc's
# libgcc_s/crt objects the musl clang driver links against.
if [ "$(id -u)" = 0 ]; then
apk add -q git sudo python3 py3-pip multipath-tools util-linux tar
# pmbootstrap pinned from git: Alpine's package is older. The pmOS
# gitlab hiccups under crawler load; a failure costs a retry, not the run
for _i in 1 2 3; do
pip install -q --break-system-packages \
git+https://gitlab.postmarketos.org/postmarketOS/pmbootstrap.git@3.11.1 \
&& break
if [ "$_i" = 3 ]; then
echo "pmbootstrap pip install failed after 3 attempts" >&2
exit 1
fi
echo "pmbootstrap pip install failed (attempt $_i/3), retrying in 15s..." >&2
sleep 15
done
# containers cannot modprobe; make pmbootstrap's 'sudo modprobe' a no-op
# (/usr/local/sbin precedes /sbin in sudo's secure_path)
mkdir -p /usr/local/sbin
printf '#!/bin/sh\nexit 0\n' > /usr/local/sbin/modprobe
chmod +x /usr/local/sbin/modprobe
apk add -q git curl tar clang lld llvm llvm-runtimes libc++-dev llvm-libunwind-dev glib-dev \
build-base abuild sudo
id build >/dev/null 2>&1 || adduser -D build
addgroup build abuild 2>/dev/null || true
echo 'build ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/build
# su scrubs the environment — carry the knobs that matter across it
exec su build -c "IMSD_NO_CROSSDIRECT='${IMSD_NO_CROSSDIRECT:-}' sh -eu '$SRC/packaging/build-package.sh'"
# abuild in cross mode strips with $CHOST-strip; llvm-strip handles any
# ELF arch, so give it that name
ln -sf "$(command -v llvm-strip)" /usr/local/bin/aarch64-alpine-linux-musl-strip
# the CI checkout arrives root-owned; crafter-build writes bin/ into it
chown -R build "$SRC"
# -l: a login shell, so HOME really is /home/build (abuild keys + output);
# it scrubs the environment, so carry the one knob that matters across
exec su -l build -c "CRAFTER_URL='${CRAFTER_URL:-}' sh -eu '$SRC/packaging/build-package.sh'"
fi
# the CI checkout is root-owned; let git read it as the build user
git config --global --add safe.directory "$SRC"
# git hosts occasionally hiccup; a clone failure costs a retry, not the run
clone_retry() { # clone_retry <dest> <git clone args...>
_dest=$1; shift
for _i in 1 2 3; do
rm -rf "$_dest"
git clone "$@" "$_dest" && return 0
echo "git clone $_dest failed (attempt $_i/3), retrying in 10s..." >&2
sleep 10
done
echo "git clone $_dest failed after 3 attempts" >&2
return 1
}
retry() { # retry <description> <cmd...>
_desc=$1; shift
for _i in 1 2 3; do
"$@" && return 0
echo "$_desc failed (attempt $_i/3), retrying in 30s..." >&2
sleep 30
echo "$_desc failed (attempt $_i/3), retrying in 15s..." >&2
sleep 15
done
echo "$_desc failed after 3 attempts" >&2
return 1
}
# pmbootstrap swallows its subcommands' stderr into its own log; surface it
# whenever this script dies so failures are diagnosable from CI output alone
trap 'rc=$?; if [ $rc -ne 0 ]; then
echo "=== build-package.sh failed (exit $rc); pmbootstrap log tail ==="
tail -60 "$HOME/.local/var/pmbootstrap/log.txt" 2>/dev/null || true
fi' EXIT
# implementations/main.cpp is the version's single source of truth (same
# derivation as make-bin-tarball.sh)
VER=$(sed -n 's/.*char\* Version = "\(.*\)".*/\1/p' "$SRC/implementations/main.cpp")
[ -n "$VER" ] || { echo "cannot read Version from implementations/main.cpp" >&2; exit 1; }
echo ">> packaging imsd $VER"
# --- pmaports with our aport dropped in; source tarball from this checkout
# (the Forgejo instance serves no source archives, so git-archive it)
WORK=${IMSD_PKG_WORK:-$HOME/imsd-pkg-work}
rm -rf "$WORK"
mkdir -p "$WORK"
clone_retry "$WORK/pmaports" -q --depth=1 "$PMAPORTS_REPO"
mkdir -p "$WORK/pmaports/modem"
cp -r "$SRC/packaging/aport" "$WORK/pmaports/modem/imsd"
COMMIT=$(git -C "$SRC" rev-parse --short HEAD)
git -C "$SRC" archive --prefix=imsd/ \
-o "$WORK/pmaports/modem/imsd/imsd-$COMMIT.tar.gz" HEAD
sed -i "s/^_commit=.*/_commit=\"$COMMIT\"/" "$WORK/pmaports/modem/imsd/APKBUILD"
# --- crafter-build: static launcher from the rolling release
mkdir -p "$HOME/crafter-build"
retry "fetch crafter-build" \
sh -c "curl -fsSL '$CRAFTER_URL' | tar -xz -C '$HOME/crafter-build'"
PATH="$HOME/crafter-build/bin:$PATH"
export CRAFTER_BUILD_HOME="$HOME/crafter-build/share/crafter-build"
# --- configure pmbootstrap (config written directly; 'init' is interactive)
WORKDIR="$HOME/.local/var/pmbootstrap"
mkdir -p "$WORKDIR/cache_git"
python3 -c "import pmb.config; print(pmb.config.work_version)" > "$WORKDIR/version"
mkdir -p "$HOME/.config"
cat > "$HOME/.config/pmbootstrap_v3.cfg" <<EOF
[pmbootstrap]
aports = $WORK/pmaports
device = fairphone-fp6
ui = console
systemd = always
EOF
# --- aarch64 Alpine sysroot (unprivileged: apk.static --usermode)
SYSROOT="$HOME/.cache/imsd/sysroot-aarch64-alpine"
retry "make sysroot" "$SRC/packaging/make-sysroot.sh" "$SYSROOT"
NOCROSS=${IMSD_NO_CROSSDIRECT:+--no-cross}
# --- cross-compile the products; run the suites natively (same sources,
# host arch — the aarch64 binaries are additionally exercised by fp6-img's
# chroot `make check` whenever an image builds)
cd "$SRC"
XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8-a --mtune=generic"
crafter-build -- $XTARGET
crafter-build -- --product=media $XTARGET
crafter-build -- --product=dialerd $XTARGET
crafter-build test
pmbootstrap checksum imsd
retry "build imsd" pmbootstrap $NOCROSS build --arch aarch64 imsd
# --- bundle + package
./packaging/make-bin-tarball.sh "$VER"
PKG="$HOME/pkg"
rm -rf "$PKG"
mkdir -p "$PKG"
cp "$SRC/packaging/APKBUILD.binary" "$PKG/APKBUILD"
cp "$SRC/packaging/aport/org.kde.modem.daemon.desktop" \
"$SRC/packaging/aport/80-imsd.preset" "$PKG/"
mv "imsd-$VER.tar.gz" "$PKG/"
sed -i "s/^pkgver=.*/pkgver=$VER/" "$PKG/APKBUILD"
# a throwaway signing key: phones trust the registry-signed APKINDEX, not
# per-package keys (same situation as fp6-img's pmbootstrap-built packages).
# abuild >= 3.18 keeps keys under ~/.config/abuild and output under
# ~/.local/share/abuild (REPODEST default).
abuild-keygen -a -n >/dev/null 2>&1
sudo cp "$HOME"/.config/abuild/*.rsa.pub /etc/apk/keys/
# CHOST puts abuild in cross mode so arch="aarch64" packages on this x86_64
# host. -d skips dependency handling entirely: nothing compiles under abuild
# (with -r, cross mode would try to install a nonexistent build-base-aarch64
# plus the runtime depends); !tracedeps in the APKBUILD keeps abuild from
# resolving the aarch64 ELF NEEDED entries against this x86_64 host.
cd "$PKG"
abuild checksum
CHOST=aarch64 abuild -d
echo "=== built packages ==="
ls -la "$WORKDIR"/packages/*/aarch64/imsd*.apk
ls -la "$HOME"/.local/share/abuild/*/aarch64/imsd*.apk