Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/25-one-repo-all-packages.md
2026-08-10 17:00:23 -04:00

13 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.

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.