Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/25-one-repo-all-packages.md
Fimeg d8820da557 distribution: add ViewTop to the laptop body
Record update delivery as a per-body offer and local candidate activation after a live shell push killed an active conversation.
2026-08-11 14:40:08 -04:00

15 KiB

TASK 25 — One gitea repo for every SouveraineOS package

Status: 2026-07-30 — Pixel3Arch now publishes every device package. packages.yml builds + signs all of them into the additive edge release, so pacman -Sl souveraine-aarch64 is the complete live manifest: 14 packages (souveraine, -callaudio, -q6voiced, -ucm-blueline, -updater, upower-souveraine, pulse-native-provider, souveraine-osk-switch, hexagonrpc-blueline, tqftpserv-blueline, blueline-edge-sense, souveraine-stevia, souveraine-squeekboard, usb-signaller). Two deferred on purpose: hyprgrass (Hyprland plugin, phased out for viewtop) and libssc-souveraine (phone runs upstream libssc; our fork isn't deployed). Goal achieved: push → pacman -Syu on the phone, no hand-carried packages. The kernel (kernel.yml) is the last holdout — it still ships a raw .pkg.tar.zst with no repo-add --sign.

The problem in one line

souveraine's CI is the only pipeline that does the last mile — repo-add --sign into a per-arch pacman database. Every other repo stops at "here is a file," so those packages have to be copied to the phone and pacman -U'd by hand.

Repo CI Publishes pacman -Syu-able
souveraine ci.yml (primary/public) signed pkgs + souveraine-{arch}.db yes
Pixel3Arch kernel.yml (main) raw .pkg.tar.zst, no db, no sig no
firefox-arm build.yml nothing published yet no
culver ci.yml tests only no
souveraine-player none no
upower none — rides souveraine as a submodule (into souveraine's db) yes

upower is the proof the pattern generalises: a separate repo, built by souveraine's CI, folded into the same database. That is the shape everything else should take.

The design — one database, many producers

Do NOT add a fourth pipeline. Make souveraine-{arch}.db the single archive and have every producer publish into it.

  1. Pixel3Arch kernel.yml gains the last mile. It already builds a valid package on the archdev runner and asserts the built hash against the gitea tip. Add the repo-add --include-sigs --sign --key "$ARCHIVE_KEY" step and publish into the shared edge release — souveraine/.gitea/workflows/ci.yml lines ~248-256 are the reference implementation. Kernel updates then arrive by pacman -Syu; flashing becomes a recovery tool, not a release channel.

    repo-add --sign is NOT the first blocker (verified 2026-07-25; parked by Casey the same day). Three things sit in front of it:

    • The job has never once passed. 11 runs, 11 failures, none since Jul 14; kernel-latest is a 404 and has never been published. The Jul 14 chain (b1d32269c51f3f) was successive gitea-credential fixes to build.sh and the last fix was never re-run — it may already work.
    • pkgver is static. kernel/PKGBUILD:12-13 is pkgver=7.1.1 / pkgrel=1; the commit lives only in the kernelrelease, so every build is 7.1.1-1 and -Syu can never see an upgrade. build.sh:27-28 hardcodes both artifact filenames — they must become dynamic in the same change.
    • The obvious fix is non-monotonic. vercmp 7.1.1.g588929024 7.1.1.gaaa111222-1: a bare .g<sha> sorts as hex, so a newer kernel can sort below an older one. Use 7.1.1.r<count>.g<sha> (verified monotonic), matching souveraine's own scheme. Never +.

    Archive shape — Casey chose "additive" 2026-07-25. Publishing into the shared edge release as written above does not work today: ci.yml deletes the release and tag and recreates it, uploading only its own pacman-repo (which it rm -rfs first), so anything another producer published there is erased on the next souveraine push. ci.yml must stop delete-and-recreate; each producer then does read-modify-write on souveraine-<arch>.db under a shared archdev flock (repo-add replaces the same-pkgname entry, so old versions drop out), uploading its own packages plus the refreshed db and deleting only its own superseded assets. Both jobs are runs-on: archdev, so one flock serialises them.

    Two things that make it easier than expected: the archive key 3CD9E99E…C8E911 is already a secret key in casey's gpg keyring on archdev and souveraine's CI signs with it there, so kernel.yml needs no new signing secret; and ~/.git-credentials on archdev holds both oauth2: and casey: gitea entries. Pixel3Arch CI does still need a token that can write to Fimeg/souveraine.

  2. squeekboard + stevia are Pixel3Arch packages — once the kernel job is generalised into a "build these packages, sign, publish" job, they are extra entries in it, not new pipelines.

  3. firefox-arm publishes into the same db instead of standing alone.

  4. Phone wiring lives in rootfs-overlay/, never only on the device — see "What bit us" below.

Signing key

One archive key signs the whole database; a pacman db cannot coherently mix signers. Pixel3Arch's CI needs the same $ARCHIVE_KEY secret souveraine's CI uses. The key asserts "SouveraineOS published this," which is true regardless of which repo built it — so sharing it is correct, not a compromise.

This is the package archive key and is deliberately shared across machines. It is NOT the machine/agent identity key: those stay unique per device and are never generated implicitly (SouveraineOS/docs/FEDERATION.md, identity tiers). Different keyring, different trust domain — do not conflate them.

Phone-side wiring (done 2026-07-24, live)

  • /etc/pacman.d/souveraine-edge.conf[souveraine-aarch64], SigLevel = Required DatabaseRequired. Template from souveraine/packaging/arch/souveraine-edge.conf, __ARCH__aarch64.
  • /usr/local/libexec/souveraine-pacman-fetch — XferCommand shim; attaches the gitea netrc credential for 10.10.20.120 only, every other mirror unchanged.
  • /etc/pacman.d/souveraine-gitea.netrcroot:alpm 0640, token, NOT in git.
  • pacman-key --add + --lsign-key 3CD9E99E222C2A174986FC9AFF4949AA20C8E911.
  • /etc/pacman.confXferCommand under [options], Include at the end.

Two traps found doing it

  • DownloadUser + a custom XferCommand are incompatible. Pacman execs the external command as DownloadUser but hands it a path inside the root-owned sync dir; it only creates the alpm-owned download-* staging dir for its INTERNAL downloader. Result: EACCES on every repo, failing on upstream core.db long before the souveraine repo is reached. Unset DownloadUser.
  • DisableSandboxFilesystem / DisableSandboxSyscalls are not pacman options. They were present in the phone's pacman.conf, silently ignored, and left pacman -Sy broken since ~2026-07-23. pacman 7.1 has exactly one: DisableSandbox — and it governs landlock, not the privilege drop, so it is not the fix here either.

What bit us — why the overlay matters

The phone wiring existed once, was configured live on the device, and was lost at the next reflash. provision-rootfs.sh copies rootfs-overlay/ wholesale into a freshly extracted rootfs; anything not represented there does not survive. The phone is Casey's daily driver — a reprovision costs him his live system, which is exactly why the repo pipeline matters: it replaces reflashing with in-place updates.

Next stage — the shell surfaces, settings included

Casey 2026-07-24: souveraine-settings should become a versioned app, and the state-manager settings should live in it.

Settings is QML, not a binarysurfaces/quickshell/settings.qml, run via qs -p. On the phone it sits in ~/.config/quickshell/souveraine/, unowned, delivered by rsync + deploy.sh. So "version the settings app" is really package the shell surfaces, and it is harder than the binaries were:

  • A package cannot own $HOME. The tree would install to a system path (/usr/share/quickshell/souveraine/) and the shell's resolution + the composed-tree symlinks (souveraine/ → stock ii/) must survive the move. See unify-shell-trees-laptop-phone.md, which is the same tree problem.
  • It is all-or-nothing: splitting settings out alone puts one QML tree behind two delivery mechanisms.
  • deploy.sh becomes the dev loop, not the delivery path.

Ordering, and why. TASK-19's rule is that every control is a view over the owning service — "no success-shaped switches". The device state machine is still observational (it logs transitions and confidence, drives nothing; idle timeouts remain hypridle's 300s/600s). Settings pages over it would be lying until TASK-15/08 give it actuators. But this is not a hard block on all of it: charge control is real now (TASK-08b, unblocked tonight) and has a live sysfs + D-Bus backend, so a battery/charge page can be honest today.

So: package the surfaces → expose what already actuates (charge, existing pages) → add state-machine controls as TASK-15/08 wire them up.

2026-08-11 — an update is offered to a body, not pushed through its glass

Observed on the Pixel daily driver: deploy.sh --phone passed its lock check, snapshotted the tree, and still replaced the live QuickShell composition while Casey was using it. The reload killed an active Souveraine conversation. A rollback makes the files recoverable; it does not make destroying live session state an acceptable delivery protocol.

Casey's required development shape is now explicit:

  1. CI publishes a signed, immutable surface candidate. The laptop never rsyncs that candidate directly over a live body's active tree.
  2. A connected-device channel may announce that a development update is available and safe to evaluate. USB Signaller is a possible transport for that offer; it is not the update authority and attachment is not consent.
  3. Each body answers independently. Its local updater acquires the durable maintenance lease, verifies the package and target profile, stages beside the running candidate, and reports what would change.
  4. Activation is an explicit local transition with conversation/session state preserved or deliberately quiesced, a health check, and an automatic return to the last working candidate. One connected device accepting must not make every connected device switch.

deploy.sh --phone remains a recovery/developer instrument for an explicitly quiesced target. It is no longer the ordinary update path. TASK-08 owns the lease and state projection; the updater owns retrieval and candidate activation; USB Signaller can carry presence and offers only.

The actual inventory (measured 2026-07-25)

379,153 files under /usr /etc /opt /boot; 5,624 owned by nothing. The same disease as the eleven binaries, just not yet colliding: 16 /usr/bin/blueline-* scripts, the gadget-* set, clatd, mmsdtng, qmic, qrtr-*, rmtfs, a hand-installed Qt6Contacts cmake tree, libcmatrix.so.0u (the ldconfig warning on every transaction), and stale chatty.orig-pacman / chatty.pacman-orig leftovers.

One of them, blueline-proximity-lock, is not merely unpackaged — it is a running second authority over proximity. Resolved 2026-07-25: it reports to sessiond and decides nothing. Only its packaging remains, under TASK-27.

pacman -Syu had been blocked for days by exactly one of these files. Intersecting the unowned set against the 112,051 files shipped by the 199 pending packages gave a conflict set of ONE: /usr/include/aquamarine/backend/FrameScheduler.hpp, hand-dropped Jul 9 into a directory whose every other header belongs to aquamarine 0.12.1-1, and shipped by extra/aquamarine 0.13.0-2. It was also a version-mismatched header sitting in the include path the whole time. Backed up to /var/backups/unowned-preupgrade/ and removed.

Compute conflicts, do not guess at them:

pacman -Fy
pacman -Ql | awk '{print $2}' | sed 's#/$##' | sort -u > owned
find /usr /etc /opt /boot -xdev \( -type f -o -type l \) | sort -u > ondisk
comm -13 owned ondisk > unowned
pacman -Qu | awk '{print $1}' > pending
xargs -a pending pacman -Fl | awk '{print "/" $2}' | sed 's#/$##' | sort -u > newfiles
comm -12 unowned newfiles      # <- exactly what will abort -Syu

The phone's login shell is zsh — no mapfile; run this under bash -s over ssh.

Acceptance

  • Pushing to any SouveraineOS repo produces a signed package in souveraine-{arch} without hand-carrying anything. DONE for every device package except the kernel (2026-07-30; Pixel3Arch packages.yml).
  • The phone installs kernel, upower, squeekboard, stevia and souveraine from pacman -Syu. DONE except the kernel — squeekboard/stevia publish alongside the rest; the first real upgrade the pipeline delivered was usb-signaller 0.3.1-2 → 0.3.1-3.
  • A reprovision restores the repo wiring from rootfs-overlay/ with no manual steps.

Cross-build shapes (landed 2026-07-30)

Every aarch64 package cross-compiles on the archdev runner against $HOME/aarch64-sysroot. Four distinct shapes live in packages.yml, each its own block because the incantation differs:

  • arch=any (pulse-native-provider, osk-switch): plain native makepkg.
  • meson C (hexagonrpc, tqftpserv): a generated --cross-file + sysroot pkg-config. Both PKGBUILDs honour MESON_CROSS_FILE in build().
  • Makefile C (blueline-edge-sense): CC="aarch64-linux-gnu-gcc --sysroot=… -L…/usr/lib"; the Makefile honours CC ?=.
  • meson+Rust / cargo (stevia, squeekboard via each build-cross.sh; usb-signaller via CARGO_BUILD_TARGET + a per-target cross linker).

Two traps worth not re-deriving: (1) --sysroot alone does NOT link on this toolchain — ld resolves the sysroot libc.so script's = paths against the wrong prefix and falls back to the host /lib/libc.so.6 ("file in wrong format"); an explicit -L$SYSROOT/usr/lib makes ld find the right script first. (2) the C cross-file is generated inline in the workflow; stevia/ squeekboard's own cross-files (~/build/*.ini) are pets-state, failed loud if missing.

What broke CI and how it was fixed

First green run was 993. Run 992 failed because stale bare VCS clones and built *.pkg.tar.zst binaries had been committed (pkgs/stevia/stevia/, pkgs/squeekboard/squeekboard/, 9 package files incl. a 1.7 MB packfile) — makepkg rejected the clone as "not a clone of ". Fix: git rm'd the junk, gitignored *.pkg.tar.zst + the VCS checkout dirs, and set SRCDEST out-of-tree in CI so clones never land in a package dir. usb-signaller's PKGBUILD was also made cross-aware (it built for the host tuple before).

Connects to

TASK-24 (keyboard: squeekboard/stevia are two of the packages), the kernel flash flow in PAF/build.md, and souveraine/packaging/arch/ which already holds the canonical configs.