Watch
1
0
Fork
You've already forked souveraine
0

split the public gates from the internal packaging half

The three portable jobs stay; the archdev packaging and archive publish move
to packages.yml, which the public branch does not carry. The handoff document
is operator archaeology and does not travel.
This commit is contained in:
Fimeg 2026-08-23 19:42:58 -04:00
commit 76846feb61
4 changed files with 457 additions and 579 deletions

View file

@ -136,448 +136,3 @@ jobs:
# archdev) where the aarch64 sysroot lives — no docker, no node, plain shell.
# Publishes the cross-built binary to a rolling `edge` prerelease; the phone
# updates itself from there instead of waiting on a laptop scp.
aarch64-artifact:
runs-on: archdev
needs: [rust-test, no-ai-attribution, action-pins]
if: github.event_name == 'push' && github.ref == 'refs/heads/primary'
steps:
- name: Clone repo + pinned tuie
run: |
set -euo pipefail
rm -rf "$GITHUB_WORKSPACE/src"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" "$GITHUB_WORKSPACE/src/Souveraine"
git -C "$GITHUB_WORKSPACE/src/Souveraine" checkout "$GITHUB_SHA"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/Fimeg/tuie.git" "$GITHUB_WORKSPACE/src/tuie"
git -C "$GITHUB_WORKSPACE/src/tuie" checkout aa43e335c8f5a3cd2c78e397a1a8dfe534a6bc35
- name: Build release binaries
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Persistent target dirs: fresh clones per run, warm compile cache.
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target"
export SOUVERAINE_AARCH64_SYSROOT="$HOME/aarch64-sysroot"
./scripts/build-cross.sh
BIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine"
file "$BIN" | grep -q aarch64 || { echo "built binary is not aarch64" >&2; exit 1; }
cp "$BIN" souveraine-aarch64
# sessiond is a separate bin behind the sessiond feature. It was
# hand-copied to the phone's /usr/local/bin and owned by no package,
# so it never received updates; build and ship it with the rest.
./scripts/build-cross.sh --features sessiond --bin souveraine-sessiond
SBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sessiond"
file "$SBIN" | grep -q aarch64 || { echo "sessiond is not aarch64" >&2; exit 1; }
cp "$SBIN" souveraine-sessiond-aarch64
# The sensor reporter. Same story as sessiond: it replaces shell
# scripts that shipped in rootfs-overlay/ and drifted per-device.
./scripts/build-cross.sh --bin souveraine-sensord
NBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sensord"
file "$NBIN" | grep -q aarch64 || { echo "sensord is not aarch64" >&2; exit 1; }
cp "$NBIN" souveraine-sensord-aarch64
# Full USB KVM receiver: GUD display-in and the HID return surface.
./scripts/build-cross.sh --features usb-kvm --bin souveraine-usb-kvm
UBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-usb-kvm"
file "$UBIN" | grep -q aarch64 || { echo "usb-kvm is not aarch64" >&2; exit 1; }
cp "$UBIN" souveraine-usb-kvm-aarch64
# The wry host: self-hosted sites as apps, and the rig she wears.
# Needs webkit2gtk on the build sysroot; it is already installed on
# the phone (TASK-59 phase 1 measured against it).
# Best effort: it links against the system webview, so it needs
# webkit2gtk in the aarch64 sysroot. Until that is there this is the
# one binary that may legitimately not build, and it must not stop the
# ones that decide whether the phone locks.
if ./scripts/build-cross.sh --features web --bin souveraine-web; then
WBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-web"
if file "$WBIN" | grep -q aarch64; then
cp "$WBIN" souveraine-web-aarch64
else
echo "::warning::web host built but is not aarch64; skipping it"
fi
else
echo "::warning::the wry host did not cross-build (webkit2gtk sysroot?); skipping it"
fi
# secrets rail + machined: also hand-copied on the phone until now.
./scripts/build-cross.sh --features secrets --bin souveraine-secrets
./scripts/build-cross.sh --bin souveraine-machined
./scripts/build-cross.sh --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/$b"
file "$B" | grep -q aarch64 || { echo "$b is not aarch64" >&2; exit 1; }
cp "$B" "$b-aarch64"
done
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target-x86_64"
cargo build --release
BIN="$CARGO_TARGET_DIR/release/souveraine"
file "$BIN" | grep -q 'x86-64' || { echo "built binary is not x86_64" >&2; exit 1; }
cp "$BIN" souveraine-x86_64
cargo build --release --features secrets --bin souveraine-secrets
cargo build --release --bin souveraine-machined
cargo build --release --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/release/$b"
file "$B" | grep -q 'x86-64' || { echo "$b is not x86_64" >&2; exit 1; }
cp "$B" "$b-x86_64"
done
# sessiond for x86_64 as well. It was skipped here with a comment
# calling it deliberate; it never was (Casey, 2026-08-02) — the
# laptop hit lock-screen errors with it once and it stayed
# phone-only by inertia. That inertia is now load-bearing in the
# wrong direction: viewtop targets the laptop screen too
# (VIEWTOP-AND-DENIAL.md), and panel power, the LockedHint report and
# button reporting all assume the daemon is there — so no session
# authority on x86 means no viewtop on the laptop.
#
# Building and packaging are not enabling. The package-owned user
# unit ships on both architectures, but package installation neither
# enables nor starts it; target activation remains separately tested.
cargo build --release --features sessiond --bin souveraine-sessiond
B="$CARGO_TARGET_DIR/release/souveraine-sessiond"
file "$B" | grep -q 'x86-64' || { echo "sessiond is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sessiond-x86_64
# sensord for x86_64 too. It stopped being phone-only when charge
# became a source: sessiond no longer probes the supplies itself, so
# a machine without the reporter has no charge evidence at all — and
# the laptop is a machine with a battery.
cargo build --release --bin souveraine-sensord
B="$CARGO_TARGET_DIR/release/souveraine-sensord"
file "$B" | grep -q 'x86-64' || { echo "sensord is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sensord-x86_64
sha256sum souveraine-aarch64 souveraine-x86_64 \
souveraine-sessiond-aarch64 souveraine-sensord-aarch64 \
souveraine-usb-kvm-aarch64 \
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
souveraine-machined-aarch64 souveraine-machined-x86_64 \
souveraine-admit-aarch64 souveraine-admit-x86_64 \
souveraine-sessiond-x86_64 souveraine-sensord-x86_64 \
> souveraine-binaries.sha256
- name: Build UPower fork (per-arch install trees)
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# The fork is a git submodule of Souveraine. The runner checkout
# above is not recursive; clone it explicitly with the same LAN
# token used for tuie, pinned to the submodule's recorded commit.
UPOWER_URL=$(git config --file .gitmodules submodule."packaging/upower-souveraine".url)
UPOWER_SHA=$(git ls-tree HEAD packaging/upower-souveraine | awk '{print $3}')
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "$UPOWER_URL" "$GITHUB_WORKSPACE/src/upower"
git -C "$GITHUB_WORKSPACE/src/upower" checkout "$UPOWER_SHA"
SYSROOT="$HOME/aarch64-sysroot"
# .cargo/aarch64-pkg-config reads SOUVERAINE_AARCH64_SYSROOT and falls
# back to /usr/aarch64-linux-gnu, which has no glib. Steps do not share
# environment, so exporting it in the binaries step does not reach here.
export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
# --- x86_64: native build on the archdev host ---
(
cd "$GITHUB_WORKSPACE/src/upower"
# gtkdoc-scan is not installed on the runner, so gtk-doc=true fails
# here too; man needs the same toolchain. Introspection is left ON
# for the native build — it works, and costs nothing.
meson setup build-x86_64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false
meson compile -C build-x86_64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-x86_64" meson install -C build-x86_64
)
tar -C "$GITHUB_WORKSPACE/src/upower/dest-x86_64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-x86_64.tar" .
# --- aarch64: cross build against the same sysroot souveraine uses ---
CROSS="$GITHUB_WORKSPACE/src/upower/aarch64-cross.ini"
cat > "$CROSS" <<EOF
[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkgconfig = '$PWD/.cargo/aarch64-pkg-config'
# gobject-introspection is on by default and RUNS the aarch64 binary
# it just built (g-ir-scanner). qemu needs -L to find the aarch64
# loader; binfmt is registered on the Proxmox host so this works
# inside the unprivileged archdev container.
exe_wrapper = ['qemu-aarch64-static', '-L', '$SYSROOT']
[built-in options]
c_args = ['--sysroot=$SYSROOT']
cpp_args = ['--sysroot=$SYSROOT']
# -L as well as --sysroot: libc.so/libbsd.so in the sysroot are linker
# SCRIPTS naming bare paths (libm, libmvec, -lmd), which ld resolves
# against its own prefix and misses without an explicit search dir.
c_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
cpp_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
EOF
(
cd "$GITHUB_WORKSPACE/src/upower"
PKG_CONFIG_ALLOW_CROSS=1 \
PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig" \
PKG_CONFIG_SYSROOT_DIR="$SYSROOT" \
# gtk-doc/man need host doc tooling, and introspection needs a
# g-ir-scanner that can scan an aarch64 build (the host copy reads
# its data files out of the sysroot and fails). None of the three
# ship anything the phone uses — quickshell's UPower module is
# native Qt, and the device carries no UPowerGlib typelib.
meson setup build-aarch64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false -Dintrospection=disabled \
--cross-file "$CROSS"
meson compile -C build-aarch64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-aarch64" meson install -C build-aarch64
)
# Sanity: the aarch64 upowerd must actually be aarch64.
# It installs to libexec/, not lib/ — the old path made this check
# fail even on a good build.
file "$GITHUB_WORKSPACE/src/upower/dest-aarch64/usr/libexec/upowerd" \
| grep -q aarch64 || { echo "aarch64 upowerd is not aarch64" >&2; exit 1; }
# Cross-install leak. upower asks pkg-config for the udev dir, and
# with a sysroot in play that answer comes back sysroot-prefixed, so
# the rules and hwdb install under $DESTDIR/home/casey/aarch64-sysroot/
# instead of /usr/lib/udev. systemdsystemunitdir is passed explicitly
# above for exactly this reason; the udev dir was missed. Measured on
# the phone 2026-07-27: all four of 60-upower-battery.{rules,hwdb},
# 95-upower-wup.rules and 95-upower-hid.hwdb were absent from
# /usr/lib/udev and present only under the junk path — so the fork's
# udev quirks had never once been applied on the device.
# Fold any leaked tree back before the tar, then refuse to ship a
# tree containing anything but the three prefixes upower legitimately
# installs into: usr/, etc/ (UPower.conf) and var/ (var/lib/upower).
# Leaving var/ out of that list is what failed run 1234.
DEST="$GITHUB_WORKSPACE/src/upower/dest-aarch64"
LEAK="$DEST$HOME/aarch64-sysroot"
if [ -d "$LEAK" ]; then
echo "cross install leaked into $HOME/aarch64-sysroot — relocating"
cp -a "$LEAK/." "$DEST/"
rm -rf "$LEAK"
# Removing the sysroot subtree leaves its empty parents behind
# ($DEST/home/casey), and an empty directory still ships AND still
# trips the stray check below — which is how runs 1234 and 1248
# failed. The PKGBUILD copy of this had the rmdir; this one did not.
rmdir -p --ignore-fail-on-non-empty "$(dirname "$LEAK")" 2>/dev/null || true
fi
stray=$(find "$DEST" -mindepth 1 -maxdepth 1 \
! -name usr ! -name etc ! -name var -printf '%f\n')
if [ -n "$stray" ]; then
echo "upower tree would ship outside usr/, etc/ and var/: $stray" >&2
exit 1
fi
# And the rules must have landed where udev actually reads them.
for f in usr/lib/udev/rules.d/60-upower-battery.rules \
usr/lib/udev/hwdb.d/60-upower-battery.hwdb; do
[ -e "$DEST/$f" ] || { echo "upower tree is missing $f" >&2; exit 1; }
done
tar -C "$GITHUB_WORKSPACE/src/upower/dest-aarch64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-aarch64.tar" .
- name: Stage the shell surface for packaging
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# deploy.sh composes ~/.config/quickshell/souveraine from repo files
# (symlinks) plus ii dirs borrowed from the ii-base pin. Run it in a
# scratch HOME and tar the result dereferenced, so the package is
# self-contained real files and never ships dangling links.
SHELL_STAGE="$GITHUB_WORKSPACE/shell-stage"
SHELL_TAR="$GITHUB_WORKSPACE/shell-package-input"
rm -rf "$SHELL_STAGE" "$SHELL_TAR"
mkdir -p "$SHELL_STAGE" "$SHELL_TAR"
HOME="$SHELL_STAGE" ./surfaces/quickshell/deploy.sh
test -L "$SHELL_STAGE/.config/quickshell/souveraine/shell.qml"
tar -h --zstd -C "$SHELL_STAGE/.config/quickshell" -cf "$SHELL_TAR/souveraine-shell-config.tar.zst" souveraine
tar -tf "$SHELL_TAR/souveraine-shell-config.tar.zst" | grep -qx 'souveraine/shell.qml'
- name: Package and sign pacman repository
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
PKGVER="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
WORK="$GITHUB_WORKSPACE/pacman-package"
REPO="$GITHUB_WORKSPACE/pacman-repo"
ARCHIVE_KEY="3CD9E99E222C2A174986FC9AFF4949AA20C8E911"
rm -rf "$WORK" "$REPO"
mkdir -p "$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-$ARCH" "$PKG_WORK/souveraine-binary"
cp "souveraine-secrets-$ARCH" "$PKG_WORK/souveraine-secrets-binary"
cp "souveraine-machined-$ARCH" "$PKG_WORK/souveraine-machined-binary"
cp "souveraine-admit-$ARCH" "$PKG_WORK/souveraine-admit-binary"
cp "souveraine-sessiond-$ARCH" "$PKG_WORK/souveraine-sessiond-binary"
cp "souveraine-sensord-$ARCH" "$PKG_WORK/souveraine-sensord-binary"
cp packaging/souveraine.service packaging/souveraine-secrets.service \
packaging/org.freedesktop.secrets.service \
packaging/souveraine-machined.service \
packaging/souveraine-sessiond.service \
packaging/souveraine-sensord.service LICENSE "$PKG_WORK/"
cp packaging/arch/souveraine.sysusers "$PKG_WORK/"
# Phone-only reporters and surfaces remain aarch64-only.
if [ "$ARCH" = aarch64 ]; then
cp "souveraine-usb-kvm-$ARCH" "$PKG_WORK/souveraine-usb-kvm-binary"
# The wry host. Guarded, and the PKGBUILD source line is removed
# when it is absent: it needs webkit2gtk in the aarch64 sysroot,
# and a missing avatar must not stop sessiond reaching the phone.
# makepkg validates every entry in `source`, so leaving a name
# there with no file fails the whole package.
if [ -f "souveraine-web-$ARCH" ]; then
cp "souveraine-web-$ARCH" "$PKG_WORK/souveraine-web-binary"
fi
fi
cp packaging/souveraine-verify-trail "$PKG_WORK/"
cp packaging/souveraine-button "$PKG_WORK/"
cp packaging/souveraine-stepup.pam packaging/souveraine-sessiond.pam \
packaging/org.souveraine.stepup.policy "$PKG_WORK/"
cp packaging/arch/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-*.pkg.tar.zst' -print -quit)
test -n "$PKG"
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/systemd/user/souveraine-sessiond.service'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sensord'
bsdtar -tf "$PKG" | grep -qx 'etc/pam.d/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/sysusers.d/souveraine.conf'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-admit'
if [ "$ARCH" = aarch64 ]; then
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-usb-kvm'
fi
cp "$PKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$PKG")"
# UPower fork package — built from packaging/upower-souveraine
# (git submodule of the Souveraine fork of upstream upower). Built
# natively per-arch by the "Build UPower fork" step above into
# upower-tree-$ARCH.tar, then folded into the SAME per-arch pacman
# database so the phone installs upower-souveraine from the same
# edge archive it already uses for souveraine.
# Version from UPSTREAM upower + the fork commit, NOT souveraine's
# r-count. The hand-built package on the phone was 1.91.3+<sha>;
# reusing souveraine's 0.1.rN here sorts LOWER, so pacman treats
# the repo copy as a downgrade and -Syu silently skips it.
UPOWER_VER=$(sed -nE "s/^ *version *: *'([^']+)'.*/\1/p" \
"$GITHUB_WORKSPACE/src/upower/meson.build" | head -1)
# '.r' not '+': a literal + in a release asset filename cannot be
# fetched from Gitea at all — it is stored decoded as a space, and
# both the raw + and %2B forms 404. Still sorts above the old
# hand-built 1.91.3+<sha> because vercmp reads + as a separator too.
UPOWER_PKGVER="${UPOWER_VER}.r$(git -C "$GITHUB_WORKSPACE/src/upower" rev-parse --short HEAD)"
UPOWER_WORK="$WORK/upower-$ARCH"
mkdir -p "$UPOWER_WORK"
cp "upower-tree-$ARCH.tar" "$UPOWER_WORK/upower-tree.tar"
cp packaging/upower-souveraine/dbus/org.freedesktop.UPower.Device.xml \
"$UPOWER_WORK/" 2>/dev/null \
|| cp "$GITHUB_WORKSPACE/src/upower/dbus/org.freedesktop.UPower.Device.xml" \
"$UPOWER_WORK/"
cp packaging/arch/PKGBUILD.upower.prebuilt "$UPOWER_WORK/PKGBUILD"
(
cd "$UPOWER_WORK"
export SOUVERAINE_PKGVER="$UPOWER_PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
UPKG=$(find "$UPOWER_WORK" -maxdepth 1 -name 'upower-souveraine-*.pkg.tar.zst' -print -quit)
test -n "$UPKG"
# libexec/, not lib/ — same path correction as the sanity check above.
bsdtar -tf "$UPKG" | grep -qx 'usr/libexec/upowerd'
cp "$UPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$UPKG")"
# Shell package — the composed quickshell config, arch=any, from
# the staged tar. Same per-arch database as the binaries above.
SHELL_WORK="$WORK/shell-$ARCH"
mkdir -p "$SHELL_WORK"
cp "$GITHUB_WORKSPACE/shell-package-input/souveraine-shell-config.tar.zst" "$SHELL_WORK/"
cp packaging/arch/PKGBUILD.shell.prebuilt "$SHELL_WORK/PKGBUILD"
cp LICENSE "$SHELL_WORK/"
(
cd "$SHELL_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
SPKG=$(find "$SHELL_WORK" -maxdepth 1 -name 'souveraine-shell-*.pkg.tar.zst' -print -quit)
test -n "$SPKG"
bsdtar -tf "$SPKG" | grep -qx 'etc/xdg/quickshell/souveraine/shell.qml'
bsdtar -tf "$SPKG" | grep -qx 'usr/share/licenses/souveraine-shell/LICENSE'
cp "$SPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$SPKG")"
# The per-arch database is NOT built here. `edge` is a shared archive
# with more than one producer in it, so the database has to be
# read-modify-written against the live copy rather than generated
# fresh — packaging/arch/publish-edge.sh owns that, under a lock.
done
cp packaging/arch/souveraine-archive-key.asc "$REPO/"
- name: Publish rolling edge prerelease
env:
# A PAT rather than the job's own GITHUB_TOKEN, so the identical
# invocation works from a producer repo other than this one.
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
ARCHIVE_KEY: 3CD9E99E222C2A174986FC9AFF4949AA20C8E911
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Additive: the release and tag are never deleted, the live databases
# are merged into, and only this producer's own superseded packages
# are removed. Before 2026-07-25 this step deleted and recreated the
# release, which erased every other producer's packages on each push.
export EDGE_TARGET_SHA="$GITHUB_SHA"
export PRODUCER_VERSION="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
./packaging/arch/publish-edge.sh souveraine "$GITHUB_WORKSPACE/pacman-repo" \
souveraine-aarch64 souveraine-x86_64 souveraine-binaries.sha256 \
"$GITHUB_WORKSPACE/pacman-repo/souveraine-archive-key.asc"
- name: Mirror into the Gitea Arch registry (alongside edge)
# Best-effort, never fails the publish: the registry sits alongside
# release-assets until proven as the phone's source. A 409 means
# (name, version) is already present immutably -- bump to republish.
# Device-side auth is handled by souveraine-pacman-fetch; this only
# uploads. Covers souveraine + upower-souveraine (both arches).
env:
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
run: |
set +e
REG="$GITHUB_SERVER_URL/api/packages/${GITHUB_REPOSITORY%/*}/arch/edge"
ok=0; skip=0; bad=0
shopt -s nullglob
for pkg in "$GITHUB_WORKSPACE"/pacman-repo/*/*.pkg.tar.zst; do
code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: token $EDGE_TOKEN" --upload-file "$pkg" "$REG")
case "$code" in
200|201) echo "registry: published $(basename "$pkg")"; ok=$((ok+1));;
409) echo "registry: present $(basename "$pkg") (bump to republish)"; skip=$((skip+1));;
*) echo "::warning::registry $(basename "$pkg") -> HTTP $code"; bad=$((bad+1));;
esac
done
echo "registry mirror: $ok new, $skip present, $bad failed"
exit 0

View file

@ -0,0 +1,456 @@
name: packages
# Internal only. Packaging, signing and archive publication run on the
# build host where the aarch64 sysroot and the signing key live. This file
# is not carried on the public branch; the gates in ci.yml are.
on:
push:
branches: [primary, main]
workflow_dispatch:
jobs:
aarch64-artifact:
runs-on: archdev
needs: [rust-test, no-ai-attribution, action-pins]
if: github.event_name == 'push' && github.ref == 'refs/heads/primary'
steps:
- name: Clone repo + pinned tuie
run: |
set -euo pipefail
rm -rf "$GITHUB_WORKSPACE/src"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" "$GITHUB_WORKSPACE/src/Souveraine"
git -C "$GITHUB_WORKSPACE/src/Souveraine" checkout "$GITHUB_SHA"
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "${GITHUB_SERVER_URL}/Fimeg/tuie.git" "$GITHUB_WORKSPACE/src/tuie"
git -C "$GITHUB_WORKSPACE/src/tuie" checkout aa43e335c8f5a3cd2c78e397a1a8dfe534a6bc35
- name: Build release binaries
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Persistent target dirs: fresh clones per run, warm compile cache.
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target"
export SOUVERAINE_AARCH64_SYSROOT="$HOME/aarch64-sysroot"
./scripts/build-cross.sh
BIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine"
file "$BIN" | grep -q aarch64 || { echo "built binary is not aarch64" >&2; exit 1; }
cp "$BIN" souveraine-aarch64
# sessiond is a separate bin behind the sessiond feature. It was
# hand-copied to the phone's /usr/local/bin and owned by no package,
# so it never received updates; build and ship it with the rest.
./scripts/build-cross.sh --features sessiond --bin souveraine-sessiond
SBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sessiond"
file "$SBIN" | grep -q aarch64 || { echo "sessiond is not aarch64" >&2; exit 1; }
cp "$SBIN" souveraine-sessiond-aarch64
# The sensor reporter. Same story as sessiond: it replaces shell
# scripts that shipped in rootfs-overlay/ and drifted per-device.
./scripts/build-cross.sh --bin souveraine-sensord
NBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-sensord"
file "$NBIN" | grep -q aarch64 || { echo "sensord is not aarch64" >&2; exit 1; }
cp "$NBIN" souveraine-sensord-aarch64
# Full USB KVM receiver: GUD display-in and the HID return surface.
./scripts/build-cross.sh --features usb-kvm --bin souveraine-usb-kvm
UBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-usb-kvm"
file "$UBIN" | grep -q aarch64 || { echo "usb-kvm is not aarch64" >&2; exit 1; }
cp "$UBIN" souveraine-usb-kvm-aarch64
# The wry host: self-hosted sites as apps, and the rig she wears.
# Needs webkit2gtk on the build sysroot; it is already installed on
# the phone (TASK-59 phase 1 measured against it).
# Best effort: it links against the system webview, so it needs
# webkit2gtk in the aarch64 sysroot. Until that is there this is the
# one binary that may legitimately not build, and it must not stop the
# ones that decide whether the phone locks.
if ./scripts/build-cross.sh --features web --bin souveraine-web; then
WBIN="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/souveraine-web"
if file "$WBIN" | grep -q aarch64; then
cp "$WBIN" souveraine-web-aarch64
else
echo "::warning::web host built but is not aarch64; skipping it"
fi
else
echo "::warning::the wry host did not cross-build (webkit2gtk sysroot?); skipping it"
fi
# secrets rail + machined: also hand-copied on the phone until now.
./scripts/build-cross.sh --features secrets --bin souveraine-secrets
./scripts/build-cross.sh --bin souveraine-machined
./scripts/build-cross.sh --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/aarch64-unknown-linux-gnu/release/$b"
file "$B" | grep -q aarch64 || { echo "$b is not aarch64" >&2; exit 1; }
cp "$B" "$b-aarch64"
done
export CARGO_TARGET_DIR="$HOME/.cache/souveraine-ci-target-x86_64"
cargo build --release
BIN="$CARGO_TARGET_DIR/release/souveraine"
file "$BIN" | grep -q 'x86-64' || { echo "built binary is not x86_64" >&2; exit 1; }
cp "$BIN" souveraine-x86_64
cargo build --release --features secrets --bin souveraine-secrets
cargo build --release --bin souveraine-machined
cargo build --release --bin souveraine-admit
for b in souveraine-secrets souveraine-machined souveraine-admit; do
B="$CARGO_TARGET_DIR/release/$b"
file "$B" | grep -q 'x86-64' || { echo "$b is not x86_64" >&2; exit 1; }
cp "$B" "$b-x86_64"
done
# sessiond for x86_64 as well. It was skipped here with a comment
# calling it deliberate; it never was (Casey, 2026-08-02) — the
# laptop hit lock-screen errors with it once and it stayed
# phone-only by inertia. That inertia is now load-bearing in the
# wrong direction: viewtop targets the laptop screen too
# (VIEWTOP-AND-DENIAL.md), and panel power, the LockedHint report and
# button reporting all assume the daemon is there — so no session
# authority on x86 means no viewtop on the laptop.
#
# Building and packaging are not enabling. The package-owned user
# unit ships on both architectures, but package installation neither
# enables nor starts it; target activation remains separately tested.
cargo build --release --features sessiond --bin souveraine-sessiond
B="$CARGO_TARGET_DIR/release/souveraine-sessiond"
file "$B" | grep -q 'x86-64' || { echo "sessiond is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sessiond-x86_64
# sensord for x86_64 too. It stopped being phone-only when charge
# became a source: sessiond no longer probes the supplies itself, so
# a machine without the reporter has no charge evidence at all — and
# the laptop is a machine with a battery.
cargo build --release --bin souveraine-sensord
B="$CARGO_TARGET_DIR/release/souveraine-sensord"
file "$B" | grep -q 'x86-64' || { echo "sensord is not x86_64" >&2; exit 1; }
cp "$B" souveraine-sensord-x86_64
sha256sum souveraine-aarch64 souveraine-x86_64 \
souveraine-sessiond-aarch64 souveraine-sensord-aarch64 \
souveraine-usb-kvm-aarch64 \
souveraine-secrets-aarch64 souveraine-secrets-x86_64 \
souveraine-machined-aarch64 souveraine-machined-x86_64 \
souveraine-admit-aarch64 souveraine-admit-x86_64 \
souveraine-sessiond-x86_64 souveraine-sensord-x86_64 \
> souveraine-binaries.sha256
- name: Build UPower fork (per-arch install trees)
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# The fork is a git submodule of Souveraine. The runner checkout
# above is not recursive; clone it explicitly with the same LAN
# token used for tuie, pinned to the submodule's recorded commit.
UPOWER_URL=$(git config --file .gitmodules submodule."packaging/upower-souveraine".url)
UPOWER_SHA=$(git ls-tree HEAD packaging/upower-souveraine | awk '{print $3}')
git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
clone "$UPOWER_URL" "$GITHUB_WORKSPACE/src/upower"
git -C "$GITHUB_WORKSPACE/src/upower" checkout "$UPOWER_SHA"
SYSROOT="$HOME/aarch64-sysroot"
# .cargo/aarch64-pkg-config reads SOUVERAINE_AARCH64_SYSROOT and falls
# back to /usr/aarch64-linux-gnu, which has no glib. Steps do not share
# environment, so exporting it in the binaries step does not reach here.
export SOUVERAINE_AARCH64_SYSROOT="$SYSROOT"
# --- x86_64: native build on the archdev host ---
(
cd "$GITHUB_WORKSPACE/src/upower"
# gtkdoc-scan is not installed on the runner, so gtk-doc=true fails
# here too; man needs the same toolchain. Introspection is left ON
# for the native build — it works, and costs nothing.
meson setup build-x86_64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false
meson compile -C build-x86_64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-x86_64" meson install -C build-x86_64
)
tar -C "$GITHUB_WORKSPACE/src/upower/dest-x86_64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-x86_64.tar" .
# --- aarch64: cross build against the same sysroot souveraine uses ---
CROSS="$GITHUB_WORKSPACE/src/upower/aarch64-cross.ini"
cat > "$CROSS" <<EOF
[binaries]
c = 'aarch64-linux-gnu-gcc'
cpp = 'aarch64-linux-gnu-g++'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkgconfig = '$PWD/.cargo/aarch64-pkg-config'
# gobject-introspection is on by default and RUNS the aarch64 binary
# it just built (g-ir-scanner). qemu needs -L to find the aarch64
# loader; binfmt is registered on the Proxmox host so this works
# inside the unprivileged archdev container.
exe_wrapper = ['qemu-aarch64-static', '-L', '$SYSROOT']
[built-in options]
c_args = ['--sysroot=$SYSROOT']
cpp_args = ['--sysroot=$SYSROOT']
# -L as well as --sysroot: libc.so/libbsd.so in the sysroot are linker
# SCRIPTS naming bare paths (libm, libmvec, -lmd), which ld resolves
# against its own prefix and misses without an explicit search dir.
c_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
cpp_link_args = ['--sysroot=$SYSROOT', '-L$SYSROOT/usr/lib']
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
EOF
(
cd "$GITHUB_WORKSPACE/src/upower"
PKG_CONFIG_ALLOW_CROSS=1 \
PKG_CONFIG_LIBDIR="$SYSROOT/usr/lib/pkgconfig" \
PKG_CONFIG_SYSROOT_DIR="$SYSROOT" \
# gtk-doc/man need host doc tooling, and introspection needs a
# g-ir-scanner that can scan an aarch64 build (the host copy reads
# its data files out of the sysroot and fails). None of the three
# ship anything the phone uses — quickshell's UPower module is
# native Qt, and the device carries no UPowerGlib typelib.
meson setup build-aarch64 \
--prefix=/usr --sysconfdir=/etc --localstatedir=/var \
-Dsystemdsystemunitdir=/usr/lib/systemd/system \
-Dgtk-doc=false -Dman=false -Dintrospection=disabled \
--cross-file "$CROSS"
meson compile -C build-aarch64
DESTDIR="$GITHUB_WORKSPACE/src/upower/dest-aarch64" meson install -C build-aarch64
)
# Sanity: the aarch64 upowerd must actually be aarch64.
# It installs to libexec/, not lib/ — the old path made this check
# fail even on a good build.
file "$GITHUB_WORKSPACE/src/upower/dest-aarch64/usr/libexec/upowerd" \
| grep -q aarch64 || { echo "aarch64 upowerd is not aarch64" >&2; exit 1; }
# Cross-install leak. upower asks pkg-config for the udev dir, and
# with a sysroot in play that answer comes back sysroot-prefixed, so
# the rules and hwdb install under $DESTDIR/home/casey/aarch64-sysroot/
# instead of /usr/lib/udev. systemdsystemunitdir is passed explicitly
# above for exactly this reason; the udev dir was missed. Measured on
# the phone 2026-07-27: all four of 60-upower-battery.{rules,hwdb},
# 95-upower-wup.rules and 95-upower-hid.hwdb were absent from
# /usr/lib/udev and present only under the junk path — so the fork's
# udev quirks had never once been applied on the device.
# Fold any leaked tree back before the tar, then refuse to ship a
# tree containing anything but the three prefixes upower legitimately
# installs into: usr/, etc/ (UPower.conf) and var/ (var/lib/upower).
# Leaving var/ out of that list is what failed run 1234.
DEST="$GITHUB_WORKSPACE/src/upower/dest-aarch64"
LEAK="$DEST$HOME/aarch64-sysroot"
if [ -d "$LEAK" ]; then
echo "cross install leaked into $HOME/aarch64-sysroot — relocating"
cp -a "$LEAK/." "$DEST/"
rm -rf "$LEAK"
# Removing the sysroot subtree leaves its empty parents behind
# ($DEST/home/casey), and an empty directory still ships AND still
# trips the stray check below — which is how runs 1234 and 1248
# failed. The PKGBUILD copy of this had the rmdir; this one did not.
rmdir -p --ignore-fail-on-non-empty "$(dirname "$LEAK")" 2>/dev/null || true
fi
stray=$(find "$DEST" -mindepth 1 -maxdepth 1 \
! -name usr ! -name etc ! -name var -printf '%f\n')
if [ -n "$stray" ]; then
echo "upower tree would ship outside usr/, etc/ and var/: $stray" >&2
exit 1
fi
# And the rules must have landed where udev actually reads them.
for f in usr/lib/udev/rules.d/60-upower-battery.rules \
usr/lib/udev/hwdb.d/60-upower-battery.hwdb; do
[ -e "$DEST/$f" ] || { echo "upower tree is missing $f" >&2; exit 1; }
done
tar -C "$GITHUB_WORKSPACE/src/upower/dest-aarch64" \
-cf "$GITHUB_WORKSPACE/src/Souveraine/upower-tree-aarch64.tar" .
- name: Stage the shell surface for packaging
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# deploy.sh composes ~/.config/quickshell/souveraine from repo files
# (symlinks) plus ii dirs borrowed from the ii-base pin. Run it in a
# scratch HOME and tar the result dereferenced, so the package is
# self-contained real files and never ships dangling links.
SHELL_STAGE="$GITHUB_WORKSPACE/shell-stage"
SHELL_TAR="$GITHUB_WORKSPACE/shell-package-input"
rm -rf "$SHELL_STAGE" "$SHELL_TAR"
mkdir -p "$SHELL_STAGE" "$SHELL_TAR"
HOME="$SHELL_STAGE" ./surfaces/quickshell/deploy.sh
test -L "$SHELL_STAGE/.config/quickshell/souveraine/shell.qml"
tar -h --zstd -C "$SHELL_STAGE/.config/quickshell" -cf "$SHELL_TAR/souveraine-shell-config.tar.zst" souveraine
tar -tf "$SHELL_TAR/souveraine-shell-config.tar.zst" | grep -qx 'souveraine/shell.qml'
- name: Package and sign pacman repository
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
PKGVER="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
WORK="$GITHUB_WORKSPACE/pacman-package"
REPO="$GITHUB_WORKSPACE/pacman-repo"
ARCHIVE_KEY="3CD9E99E222C2A174986FC9AFF4949AA20C8E911"
rm -rf "$WORK" "$REPO"
mkdir -p "$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-$ARCH" "$PKG_WORK/souveraine-binary"
cp "souveraine-secrets-$ARCH" "$PKG_WORK/souveraine-secrets-binary"
cp "souveraine-machined-$ARCH" "$PKG_WORK/souveraine-machined-binary"
cp "souveraine-admit-$ARCH" "$PKG_WORK/souveraine-admit-binary"
cp "souveraine-sessiond-$ARCH" "$PKG_WORK/souveraine-sessiond-binary"
cp "souveraine-sensord-$ARCH" "$PKG_WORK/souveraine-sensord-binary"
cp packaging/souveraine.service packaging/souveraine-secrets.service \
packaging/org.freedesktop.secrets.service \
packaging/souveraine-machined.service \
packaging/souveraine-sessiond.service \
packaging/souveraine-sensord.service LICENSE "$PKG_WORK/"
cp packaging/arch/souveraine.sysusers "$PKG_WORK/"
# Phone-only reporters and surfaces remain aarch64-only.
if [ "$ARCH" = aarch64 ]; then
cp "souveraine-usb-kvm-$ARCH" "$PKG_WORK/souveraine-usb-kvm-binary"
# The wry host. Guarded, and the PKGBUILD source line is removed
# when it is absent: it needs webkit2gtk in the aarch64 sysroot,
# and a missing avatar must not stop sessiond reaching the phone.
# makepkg validates every entry in `source`, so leaving a name
# there with no file fails the whole package.
if [ -f "souveraine-web-$ARCH" ]; then
cp "souveraine-web-$ARCH" "$PKG_WORK/souveraine-web-binary"
fi
fi
cp packaging/souveraine-verify-trail "$PKG_WORK/"
cp packaging/souveraine-button "$PKG_WORK/"
cp packaging/souveraine-stepup.pam packaging/souveraine-sessiond.pam \
packaging/org.souveraine.stepup.policy "$PKG_WORK/"
cp packaging/arch/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-*.pkg.tar.zst' -print -quit)
test -n "$PKG"
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/systemd/user/souveraine-sessiond.service'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-sensord'
bsdtar -tf "$PKG" | grep -qx 'etc/pam.d/souveraine-sessiond'
bsdtar -tf "$PKG" | grep -qx 'usr/lib/sysusers.d/souveraine.conf'
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-admit'
if [ "$ARCH" = aarch64 ]; then
bsdtar -tf "$PKG" | grep -qx 'usr/bin/souveraine-usb-kvm'
fi
cp "$PKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$PKG")"
# UPower fork package — built from packaging/upower-souveraine
# (git submodule of the Souveraine fork of upstream upower). Built
# natively per-arch by the "Build UPower fork" step above into
# upower-tree-$ARCH.tar, then folded into the SAME per-arch pacman
# database so the phone installs upower-souveraine from the same
# edge archive it already uses for souveraine.
# Version from UPSTREAM upower + the fork commit, NOT souveraine's
# r-count. The hand-built package on the phone was 1.91.3+<sha>;
# reusing souveraine's 0.1.rN here sorts LOWER, so pacman treats
# the repo copy as a downgrade and -Syu silently skips it.
UPOWER_VER=$(sed -nE "s/^ *version *: *'([^']+)'.*/\1/p" \
"$GITHUB_WORKSPACE/src/upower/meson.build" | head -1)
# '.r' not '+': a literal + in a release asset filename cannot be
# fetched from Gitea at all — it is stored decoded as a space, and
# both the raw + and %2B forms 404. Still sorts above the old
# hand-built 1.91.3+<sha> because vercmp reads + as a separator too.
UPOWER_PKGVER="${UPOWER_VER}.r$(git -C "$GITHUB_WORKSPACE/src/upower" rev-parse --short HEAD)"
UPOWER_WORK="$WORK/upower-$ARCH"
mkdir -p "$UPOWER_WORK"
cp "upower-tree-$ARCH.tar" "$UPOWER_WORK/upower-tree.tar"
cp packaging/upower-souveraine/dbus/org.freedesktop.UPower.Device.xml \
"$UPOWER_WORK/" 2>/dev/null \
|| cp "$GITHUB_WORKSPACE/src/upower/dbus/org.freedesktop.UPower.Device.xml" \
"$UPOWER_WORK/"
cp packaging/arch/PKGBUILD.upower.prebuilt "$UPOWER_WORK/PKGBUILD"
(
cd "$UPOWER_WORK"
export SOUVERAINE_PKGVER="$UPOWER_PKGVER"
export SOUVERAINE_PKGARCH="$ARCH"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
UPKG=$(find "$UPOWER_WORK" -maxdepth 1 -name 'upower-souveraine-*.pkg.tar.zst' -print -quit)
test -n "$UPKG"
# libexec/, not lib/ — same path correction as the sanity check above.
bsdtar -tf "$UPKG" | grep -qx 'usr/libexec/upowerd'
cp "$UPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$UPKG")"
# Shell package — the composed quickshell config, arch=any, from
# the staged tar. Same per-arch database as the binaries above.
SHELL_WORK="$WORK/shell-$ARCH"
mkdir -p "$SHELL_WORK"
cp "$GITHUB_WORKSPACE/shell-package-input/souveraine-shell-config.tar.zst" "$SHELL_WORK/"
cp packaging/arch/PKGBUILD.shell.prebuilt "$SHELL_WORK/PKGBUILD"
cp LICENSE "$SHELL_WORK/"
(
cd "$SHELL_WORK"
export SOUVERAINE_PKGVER="$PKGVER"
CARCH="$ARCH" makepkg --nodeps --noconfirm --cleanbuild
)
SPKG=$(find "$SHELL_WORK" -maxdepth 1 -name 'souveraine-shell-*.pkg.tar.zst' -print -quit)
test -n "$SPKG"
bsdtar -tf "$SPKG" | grep -qx 'etc/xdg/quickshell/souveraine/shell.qml'
bsdtar -tf "$SPKG" | grep -qx 'usr/share/licenses/souveraine-shell/LICENSE'
cp "$SPKG" "$ARCH_REPO/"
gpg --batch --yes --local-user "$ARCHIVE_KEY" \
--detach-sign "$ARCH_REPO/$(basename "$SPKG")"
# The per-arch database is NOT built here. `edge` is a shared archive
# with more than one producer in it, so the database has to be
# read-modify-written against the live copy rather than generated
# fresh — packaging/arch/publish-edge.sh owns that, under a lock.
done
cp packaging/arch/souveraine-archive-key.asc "$REPO/"
- name: Publish rolling edge prerelease
env:
# A PAT rather than the job's own GITHUB_TOKEN, so the identical
# invocation works from a producer repo other than this one.
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
ARCHIVE_KEY: 3CD9E99E222C2A174986FC9AFF4949AA20C8E911
run: |
set -euo pipefail
cd "$GITHUB_WORKSPACE/src/Souveraine"
# Additive: the release and tag are never deleted, the live databases
# are merged into, and only this producer's own superseded packages
# are removed. Before 2026-07-25 this step deleted and recreated the
# release, which erased every other producer's packages on each push.
export EDGE_TARGET_SHA="$GITHUB_SHA"
export PRODUCER_VERSION="0.1.r$(git rev-list --count HEAD).g${GITHUB_SHA:0:12}"
./packaging/arch/publish-edge.sh souveraine "$GITHUB_WORKSPACE/pacman-repo" \
souveraine-aarch64 souveraine-x86_64 souveraine-binaries.sha256 \
"$GITHUB_WORKSPACE/pacman-repo/souveraine-archive-key.asc"
- name: Mirror into the Gitea Arch registry (alongside edge)
# Best-effort, never fails the publish: the registry sits alongside
# release-assets until proven as the phone's source. A 409 means
# (name, version) is already present immutably -- bump to republish.
# Device-side auth is handled by souveraine-pacman-fetch; this only
# uploads. Covers souveraine + upower-souveraine (both arches).
env:
EDGE_TOKEN: ${{ secrets.LOCAL_GITEA_TOKEN }}
run: |
set +e
REG="$GITHUB_SERVER_URL/api/packages/${GITHUB_REPOSITORY%/*}/arch/edge"
ok=0; skip=0; bad=0
shopt -s nullglob
for pkg in "$GITHUB_WORKSPACE"/pacman-repo/*/*.pkg.tar.zst; do
code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
-H "Authorization: token $EDGE_TOKEN" --upload-file "$pkg" "$REG")
case "$code" in
200|201) echo "registry: published $(basename "$pkg")"; ok=$((ok+1));;
409) echo "registry: present $(basename "$pkg") (bump to republish)"; skip=$((skip+1));;
*) echo "::warning::registry $(basename "$pkg") -> HTTP $code"; bad=$((bad+1));;
esac
done
echo "registry mirror: $ok new, $skip present, $bad failed"
exit 0

View file

@ -21,7 +21,7 @@ The personal agreement is `~/.codex/AGENTS.md`. The cross-repo map is
- Gitea Actions is the reproducible build/test/package path. Push the scoped
change and let CI build it.
- Keep local checks small and targeted. Do not replace the pipeline with a
direct ArchDev build, an offline build, or a hand-copied artifact.
a direct build on the build host, an offline build, or a hand-copied artifact.
- Runner access is diagnostic. A green result matters only if the relevant job
ran and, when required, published the expected package.
- Phone delivery is through package ownership and pacman. A symlink or copied

View file

@ -1,133 +0,0 @@
# Handoff — 2026-08-07, the day her face went on the glass
Everything below is pushed. Nothing is half-applied. The one thing that is
**not** durable is the phone's binaries — see "The phone is running test
installs" at the end, and fix that first if a `-Syu` is coming.
## Read these before touching any of it
`docs/tasks/59-her-face-on-the-glass.md` (rewritten today — the five bugs in
the way, and why each presented as something else), `docs/tasks/60-the-
multitasking-view.md`, `docs/DUMP-bugs-2026-08-06.md` (items 3/4/5 closed, 7
retracted, 8/9/10 new).
## What is on the phone and working
She draws. She is **furniture on home**, not an app: no zone, no dock entry, no
overview card, no frame, her own size, hung from the top of the zone with the
room below deliberately left free for what she shares. She takes touch only on
her own silhouette, so the home screen underneath still works. She follows a
finger **anywhere on the panel**. Holding her records and sends through the
existing STT into the sidebar. Double-tapping her dismisses her; locking the
phone sends her away and she stays away. The clock and the quote fade out when
she is up.
Also landed: multitasking cards draw; zones mint correctly; the dial records
the screen on a hold; layer-surface popups exist at all; move mode can be left;
the dock is bigger and its scale is a setting.
## The five that cost the most, kept short
1. **The rig drew nothing because of one missing DOM element.** `live2d.js` is
the reference's whole bundle, not just Cubism, and it reads `#live_talk`
every frame. Missing, it throws on the first update — after the model and
all four textures have loaded. Everything else reads healthy.
2. **`--app-id` was a no-op.** `set_prgname` after GTK made the surface. The
compositor saw `souveraine-web`; window rules could only match the title.
3. **Claim the GL context *after* sizing the canvas.** Before it, the context is
born 300x150, `gl.viewport` stays there, and she renders as a thumbnail in
the bottom-left corner of a full-size buffer.
4. **Multitasking cards were `width: 0`** — a Column↔loader binding cycle with
no search widget to break it. Every other signal read healthy.
5. **`Feeds::publish` dropped subscribers whose channel was `Full`**, so a fast
stream unsubscribed its own reader inside the first burst. An external probe
that read faster kept working, which is what made it look like a shell bug.
## Traps this session re-learned the hard way
- **`pgrep -f` / `pkill -f` match your own command string.** Killed the SSH
session and a kitty more than once. Bracket the pattern *and* make sure the
unbracketed form does not also appear later in the same command.
- **A duplicate signal handler fails the whole QML module** and takes the shell
into a restart loop. `onCanceled` already existed on the rail; adding a second
is `Property value set multiple times`.
- **Quickshell's `Socket` emits `onConnectionStateChanged`,** not
`onConnectedChanged`. A handler for the wrong one is silent.
- **Check `locked` in viewtop's `state` before diagnosing a capture failure.**
A session left locked by a test refuses every capture, correctly, and it
looks exactly like a broken capture path.
- **`GlobalStates.screenLocked` drifts.** `session lock` answered
`already-locked` on a phone in use while logind said `LockedHint=no`. Gate on
`screenLockSecure`, the compositor's ack.
- **A greetd restart leaves the phone locked** and only the PIN opens it —
`unlock` is deliberately not a verb anything but a human can call. Do not
leave it locked and walk away.
- **`scene` intent `drop`** clears a stuck window grab in one call. It is the
unstick for a phone frozen in move mode. The intent is a bare string
(`{"op":"scene","intent":"drop"}`), not a map.
## What is open, honestly
- **The pill strands a pose** (DUMP 8). My cancel-path theory was **wrong**
`onCanceled` already handles it. Cause unfound. Casey wants no backing-off
state at all: gate the partial slide on speed and length, then commit.
- **The swipe into multitasking is awkward** (TASK-60). Two named suspects, both
needing to be watched in motion.
- **Side panels glitch on the way out** (DUMP 9) — keyboard half fixed, visual
glitch uncharacterised. **The terminal cannot keep its keyboard** (DUMP 10);
the two are mirror images and probably one focus-handoff bug.
- **Dock appstacks** — the popup fix is committed and **unverified on hardware**.
Layer-surface popups were never composited *or* hit-tested, which is why
tapping a stack did nothing. Needs a finger on it.
- **Holding her to talk is the wrong affordance.** Casey: *"holding onto her has
a different connotation."* It works; it should become explicit controls.
- **Her states are wired but thin.** Nine postures collapse onto two levers —
`#live_talk` and the bundle's head/body hit test. That is all the rig has.
- **VPN through the state machine** — Casey's call: its own session, and it
probably wants a settings surface for SSIDs. Do not start it as a side quest,
and remember §12: an `Action`, never a daemon (`wiuf-vpn-gate` was 652
recycles in 90 minutes).
## The rig is the next big thing, and it is a pipeline
Not a repaint. Strip the parts out by name with their exact UV footprints,
rebuild by compositing them back at those footprints, and let agents swap
components the way they swap outfits — the rebuild half is what makes swapping
real. V1 of the *look* is the face only: shrink the eyes, slim the face with
transparency, because Histoire reads as a little girl and not a woman. Wings go
holographic. Outfit is fine for now.
**Ceiling:** parts are paint, silhouette is mesh. Twin-tails and the pose need a
new rig, and that is a Cubism 2 → 4/5 change — a different loader, which also
replaces the character layer that reads `#live_talk`.
Assets: `~/Projects/live2d-editing` (4 × 1024², palette-indexed, convert to RGBA
first). Style reference: `~/Projects/AniAvatar`.
## The phone is running test installs
`/usr/bin/viewtop` and `/usr/bin/souveraine-web` are **hand-installed**, and the
QML lives in `/home/casey/souveraine-surfaces`. Originals parked at
`/usr/local/bin/viewtop.prev-r88-package` and `souveraine-web.prev-r363`. A
`pacman -Syu` reverts the binaries. This is `souveraine/CLAUDE.md` failure mode
#4 and it should be closed by building the packages, not by copying again.
Build loop that works: edit on the laptop → `scp` the changed file to archdev
(`/home/casey/souveraine-viewtop`, `/home/casey/souveraine`) → **checksum it**,
because another session may be in the same tree → `VIEWTOP_AARCH64_SYSROOT=
/home/casey/aarch64-sysroot ./scripts/build-cross.sh --release` → base64 down,
scp up, `install -m755`. `~/souveraine` needs `~/tuie` beside it (symlinked to
`~/Projects/tuie`).
## Cross-repo state
- `souveraine``primary` @ `630d691`
- `souveraine-viewtop``main` @ `62ec16b`
- `SouveraineOS``main` @ (this commit)
- smithay fork → `souveraine` @ `645961c7`, pinned; the `colour-transform`
branch was folded in and **deleted**, and the local `[patch]` that pointed at
`/home/casey/smithay-colour` is gone — the tree builds from the fork now.
- Dead branches removed: `public`, `main`, `detached-reinit-backup`,
`experimental/session-trust-phase1` (souveraine), `public-main`,
`codeberg-publish-tmp` (Pixel3Arch). Nothing of value was stranded on any of
them — all merges and dead codeberg snapshots. `archive/*` refs kept.