Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/25-one-repo-all-packages.md

266 lines
14 KiB
Markdown

# TASK 25 — One gitea repo for every SouveraineOS package
**Status:** 2026-08-13 — Pixel3Arch now has one package workflow. Its device
and kernel jobs publish signed artifacts into the additive `edge` release and
validate every emitted artifact against the central distribution contract. The
former raw `kernel-latest` release path is retired; its first shared-archive CI
proof remains required before calling the kernel delivery complete.
## 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 (`b1d3226``9c51f3f`) 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 -rf`s 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.netrc` — `root:alpm 0640`, token, NOT in git.
- `pacman-key --add` + `--lsign-key 3CD9E99E222C2A174986FC9AFF4949AA20C8E911`.
- `/etc/pacman.conf` — `XferCommand` 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 binary** — `surfaces/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 <url>". 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.