Watch
1
0
Fork
You've already forked souveraine-updater
0

ci: build, package and publish into edge

Cross-build helpers from culver, prebuilt PKGBUILD, and a packaging step that
signs into souveraine-{arch} via souveraine's additive publisher. Replaces
pachub. Smoke test loads the QML root, since a null root builds green.
This commit is contained in:
Fimeg 2026-07-25 18:30:31 -04:00
commit e4f00c5ffc
8 changed files with 364 additions and 0 deletions

11
.cargo/aarch64-pkg-config Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash
# ponytail: cross pkg-config wrapper for aarch64-unknown-linux-gnu.
# cargo's [env] block doesn't reliably reach build-script pkg-config probes,
# so alsa-sys's built-in cross guard trips. This wrapper sets the vars itself
# and is referenced via PKG_CONFIG in .cargo/config.toml — every pkg-config
# probe in the tree (alsa-sys today, any future -sys crate) goes through it.
SYSROOT="${SOUVERAINE_AARCH64_SYSROOT:-/usr/aarch64-linux-gnu}"
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig"
export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"
exec /usr/bin/pkg-config "$@"

6
.cargo/config.toml Normal file
View file

@ -0,0 +1,6 @@
# Cross-build target for the Pixel 3 (aarch64 Souveraine OS). Mirrors the
# souveraine repo's proven setup; the linker wrapper resolves absolute lib
# paths inside the sysroot. Qt6 6.11.1 was extracted into
# /usr/aarch64-linux-gnu/usr from the phone's own package cache.
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

184
.gitea/workflows/ci.yml Normal file
View file

