feat(packaging): the trial's Linux column and the Linux packages (P11a steps 1 and 3)
The Linux half of the packaging trial, run on this machine and in Debian, Fedora and Arch containers, and the recipes it produced (docs/packaging.md holds the table and what it found): - mise.toml pins cargo-packager 0.11.8 and cargo-generate-rpm 0.21.1, now that `just package-linux` calls them. cargo-packager-updater is a library, not a tool, so it is not pinned (update.rs does the checking with minisign-verify, which is what the updater uses). - deploy/packaging/: packager.toml (.deb, AppImage, the pacman tarball and its PKGBUILD), the rpm table in crates/ledger-cli/Cargo.toml, the AUR's own PKGBUILD (repacks the release tarball), build-linux.sh, repo-indexes.sh (the apt and dnf indexes, unsigned until the signing step), local-release.sh (a throwaway-keyed feed to try `update`), test-linux-packages.sh (linux_packages_install_and_upgrade: install 0.1.0, open books as an ordinary user, upgrade to 0.1.1, check the host restarted onto the new file, the books' last journal line unchanged and `update` refuses; passes on Debian trixie, Fedora and Arch). A package marks itself package-managed with share/wanderbooks/install-method. - The icon is a placeholder square (the AppImage cannot be built without one). - .github/workflows/release.yml: on a version tag, build on ubuntu-24.04 (the oldest glibc the packages claim: a program linked on this machine needs GLIBC_2.43 and does not start on Debian 13), attach the packages to the release, and add them to the apt and dnf repositories on gh-pages. No signing and no macOS or Windows job yet. - The release profile strips symbols. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
4b5f44689d
commit
3058b5c229
20 files changed
+669
-18
No files matched your search
+41
-1
@@ -36,7 +36,7 @@ the specification in `bookspec/` (which never depends on the engine).
|
||||
| `ledger-import` | formats: QuickBooks CSV in; IIF, XLSX, Typst PDFs (statements, the invoice, labels) out | `ledger-core`, `ledger-reader` |
|
||||
| `ledger-package` | the shared read-only view over `Store` or `Snapshot` (`PackageBooks`): the CPA package, the 1099 worksheet, the invoice document, Needs attention | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-vault` |
|
||||
| `ledger-mcp` | the books server: the MCP tools (122), resources, the handbook, scopes, the door and the access log, backups' scheduling | all of the above |
|
||||
| `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in 0600 files under the data root (D-63), the owner's commands, uploads and downloads | `ledger-store`, `ledger-mcp`, `ledger-config` |
|
||||
| `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in 0600 files under the data root (D-63), the owner's commands, uploads and downloads, `update` and `rollback` (P11a) | `ledger-store`, `ledger-mcp`, `ledger-config` |
|
||||
| `ledger-gui` | the viewer: read-only, live, egui; the Books menu hands work to the server | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-package`, `ledger-vault`, `ledger-host` |
|
||||
| `ledger-cli` | `wanderbooks`, the one executable: every mode (`serve`, `server`, `host`, `mcp`, `gui`, `setup`, `books`, `token`, `connect`, `doctor`, …) and the over-the-wire test suites | everything |
|
||||
| `bookspec/checker`, `bookspec/runner` | the example schema and parser, the rule catalogue, the `Engine` trait; the per-rule runner | nothing of ours (the adapter in `ledger-core`'s tests depends on them) |
|
||||
@@ -313,3 +313,43 @@ still run on the server). The recipe, the head check for the move and the
|
||||
two-account CI job are in `deploy/README.md`, `deploy/wanderbooks-server.service`
|
||||
and `.github/workflows/two-user.yml`; the checks are
|
||||
`crates/ledger-cli/tests/two_user.rs`.
|
||||
|
||||
## Packaging and updates (`ledger-host`'s `update.rs`, `deploy/packaging/`, P11a)
|
||||
|
||||
One program, installed one of two ways, updated one of two ways
|
||||
(plan D-24). The install says which: a package ships
|
||||
`share/wanderbooks/install-method` beside `bin/`, and a file there makes
|
||||
the program package-managed; `wanderbooks update` and `rollback` refuse it
|
||||
and name the manager (apt, dnf and the AUR keep every release, so
|
||||
downgrading is theirs). Anything else (an AppImage, whose file is named by
|
||||
`$APPIMAGE`; an executable put somewhere by hand) updates itself:
|
||||
|
||||
- the feed is a `manifest.json` (a version, and per platform a file's
|
||||
address, sha256 and minisign signature; the shape `cargo-packager`'s
|
||||
updater reads, with the sha256 added) at GitHub Releases' fixed address
|
||||
(D-26); `--feed` reads another, and only with a `file://` feed may
|
||||
`--trusted-key` name the key;
|
||||
- the key a release is checked against is compiled in
|
||||
(`WANDERBOOKS_RELEASE_PUBKEY`, set by the release workflow once the
|
||||
owner's key exists; step 5): a build without one updates from nothing;
|
||||
- a download is accepted only if its sha256 matches AND its signature
|
||||
verifies; then it is staged, the running version kept under
|
||||
`<root>/updates/previous/` with the journal format it understood, and the
|
||||
file replaced by a rename of a complete copy made beside it;
|
||||
- nothing tells the books to stop. The host notices its executable's file
|
||||
was replaced and restarts as it does for a package upgrade (host.rs
|
||||
INVARIANT 7), finishing calls in progress; bridges reconnect upstream
|
||||
without the agent noticing. The format gate stays the server's (3.9):
|
||||
`rollback` refuses while a book declares a format or feature the kept
|
||||
version does not know;
|
||||
- an AppImage is the exception to the restart: the host runs from the old
|
||||
image's mount, whose path does not change, so it keeps running the old
|
||||
version until the books are next closed and opened (docs/packaging.md).
|
||||
|
||||
`deploy/packaging/` holds the Linux packaging: `packager.toml`
|
||||
(cargo-packager: `.deb`, AppImage, the pacman tarball with its PKGBUILD),
|
||||
the rpm table in `crates/ledger-cli/Cargo.toml`, `aur/PKGBUILD` (the AUR
|
||||
package, which repacks the release's tarball), `build-linux.sh`,
|
||||
`repo-indexes.sh` (the apt and dnf indexes), `local-release.sh` (a
|
||||
throwaway-keyed feed on this machine) and `test-linux-packages.sh`.
|
||||
`.github/workflows/release.yml` builds them on a version tag.
|
||||
+133
-15
@@ -1,23 +1,141 @@
|
||||
# Packaging and updates: the trial (P11a step 1)
|
||||
|
||||
Status 2026-10-29: **the trial has not been run.** It needs clean Linux,
|
||||
macOS and Windows machines for the six formats (`.deb`, `.rpm`, AUR,
|
||||
AppImage, `.dmg`, NSIS `.exe`), and the one machine that built step 0 is a
|
||||
Linux desktop with neither a macOS nor a Windows target, no container
|
||||
runtime and none of the packager tools installed. The tools are pinned in
|
||||
`mise.toml` now (`cargo-packager` 0.11.8, `cargo-packager-updater` 0.2.3,
|
||||
`cargo-generate-rpm` 0.21.1), so the trial is `mise install` and the VMs.
|
||||
Nothing below is a result; it is what is known before the trial, and the
|
||||
table is filled by the person who runs it.
|
||||
Status 2026-10-29: **the Linux half of the trial is run** (on the Arch
|
||||
desktop that builds the project, with Debian, Fedora and Arch containers
|
||||
for the installs); **the macOS and Windows halves are not**: they need
|
||||
those machines (and for the `.dmg` and the NSIS `.exe`, the signing
|
||||
identities of step 5), and this machine has neither a macOS nor a Windows
|
||||
target. `cargo-packager` 0.11.8 and `cargo-generate-rpm` 0.21.1 are pinned in
|
||||
`mise.toml`; `cargo-packager-updater` is **not** pinned: it is a library
|
||||
(the program checks releases itself, `crates/ledger-host/src/update.rs`),
|
||||
not a tool, so there is nothing for a recipe to call.
|
||||
|
||||
| format | built by | installs as | updated by | signed by |
|
||||
|---|---|---|---|---|
|
||||
| `.deb` | `cargo packager` | (trial) | apt repository (step 3) | (trial) |
|
||||
| `.rpm` | `cargo-generate-rpm` | (trial) | dnf repository (step 3) | (trial) |
|
||||
| AUR `PKGBUILD` | hand-written | (trial) | the package manager | n/a |
|
||||
| AppImage | `cargo packager` | (trial) | `cargo-packager-updater` | (trial) |
|
||||
| `.dmg` | `cargo packager` | (trial) | `cargo-packager-updater` | Developer ID (step 5) |
|
||||
| NSIS `.exe` | `cargo packager` | (trial) | `cargo-packager-updater` | Windows identity (step 5) |
|
||||
| `.deb` | `cargo packager` (`deb`) | `/usr/bin/wanderbooks`, a menu entry, `/usr/share/wanderbooks/install-method` = `deb`; 47 MB | apt repository (`repo-indexes.sh apt`, GitHub Pages) | unsigned until step 5 (the repository's index is `trusted=yes` in the test) |
|
||||
| `.rpm` | `cargo-generate-rpm` (table in `crates/ledger-cli/Cargo.toml`) | same paths, marker `rpm`; 36 MB | dnf repository (`repo-indexes.sh dnf`) | unsigned until step 5 (`--signing-key` takes the PGP key then) |
|
||||
| AUR `PKGBUILD` | `cargo packager` makes the tarball (`pacman` format) and a PKGBUILD for it; ours is `deploy/packaging/aur/PKGBUILD`, which repacks the release's tarball (package `wanderbooks-bin`) | same paths, marker `aur`; built with `makepkg` | pacman (the AUR keeps old PKGBUILDs) | n/a (pacman trusts the AUR's checksums) |
|
||||
| AppImage | `cargo packager` (`appimage`); 44 MB | one file the owner puts anywhere and runs; needs no admin password | `wanderbooks update` (`$APPIMAGE` is the file replaced) against the manifest; minisign | minisign by the release key (step 5) |
|
||||
| `.dmg` | `cargo packager` (`dmg`) | (trial) needs a Mac | `wanderbooks update` | Developer ID and notarization (step 5) |
|
||||
| NSIS `.exe` | `cargo packager` (`nsis`) | (trial) needs Windows, and native Windows is unit P11c (D-62) | `wanderbooks update` | the Windows identity (step 5) |
|
||||
|
||||
`just package-linux` builds the first four (`deploy/packaging/build-linux.sh`,
|
||||
about a minute after the release build, itself 8 to 12 minutes on a
|
||||
loaded 16-core machine); `just test-linux-packages` is the install-and-upgrade test.
|
||||
|
||||
## Linux results (2026-10-29)
|
||||
|
||||
What the trial found, each of which is built around now:
|
||||
|
||||
1. **A program linked here does not start on Debian.** This machine's
|
||||
glibc is 2.44; the binary asks for `GLIBC_2.43` and Debian 13 (trixie)
|
||||
has 2.41. A package must be built on the oldest system it claims.
|
||||
`.github/workflows/release.yml` builds on `ubuntu-24.04` (glibc 2.39);
|
||||
`just release-trixie` builds in `rust:1.99-trixie` for the local test. The
|
||||
claim is therefore "Debian 13, Ubuntu 24.04 and newer, current Fedora and
|
||||
Arch"; Debian 12 and Ubuntu 22.04 are not claimed (they would need a
|
||||
build container of their own).
|
||||
2. **The books refuse to run under a seccomp filter** (`host.rs`
|
||||
INVARIANT 2), and Docker's default profile is one: in a container the
|
||||
host fails to start ("refusing to run under a seccomp filter"). The
|
||||
tests run containers with `--security-opt seccomp=unconfined`. Anyone
|
||||
running the books in Docker meets this; it is not a bug of the packages.
|
||||
3. **An AppImage's launcher runs the desktop file's `Exec` line and appends
|
||||
the arguments it was given.** With `Exec=wanderbooks gui`,
|
||||
`Wanderbooks.AppImage update` ran `wanderbooks gui update`. The desktop
|
||||
files say plain `Exec=wanderbooks`, and a launch with nothing to say and
|
||||
no terminal is the viewer (`main.rs::is_bare_launch`); at a terminal
|
||||
it is still the usage. So the AppImage is also the command line:
|
||||
`Wanderbooks.AppImage update`, `... mcp --book acme`.
|
||||
4. **cargo-packager's configuration** (0.11.8): paths in a config are
|
||||
relative to the *config file*, not the working directory (the build script
|
||||
writes an absolute-path copy); `files` maps *source to destination*;
|
||||
`category` is a fixed list (`Finance`); the AppImage needs a square icon
|
||||
(`deploy/packaging/icons/512x512.png`, **a placeholder the owner should
|
||||
replace**); the `pacman` format yields the tarball and PKGBUILD but not a
|
||||
package (that is `makepkg`'s); its `.deb` has `Depends` from the config.
|
||||
5. **The AppImage tools are downloaded when it is built** (`AppRun`,
|
||||
`linuxdeploy` and its AppImage plugin from tauri-apps' and linuxdeploy's
|
||||
GitHub releases, unverified by cargo-packager): a supply-chain edge of
|
||||
the release build, not of the installed app. They run with
|
||||
`APPIMAGE_EXTRACT_AND_RUN=1`, so the build needs no FUSE.
|
||||
6. **`cargo packager signer` signs in a format `update` verifies.** Its
|
||||
key and its `.sig` are the base64 of minisign's two-line text; the
|
||||
manifest's `signature` is that `.sig` as written, and `update` reads the
|
||||
public key in either form. Checked end to end by hand (below), and by
|
||||
`a_public_key_is_read_in_every_form_minisign_and_the_packager_write_it`.
|
||||
7. **The AppImage updates itself but the books it holds do not restart
|
||||
onto the new image.** `update` replaces `$APPIMAGE`, and an AppImage run
|
||||
from a shell (`Wanderbooks.AppImage update`, or the one that was running)
|
||||
proves the swap and the rollback: after `update`, the AppImage on disk
|
||||
is the new file, the old one is kept, and `rollback` restores it. But the
|
||||
host that is open runs from the old image's mount
|
||||
(`/tmp/.mount_*/usr/bin/wanderbooks`), a path that does not change, so
|
||||
`host.rs` never sees a replaced file and the open books keep the old
|
||||
version until they are closed and opened again (`wanderbooks close`).
|
||||
Fixing it means the host learning the image's path (its environment is
|
||||
scrubbed on purpose; `APPIMAGE` would join the allow-list of `start.rs`,
|
||||
or the path would come from the mount table) and starting its successor
|
||||
from it: a decision about what the host may trust, left to the EM (the
|
||||
unit's report names it). Package installs and a hand-placed executable
|
||||
restart as designed (`bridge_survives_update`).
|
||||
8. **Onboarding with no terminal does not exist yet.** The unit's Done-when
|
||||
(1) says an AppImage "opens, onboards and posts an entry with no
|
||||
terminal". The AppImage opens (a bare launch is the viewer), but the viewer
|
||||
opens only an existing book (`ledger_gui::book_in_root`), and a data root
|
||||
and a first book come from `wanderbooks setup` and `books create`, which
|
||||
need a terminal. A first-run path in the viewer ("New books…" is in its
|
||||
menu, but the menu is not reachable until a book is open) is not in P11a's
|
||||
steps. It is the gap between this wave's AppImage and Done-when (1).
|
||||
|
||||
By hand, on this machine, against a throwaway release folder made by `just
|
||||
local-release` (a key from `cargo packager signer generate`; the feed
|
||||
`file://…/manifest.json`; a scratch home made by `wanderbooks setup --home`,
|
||||
nothing of the owner's):
|
||||
|
||||
```text
|
||||
Wanderbooks.AppImage --help -> the usage (FUSE mount, and with APPIMAGE_EXTRACT_AND_RUN=1)
|
||||
Wanderbooks.AppImage setup --home H -> the private folder; books create acme -> books list: "acme open"
|
||||
Wanderbooks.AppImage update --check --feed F --trusted-key K
|
||||
-> "wanderbooks 0.2.0 is available (this is 0.1.0)"
|
||||
Wanderbooks.AppImage update --feed F --trusted-key K
|
||||
-> "Updated wanderbooks from 0.1.0 to 0.2.0."; the file's sha256
|
||||
is the release's; updates/previous/ holds the 0.1.0 image
|
||||
Wanderbooks.AppImage rollback -> "Went back to wanderbooks 0.1.0."; the file is the original again
|
||||
```
|
||||
|
||||
`linux_packages_install_and_upgrade` (`deploy/packaging/test-linux-packages.sh`),
|
||||
2026-10-29, 0.1.0 then 0.1.1, all three passing: in `debian:trixie-slim`
|
||||
and `fedora:latest` the package installs from a repository made by
|
||||
`repo-indexes.sh` (the release workflow runs the same script), in
|
||||
`archlinux:latest` from `makepkg` over `deploy/packaging/aur/PKGBUILD`; then
|
||||
books are made as an ordinary user and held open, 0.1.1 is added and
|
||||
upgraded to by the package manager (`apt-get install --only-upgrade`, `dnf
|
||||
upgrade`, `pacman -U`), the host restarts onto the new file (a new pid whose
|
||||
`/proc/PID/exe` is not deleted), the journal's last line (the head) is
|
||||
byte-for-byte unchanged, and `wanderbooks update` exits non-zero naming the
|
||||
package manager. Not tested: Ubuntu, openSUSE, a package-manager
|
||||
*downgrade* (the repositories keep every release by construction), and the
|
||||
`.rpm` and `.deb` under their real signatures (step 5).
|
||||
|
||||
## What `update` and `rollback` need from a release
|
||||
|
||||
`manifest.json` at `releases/latest/download/` (D-26):
|
||||
|
||||
```json
|
||||
{ "version": "0.2.0", "notes": "…",
|
||||
"platforms": { "linux-x86_64": { "url": "wanderbooks-0.2.0-linux-x86_64",
|
||||
"sha256": "…hex…", "signature": "…base64 of the .sig file…" } } }
|
||||
```
|
||||
|
||||
`url` is absolute or relative to the manifest; the platform key is
|
||||
`std::env::consts::OS-ARCH`; the file is the whole program (the AppImage
|
||||
on Linux). The program checks the version is higher, the sha256, and the
|
||||
minisign signature against `WANDERBOOKS_RELEASE_PUBKEY` (compiled in by the
|
||||
release workflow once the owner has a key: **step 5, Q-4**; today no build
|
||||
carries one, so a build updates only from a `file://` feed with
|
||||
`--trusted-key`). The two-key release manifest of D-25 would wrap this
|
||||
(rotation, expiry, a monotone version) and is step 5's.
|
||||
|
||||
## What step 0 found about the platforms (read before the macOS and Windows rows)
|
||||
|
||||
|
||||
@@ -3669,3 +3669,79 @@ it); the digest's `since_seq` comparison (`>`), its `as_of` bound, the
|
||||
`zero_net_accounts` branch and the `never_reconciled` split;
|
||||
`report_status`'s three open conditions; the open-drafts refusal's
|
||||
`<= as_of` and its decision; the attention `awaiting_evidence` branch.
|
||||
|
||||
## P11a steps 2 and 3: update, rollback, the Linux packages (2026-10-29)
|
||||
|
||||
`wanderbooks update` and `rollback` (`ledger-host/src/update.rs`) are
|
||||
tested over a local release folder (a `file://` feed) and a minisign key
|
||||
the test makes, with real programs: each test makes a home, a stripped
|
||||
copy of the program in its `bin/`, and a manifest naming a newer copy of
|
||||
it (the same program with a tail, so its hash differs). All in
|
||||
`crates/ledger-cli/tests/update.rs` (Linux only):
|
||||
|
||||
- `an_update_replaces_the_program_keeps_the_old_one_and_rollback_brings_it_back`:
|
||||
no key and no feed is refused ("no release key"); the same version is "the
|
||||
newest"; an older offer is refused and nothing is kept; an update puts
|
||||
the new bytes in place, keeps the old under `updates/previous/` with the
|
||||
format it understood, leaves no temporary or staged file; rollback puts
|
||||
the old bytes back and consumes what was kept; a second rollback says
|
||||
there is nothing to go back to.
|
||||
- `update_is_atomic`: a reader polling the program file all through the
|
||||
update sees, on every read, the old file whole or the new whole.
|
||||
- `failed_update_rolls_back`: a validly signed file that does not start
|
||||
(`exit 3`) is swapped in, fails `--help`, and the old program is put
|
||||
back; nothing is staged or left beside the program.
|
||||
- `unsigned_update_refused`: five bad manifests (a good signature by
|
||||
another key; a signature of other bytes; a sha256 that differs; an empty
|
||||
signature; a signature that is not one), each refused with the program
|
||||
unchanged and nothing kept or staged; `--trusted-key` with an `https`
|
||||
feed is refused.
|
||||
- `package_managed_install_refuses_update`: a
|
||||
`share/wanderbooks/install-method` marker beside `bin/` makes both
|
||||
commands refuse, naming the manager, with nothing made in the data root.
|
||||
- `rollback_refuses_after_a_book_consented_to_a_newer_format`: with a
|
||||
book that declares this version's format and a kept version that knows
|
||||
one feature less, rollback refuses and the program stays; with the kept
|
||||
version knowing everything the book declares, it goes back.
|
||||
- `bridge_survives_update` and `update_waits_for_a_running_call`: an
|
||||
agent's bridge session writes without pause while `update` runs and the
|
||||
host restarts onto the new file; every write is answered and made once
|
||||
(the account is in the chart exactly once), the session needs no
|
||||
reconnect, its tool list is unchanged, and the host and server run the
|
||||
file now on disk (not a deleted one). The host's own restart laws stay
|
||||
`open_while_held.rs`'s (`a_restart_is_not_a_close`).
|
||||
|
||||
Unit tests in `update.rs`: `versions_are_three_numbers_and_order_by_number`,
|
||||
`a_package_marker_beside_the_program_makes_it_package_managed`,
|
||||
`a_manifest_address_is_resolved_beside_the_manifest`,
|
||||
`a_swap_leaves_the_whole_new_file_and_no_temporary`,
|
||||
`a_public_key_is_read_in_every_form_minisign_and_the_packager_write_it`.
|
||||
`main.rs`: `only_nothing_at_all_with_no_terminal_is_the_viewer` (the bare
|
||||
launch an AppImage's launcher makes; it cannot be run as a test, because the
|
||||
viewer finds the account's real data root from the password database).
|
||||
|
||||
Not tests, run by hand on this machine and recorded in `docs/packaging.md`:
|
||||
the AppImage updating and rolling back itself against a feed signed by
|
||||
`cargo packager signer` (`just local-release`), which is the check that the
|
||||
packager's signature format is the one `update` verifies.
|
||||
|
||||
**`linux_packages_install_and_upgrade`** is `deploy/packaging/test-linux-packages.sh`
|
||||
(`just test-linux-packages`, needs docker; not part of `just check`): in a
|
||||
clean Debian, Fedora and Arch container it installs release 0.1.0 (Debian
|
||||
and Fedora from an apt and a dnf repository made by `repo-indexes.sh`, the
|
||||
same script the release workflow runs; Arch by `makepkg` from the
|
||||
packager's PKGBUILD), makes books as an ordinary user, holds them open,
|
||||
adds 0.1.1 to the repository and upgrades; then checks the host restarted
|
||||
onto the new file, `wanderbooks books list` is unchanged, and
|
||||
`wanderbooks update` refuses and names the package manager. The programs
|
||||
are built in a `rust:1.99-trixie` container (`just release-trixie`):
|
||||
see `docs/packaging.md` for why the build machine's own binary is not used.
|
||||
|
||||
**For P12 (mutation targets, none run, D-33):** `update.rs`'s `verify`
|
||||
(a mutant that drops the sha check or the signature check must die on
|
||||
`unsigned_update_refused`'s sha and key cases), `refuse_if_consented`'s
|
||||
two comparisons (`rollback_refuses_after_a_book_consented_to_a_newer_format`
|
||||
covers the feature one; a case with a higher format number is a gap to
|
||||
add), `swap_in`'s temporary-then-rename (`update_is_atomic`), `healthy`
|
||||
(`failed_update_rolls_back`), `install_kind` (the marker test), and
|
||||
`parse_version`'s rejections.
|
||||
Reference in new issue
Block a user