From 87376ccd9cbd9d3b002a1c91dde62726bdb1fe7f Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Tue, 1 Sep 2026 23:31:27 +0200 Subject: [PATCH] ci: cross-compile with crafter-build instead of pmbootstrap chroots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .forgejo/workflows/package.yml | 42 +++++---- packaging/APKBUILD.binary | 47 +++++++-- packaging/build-package.sh | 168 ++++++++++++++++----------------- 3 files changed, 147 insertions(+), 110 deletions(-) diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml index 2a12a13..e7ed598 100644 --- a/.forgejo/workflows/package.yml +++ b/.forgejo/workflows/package.yml @@ -1,20 +1,26 @@ name: package -# Builds the imsd apk(s) for aarch64 from packaging/aport/ at the pushed -# commit and publishes them to the Forgejo Alpine registry — the repo -# installed phones already point at (via catcrafts-fp6-repo), so a release -# reaches users through plain 'apk upgrade' without an fp6-img image run. +# Builds the imsd apk(s) for aarch64 from the pushed commit and publishes +# them to the Forgejo Alpine registry — the repo installed phones already +# point at (via catcrafts-fp6-repo), so a release reaches users through +# plain 'apk upgrade' without an fp6-img image run. # -# Release gating is the pkgver: the registry answers 409 for an -# already-published version and the publish step treats that as "nothing to -# do" — so pushes only release when packaging/aport/APKBUILD bumps -# pkgver/pkgrel. fp6-img images keep building imsd from their own pinned -# checkout of this repo; same aport, so the two pipelines cannot skew. +# Build: crafter-build cross-compiles against an Alpine aarch64 sysroot +# (packaging/build-package.sh — the README's "Cross-compiling" flow), the +# test suite runs natively, and packaging/APKBUILD.binary wraps the result. +# This needs no privileged runner: it runs in an alpine:edge container on +# the ordinary arch-latest runner. # -# Runs on the privileged "pmos" runner (qemu-user binfmt on the host for -# pmbootstrap's aarch64 chroots). Requires the PACKAGE_TOKEN secret -# (catbot account, package:write scope) to publish; without it the build -# still runs and the publish step skips quietly. +# Release gating is the version: pkgver comes from implementations/main.cpp, +# the registry answers 409 for an already-published version, and the publish +# step treats that as "nothing to do" — so pushes only release when the +# Version constant bumps. fp6-img images keep building imsd from their own +# pinned checkout of this repo (packaging/aport/); APKBUILD.binary's payload +# must stay identical to that aport's, so the two pipelines cannot skew. +# +# Requires the PACKAGE_TOKEN secret (catbot account, package:write scope) to +# publish; without it the build still runs and the publish step skips +# quietly. on: workflow_dispatch: @@ -23,8 +29,10 @@ on: jobs: package: - runs-on: pmos - timeout-minutes: 180 + runs-on: arch-latest + container: + image: alpine:edge + timeout-minutes: 90 steps: # actions/checkout is a Node action; bare alpine has no node - name: Provision job container @@ -33,7 +41,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Build package + - name: Build and package run: ./packaging/build-package.sh - name: Publish to the apk registry @@ -45,7 +53,7 @@ jobs: exit 0 fi found=0 - for f in /home/build/.local/var/pmbootstrap/packages/*/aarch64/imsd*.apk; do + for f in /home/build/.local/share/abuild/*/aarch64/imsd*.apk; do [ -e "$f" ] || continue found=1 code=$(curl -s -o /dev/null -w '%{http_code}' \ diff --git a/packaging/APKBUILD.binary b/packaging/APKBUILD.binary index 8c4c09b..e874d5d 100644 --- a/packaging/APKBUILD.binary +++ b/packaging/APKBUILD.binary @@ -1,12 +1,16 @@ # SPDX-License-Identifier: GPL-3.0-only # SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® # Maintainer: Jorijn van der Graaf -# Interim packaging: wraps a crafter-build binary cross-compiled on a dev box -# (see README "Cross-compiling") into a proper apk, so the daemon is -# apk-managed on the phone while crafter-build itself isn't packaged for -# Alpine yet. The source tarball is produced by packaging/make-bin-tarball.sh. +# Binary packaging: wraps a crafter-build binary cross-compiled per the +# README's "Cross-compiling" section into a proper apk — used by this repo's +# package CI (packaging/build-package.sh, which also seds pkgver from +# implementations/main.cpp) and runnable by hand. The source tarball is +# produced by packaging/make-bin-tarball.sh; the desktop/preset files come +# from packaging/aport/ (copy them next to this file). The package payload +# must stay identical to packaging/aport/APKBUILD's, or an apk upgrade +# across the two pipelines would add/strip files on users' phones. pkgname=imsd -pkgver=0.3.0 +pkgver=0.3.1 pkgrel=0 pkgdesc="Userspace IMS/VoLTE daemon for mainline Linux phones" url="https://forgejo.catcrafts.net/Catcrafts/imsd" @@ -19,8 +23,15 @@ depends="modemmanager libc++ opencore-amr vo-amrwbenc pipewire-tools" # which races imsd for the PDN and flaps it with a new prefix every ~2.5 min # (fp6 journal/ims.md s57) — the two IMS stacks cannot share one PDN provides="81voltd=$pkgver-r$pkgrel" -options="!check" -source="imsd-$pkgver.tar.gz" +# no OpenRC service yet: the unit's PDN-bring-up/env-file sequencing is only +# tested under systemd; an initd is welcome once someone can verify one +subpackages="$pkgname-systemd" +options="!check !tracedeps" +source=" + imsd-$pkgver.tar.gz + org.kde.modem.daemon.desktop + 80-imsd.preset +" package() { cd "$srcdir/imsd-$pkgver" @@ -33,4 +44,26 @@ package() { "$pkgdir"/etc/xdg/autostart/imsd-dialerd.desktop install -Dm644 net.catcrafts.IMS1.conf \ "$pkgdir"/usr/share/dbus-1/system.d/net.catcrafts.IMS1.conf + # the skel override hides kde-telephony's modem daemon autostart for the + # account created at install — imsd-dialerd owns those session bus names + install -Dm644 "$srcdir"/org.kde.modem.daemon.desktop \ + "$pkgdir"/etc/skel/.config/autostart/org.kde.modem.daemon.desktop + # enabled by preset: the unit is a no-op until /etc/imsd.env exists, and + # VoLTE surviving reboots must not depend on a manual systemctl enable + install -Dm644 "$srcdir"/80-imsd.preset \ + "$pkgdir"/usr/lib/systemd/system-preset/80-imsd.preset + mkdir -p "$pkgdir"/etc/systemd/system/multi-user.target.wants + ln -s /usr/lib/systemd/system/imsd.service \ + "$pkgdir"/etc/systemd/system/multi-user.target.wants/imsd.service + # ...but only actually start once the carrier config exists, so + # unconfigured systems don't boot into a failing unit + mkdir -p "$pkgdir"/usr/lib/systemd/system/imsd.service.d + printf '[Unit]\nConditionPathExists=/etc/imsd.env\n' \ + > "$pkgdir"/usr/lib/systemd/system/imsd.service.d/10-require-config.conf +} + +systemd() { + install_if="$pkgname=$pkgver-r$pkgrel systemd" + + amove usr/lib/systemd/system } diff --git a/packaging/build-package.sh b/packaging/build-package.sh index 52da88b..9b130aa 100755 --- a/packaging/build-package.sh +++ b/packaging/build-package.sh @@ -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=$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 _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" <= 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