@ -0,0 +1,184 @@
name: ci
# CI for the SouveraineOS Updater (Rust + QML via CXX-Qt).
#
# Runs on the ArchDev runner (host mode, label archdev) — the same box hand
# builds use. No `uses:` actions on purpose: the runner is host-mode (no docker,
# no node), so every step is plain shell against the tools already installed
# (Qt6, rust, pacman, the aarch64 sysroot).
#
# Unlike culver, this does not stop at `cargo build`. The point of TASK-27 is
# that a green build which ships nothing is not a pipeline, so this ends in a
# signed package published into the shared souveraine-{arch} archive.
#
# primary is the dev branch; publishing is gated to it for now because this repo
# has no public branch yet.
#
# Required repo secrets:
# LOCAL_GITEA_TOKEN — PAT with write access to Fimeg/souveraine. The archive
# release lives on that repo, and a job's own GITHUB_TOKEN is scoped to its
# own repository, so it cannot upload there.
on:
push:
branches: [primary]
pull_request:
branches: [primary]
jobs:
build:
runs-on: archdev
steps:
- name: Clone repo
run: |
set -euo pipefail
rm -rf "$GITHUB_WORKSPACE/souveraine-updater"
git clone --depth 1 --branch "${GITHUB_REF_NAME}" \
"http://oauth2:${{ secrets.GITHUB_TOKEN }}@10.10.20.120:4455/${GITHUB_REPOSITORY}.git" \
"$GITHUB_WORKSPACE/souveraine-updater"
- name: cargo test
env:
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/souveraine-updater"
cargo test --workspace
- name: "cargo clippy (-D warnings)"
env:
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/souveraine-updater"
cargo clippy --workspace --all-targets -- -D warnings
- name: Build release binaries (aarch64 + x86_64)
env:
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/souveraine-updater"
# Persistent target dir: fresh clone per run, warm compile cache.
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-updater-ci-target"
export SOUVERAINE_AARCH64_SYSROOT="$HOME/aarch64-sysroot"
# build-cross.sh asserts the binary is aarch64 AND that every Qt
# library it NEEDs exists in the sysroot it linked against — the
# archdev sysroot has half-updated silently before (TASK-27 finding 6)
# and a mixed-ABI cross build otherwise ships looking healthy.
./scripts/build-cross.sh
cp "$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-updater" \
souveraine-updater-aarch64
cargo build --release
BIN="$CARGO_TARGET_DIR/release/souveraine-updater"
file "$BIN" | grep -q 'x86-64' || { echo "built binary is not x86_64" >&2; exit 1; }
cp "$BIN" souveraine-updater-x86_64
- name: "Smoke test: QML root constructs"
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/souveraine-updater"
# The x86_64 binary is runnable here, so actually load the UI. A null
# root is the failure mode this app has had from the start — an
# unresolved type or a missing qrc alias produces a clean exit and a
# blank window, which `cargo build` cannot see. main.rs reports it on
# stderr via on_object_creation_failed; treat any output as failure.
OUT=$(QT_QPA_PLATFORM=offscreen timeout 30 ./souveraine-updater-x86_64 2>&1 || true)
if [ -n "$OUT" ]; then
echo "::error::QML root did not construct cleanly:"
echo "$OUT"
exit 1
fi
echo "QML root constructed."
- name: Package and sign
env:
ARCHIVE_KEY: 3CD9E99E222C2A174986FC9AFF4949AA20C8E911
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/souveraine-updater"
PKGVER="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
WORK="$GITHUB_WORKSPACE/pacman-package"
REPO="$GITHUB_WORKSPACE/pacman-repo"
rm -rf "$WORK" "$REPO"
for ARCH in aarch64 x86_64; do
PKG_WORK="$WORK/$ARCH"
ARCH_REPO="$REPO/$ARCH"
mkdir -p "$PKG_WORK" "$ARCH_REPO"
cp "souveraine-updater-$ARCH" "$PKG_WORK/souveraine-updater-binary"
cp updater-ui/net.souveraine.Updater.svg "$PKG_WORK/"
cp packaging/net.souveraine.Updater.desktop "$PKG_WORK/"
cp LICENSE "$PKG_WORK/"
cp packaging/PKGBUILD.prebuilt "$PKG_WORK/PKGBUILD"
(
cd "$PKG_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
PKG=$(find "$PKG_WORK" -maxdepth 1 -name 'souveraine-updater-*.pkg.tar.zst' -print -quit)
test -n "$PKG"
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-updater'
bsdtar -tf "$PKG" | grep -qx 'usr/share/applications/net.souveraine.Updater.desktop'
cp "$PKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$PKG")"
done
- name: Publish into the edge archive
env:
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
ARCHIVE_KEY: 3CD9E99E222C2A174986FC9AFF4949AA20C8E911
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/souveraine-updater"
# The publisher lives in the souveraine repo and is pinned to a commit,
# the same way this pipeline pins tuie: upstream HEAD must never decide
# whether our publish is additive. It merges into the live per-arch
# database under a shared archdev flock and removes only this
# producer's superseded packages, so a souveraine push cannot delete
# the Updater (TASK-27 finding 3).
PUBLISHER_SHA=044c373
rm -rf "$GITHUB_WORKSPACE/publisher"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/Fimeg/souveraine.git" "$GITHUB_WORKSPACE/publisher"
git -C "$GITHUB_WORKSPACE/publisher" checkout "$PUBLISHER_SHA"
export PRODUCER_VERSION="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
"$GITHUB_WORKSPACE/publisher/packaging/arch/publish-edge.sh" \
souveraine-updater "$GITHUB_WORKSPACE/pacman-repo"
no-ai-attribution:
runs-on: archdev
steps:
- name: Clone repo (full history)
run: |
set -euo pipefail
rm -rf "$GITHUB_WORKSPACE/attribution-check"
git clone \
"http://oauth2:${{ secrets.GITHUB_TOKEN }}@10.10.20.120:4455/${GITHUB_REPOSITORY}.git" \
"$GITHUB_WORKSPACE/attribution-check"
- name: Check commit messages for AI attribution
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/attribution-check"
if [ "${{ github.event_name }}" = "pull_request" ]; then
RANGE="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
else
RANGE="${{ github.event.before }}..${{ github.event.after }}"
if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
RANGE="HEAD~10..HEAD"
fi
fi
CO="Co-Authored""-By:"
PATTERNS="${CO}.*[Cc]laude|${CO}.*OpenAI|${CO}.*ChatGPT|${CO}.*Copilot|${CO}.*Letta|${CO}.*Cursor|Generated by|Generated with|AI-assisted|Auto-generated by"
FAIL=0
while IFS= read -r msg; do
if echo "$msg" | grep -qiE "$PATTERNS"; then
echo "AI attribution found in commit: $msg"
FAIL=1
fi
done < <(git log --format='%H %s%n%b' "$RANGE" 2>/dev/null || true)
exit $FAIL

View file

@ -0,0 +1,40 @@
# CI package recipe for a prebuilt SouveraineOS Updater binary.
#
# The runner cross-compiles first, then makepkg assembles this package. The UI
# is embedded in the binary as a qrc module, so unlike the old pachub package
# there is no loose QML to install under /usr/share — moving the prefix cannot
# break the app, and there are no unowned .qml files left on the device.
pkgname=souveraine-updater
pkgver="${SOUVERAINE_PKGVER:?CI must set SOUVERAINE_PKGVER}"
pkgrel=1
pkgdesc="SouveraineOS Updater — pacman frontend"
arch=("${SOUVERAINE_PKGARCH:?CI must set SOUVERAINE_PKGARCH}")
url="https://gitea.wiuf.net/Fimeg/souveraine-updater"
license=('GPL-2.0-only')
# qt6-svg supplies the image-format plugin that renders the app icon; without
# it the sidebar logo silently fails to load. polkit provides pkexec, which is
# how every privileged pacman call escalates.
depends=('gcc-libs' 'qt6-base' 'qt6-declarative' 'qt6-svg' 'pacman' 'polkit')
optdepends=('pacman-contrib: paccache cleaning'
'reflector: mirror rating')
# The old Python implementation. Two package managers on one phone is how you
# end up with two views of the same database.
conflicts=('pachub')
replaces=('pachub')
# !debug as well as !strip: the binary is already built, and leaving debug on
# packages an empty /usr/src/debug tree onto the phone.
options=('!strip' '!debug')
source=('souveraine-updater-binary'
'net.souveraine.Updater.svg'
'net.souveraine.Updater.desktop'
'LICENSE')
sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP')
package() {
install -Dm755 "$srcdir/souveraine-updater-binary" "$pkgdir/usr/bin/souveraine-updater"
install -Dm644 "$srcdir/net.souveraine.Updater.svg" \
"$pkgdir/usr/share/icons/hicolor/scalable/apps/net.souveraine.Updater.svg"
install -Dm644 "$srcdir/net.souveraine.Updater.desktop" \
"$pkgdir/usr/share/applications/net.souveraine.Updater.desktop"
install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}

View file

@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Name=Updater
GenericName=Package Manager
Comment=Install, remove and upgrade packages
Exec=souveraine-updater
Icon=net.souveraine.Updater
Terminal=false
Categories=System;PackageManager;
Keywords=pacman;package;update;upgrade;
StartupNotify=true

17
scripts/aarch64-linker Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# aarch64-linker — Cargo linker wrapper for the SouveraineOS ARM sysroot.
#
# GCC's default aarch64 linker scripts contain absolute library paths. Cargo
# supplies -lm before its late rustflags, so --sysroot alone is insufficient:
# ld can select the host's x86 library. build-cross.sh supplies an overlay
# containing sysroot-relative copies of those scripts; put it first.
set -euo pipefail
SYSROOT="${SOUVERAINE_AARCH64_SYSROOT:-/usr/aarch64-linux-gnu}"
SCRIPTS="${SOUVERAINE_AARCH64_LINKER_SCRIPTS:?run scripts/build-cross.sh, not this wrapper directly}"
exec aarch64-linux-gnu-gcc \
--sysroot="$SYSROOT" \
-L"$SCRIPTS" \
-L"$SYSROOT/usr/lib" \
"$@"

42
scripts/aarch64-qmake Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env bash
# aarch64-qmake — a host-runnable qmake stand-in for cross-building cxx-qt.
# Answers the -query keys cxx-qt-build/qt-build-utils need with sysroot paths,
# so no aarch64 binary is executed on the x86 host.
# cxx-qt 0.9 runs qmake with a SCRUBBED environment (no exported vars reach
# this process), so the sysroot cannot be passed via env alone. Probe the
# known locations for one that actually holds the aarch64 Qt6.
SYS="${SOUVERAINE_AARCH64_SYSROOT:-}"
if [[ -z "$SYS" ]]; then
for c in "${HOME:-/home/casey}/aarch64-sysroot" /home/casey/aarch64-sysroot /usr/aarch64-linux-gnu; do
if [[ -e "$c/usr/lib/libQt6Core.so" ]]; then SYS="$c"; break; fi
done
SYS="${SYS:-/usr/aarch64-linux-gnu}"
fi
QT="$SYS/usr/lib/qt6"
case "$1" in
-query)
# qt-build-utils asks the "/get" variants first (yocto-ism); an empty
# answer makes it try bare "moc", which PATH-resolves to Qt5's moc.
case "${2%/get}" in
QT_VERSION) echo "6.11.1" ;;
QT_INSTALL_PREFIX) echo "$SYS/usr" ;;
QT_INSTALL_LIBS) echo "$SYS/usr/lib" ;;
QT_INSTALL_HEADERS) echo "$SYS/usr/include/qt6" ;;
# Tool dirs must be HOST paths: qt-build-utils resolves moc/rcc from
# QT_INSTALL_BINS/LIBEXECS (not QT_HOST_*), and a sysroot aarch64 moc
# silently falls back to the host Qt5 moc (version-mismatch #error).
# Arch keeps Qt6 moc/rcc/qmltyperegistrar directly in /usr/lib/qt6
# (NOT /usr/lib/qt6/bin, which only has designer-class apps).
QT_INSTALL_BINS) echo "/usr/lib/qt6" ;;
QT_INSTALL_LIBEXECS) echo "/usr/lib/qt6" ;;
QT_INSTALL_PLUGINS) echo "$SYS/usr/lib/qt6/plugins" ;;
QT_INSTALL_QML) echo "$SYS/usr/lib/qt6/qml" ;;
QT_HOST_BINS) echo "/usr/lib/qt6" ;; # host tools (moc/rcc) = x86
QT_HOST_LIBEXECS) echo "/usr/lib/qt6" ;;
QT_HOST_DATA) echo "$QT" ;;
*) echo "" ;;
esac
;;
-query) echo "" ;;
*) exec /usr/bin/qmake6 "$@" ;;
esac

53
scripts/build-cross.sh Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# build-cross.sh — cross-compile the SouveraineOS Updater for the Pixel 3.
# Same flow as culver's, which is the proven one for a cxx-qt app: the linker
# wrapper fixes GCC's absolute-path linker scripts, and the qmake shim answers
# cxx-qt's queries with sysroot paths so no aarch64 binary runs on the host.
set -euo pipefail
cd "$(dirname "$0")/.."
# Same sysroot probe as scripts/aarch64-qmake (which cannot rely on env at
# all: cxx-qt 0.9 invokes qmake with a scrubbed environment).
SYSROOT="${SOUVERAINE_AARCH64_SYSROOT:-}"
if [[ -z "$SYSROOT" ]]; then
for c in "$HOME/aarch64-sysroot" /usr/aarch64-linux-gnu; do
[[ -e "$c/usr/lib/libQt6Core.so" ]] && { SYSROOT="$c"; break; }
done
SYSROOT="${SYSROOT:-/usr/aarch64-linux-gnu}"
fi
TARGET_DIR="${CARGO_TARGET_DIR:-$PWD/target}"
LINKER_SCRIPTS="$TARGET_DIR/aarch64-linker-scripts"
[[ -d "$SYSROOT/usr/lib" ]] || { echo "sysroot missing: $SYSROOT" >&2; exit 1; }
mkdir -p "$LINKER_SCRIPTS"
while IFS= read -r script; do
name="${script##*/}"; tmp="$LINKER_SCRIPTS/.${name}.$$"
sed -E 's#([ (])/(usr/)?lib/#\1=/\2lib/#g' "$script" > "$tmp"; mv "$tmp" "$LINKER_SCRIPTS/$name"
done < <(find "$SYSROOT/usr/lib" -maxdepth 1 -type f -name '*.so' \
-exec grep -lE '(^|[ (])/(usr/)?lib/' {} +)
export PKG_CONFIG_ALLOW_CROSS=1
export PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig"
export PKG_CONFIG_SYSROOT_DIR="$SYSROOT"
export PKG_CONFIG="$PWD/.cargo/aarch64-pkg-config"
export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
export SOUVERAINE_AARCH64_LINKER_SCRIPTS="$LINKER_SCRIPTS"
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER="$PWD/scripts/aarch64-linker"
export QMAKE="$PWD/scripts/aarch64-qmake"
cargo build --release --target aarch64-unknown-linux-gnu "$@"
BIN="$TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-updater"
# TASK-27 finding 6: the archdev sysroot has silently half-updated before, and a
# cross build against a mixed ABI compiles happily and ships broken. Assert the
# arch, and that every Qt library the binary asks for is actually resolvable in
# the sysroot we built against — a NEEDED with no match there means we linked
# something that will not be present on the phone.
file "$BIN" | grep -q aarch64 || { echo "built binary is not aarch64" >&2; exit 1; }
MISSING=0
while IFS= read -r lib; do
[[ -e "$SYSROOT/usr/lib/$lib" ]] || { echo "NEEDED $lib not in sysroot" >&2; MISSING=1; }
done < <(aarch64-linux-gnu-objdump -p "$BIN" | awk '/NEEDED/{print $2}' | grep '^libQt6')
[[ "$MISSING" -eq 0 ]] || { echo "sysroot does not satisfy the binary's Qt deps" >&2; exit 1; }
echo "== built: $BIN =="; file "$BIN"