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:
swaitsandClaude Fable 5.1 committed 2026-10-29 15:12:18 -06:00
1 parent 4b5f44689d
commit 3058b5c229
20 files changed
+669 -18

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
# Releases (plan P11a, D-24, D-26): on a version tag, build the Linux
# packages on the oldest system they claim (ubuntu-24.04, glibc 2.39; a
# program linked on a newer one does not start on an older one), attach
# them to the GitHub release, and add them to the apt and dnf repositories
# on the gh-pages branch (GitHub Pages serves them). Unsigned: step 5 of
# the unit (the release key, Q-4) signs the manifest and the indexes, and
# step 6 adds the macOS and Windows jobs. The release holds no key today.
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
jobs:
linux:
runs-on: ubuntu-24.04
# Same disk discipline as check.yml: no incremental caches.
env:
CARGO_INCREMENTAL: "0"
steps:
- uses: actions/checkout@v4
- name: Room for the build (the image ships toolchains we never use)
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \
/opt/hostedtoolcache/CodeQL /usr/local/.ghcup
df -h / | tail -1
- uses: jdx/mise-action@v2
with:
# Only what packaging needs, not the proof and fuzz tools.
install_args: "rust just cargo:cargo-packager cargo:cargo-generate-rpm"
- name: GUI build dependencies
run: sudo apt-get update && sudo apt-get install -y libxkbcommon-dev libgtk-3-dev apt-utils createrepo-c
- name: The tag is the program's version
id: version
run: |
own=$(sed -n 's/^version = "\(.*\)"/\1/p' crates/ledger-cli/Cargo.toml | head -n1)
tag="${GITHUB_REF_NAME#v}"
if [ "${GITHUB_REF_TYPE}" = tag ] && [ "$tag" != "$own" ]; then
echo "tag $GITHUB_REF_NAME is not version $own" >&2
exit 1
fi
echo "version=$own" >> "$GITHUB_OUTPUT"
- name: Build the packages
run: just package-linux ${{ steps.version.outputs.version }}
- name: Attach them to the release
if: github.ref_type == 'tag'
env:
GH_TOKEN: ${{ github.token }}
run: |
v=${{ steps.version.outputs.version }}
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 || gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes "wanderbooks $v"
gh release upload "$GITHUB_REF_NAME" --clobber target/packages/$v/*.AppImage target/packages/$v/*.deb target/packages/$v/*.rpm target/packages/$v/*.tar.gz
- name: Add them to the apt and dnf repositories (gh-pages)
if: github.ref_type == 'tag'
run: |
v=${{ steps.version.outputs.version }}
git fetch origin gh-pages || true
if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then
git worktree add repo origin/gh-pages
else
git worktree add --detach repo && git -C repo checkout --orphan gh-pages && git -C repo rm -rfq . || true
fi
mkdir -p repo/apt/pool repo/dnf
cp target/packages/$v/*.deb repo/apt/pool/
cp target/packages/$v/*.rpm repo/dnf/
deploy/packaging/repo-indexes.sh apt repo/apt
deploy/packaging/repo-indexes.sh dnf repo/dnf
cd repo
git config user.name "wanderbooks release"
git config user.email "release@wanderbooks.ai"
git add -A
git commit -m "wanderbooks $v"
git push origin HEAD:gh-pages
+9
View File
@@ -95,6 +95,15 @@ create acme`, `wanderbooks connect claude --book acme` (or `connect codex`).
Then talk to the agent; `wanderbooks gui acme` shows the books, and its
Books menu backs them up, restores, starts new books and closes them.
Updating: an AppImage, or a `wanderbooks` you put somewhere yourself, brings
itself to the newest release with `wanderbooks update` (it checks the
download against our release key, keeps the version it replaces, and the
open books restart onto the new one when idle); `wanderbooks rollback` goes
back, unless a book has since moved to a newer journal format. A `.deb`,
`.rpm` or AUR install is updated by its package manager and `update` says
so. The Linux packages are built here with `just package-linux`
(docs/packaging.md); none is published yet.
## Developing
Every tool the recipes use (Rust, just, Kani, Lean, cargo-mutants,
+4 -2
View File
@@ -32,8 +32,10 @@ sudo install -D -m 0644 deploy/wanderbooks-server.service \
```
The executable is the server-only build: no viewer, no window libraries.
On a Linux package install (P11) this is the same binary and the package
manager writes the file; until then, copy it as above.
The Linux packages (`.deb`, `.rpm`, AUR; `just package-linux`, docs/packaging.md)
carry the viewer build, which also serves: on a package install the package
manager writes the file and `wanderbooks update` refuses. A server with no
window libraries is still this server-only build, copied as above.
## 2. Make the data root, once
+25
View File
@@ -0,0 +1,25 @@
# Maintainer: Stephen Waits
# The AUR package (P11a step 3, D-24, D-26): it repacks the tarball of the
# release's files that GitHub Releases serves; nothing is built from source.
# `updpkgsums` fills the checksum when a release is published.
pkgname=wanderbooks-bin
pkgver=0.1.0
pkgrel=1
pkgdesc="Double-entry bookkeeping your agent keeps and you can read"
arch=('x86_64')
url="https://wanderbooks.ai"
license=('MIT' 'Apache-2.0')
depends=('gcc-libs' 'glibc')
provides=('wanderbooks')
conflicts=('wanderbooks')
options=('!lto' '!strip')
# The tarball holds usr/bin/wanderbooks, usr/share/applications/ and
# usr/share/wanderbooks/install-method (which makes `wanderbooks update`
# refuse and send the owner to pacman).
source=("wanderbooks_${pkgver}_x86_64.tar.gz::https://github.com/swaits/wanderbooks/releases/download/v${pkgver}/wanderbooks_${pkgver}_x86_64.tar.gz")
sha256sums=('SKIP')
noextract=()
package() {
cp -r "${srcdir}/usr" "${pkgdir}/usr"
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# The Linux packages (P11a step 3), from the release program already built
# at target/release/wanderbooks (`cargo build --release -p ledger-cli`).
#
# build-linux.sh [VERSION] [FORMATS]
#
# Puts into target/packages/VERSION/: the .deb, the AppImage and the
# pacman tarball with its PKGBUILD (cargo-packager), and the .rpm
# (cargo-generate-rpm). VERSION defaults to the program's own; a different
# one repackages the same program under that version, which is how the
# install-and-upgrade test gets its second release. FORMATS is
# cargo-packager's list (default deb,appimage,pacman); the .rpm is always
# built. Run from the repository root.
#
# The program is target/release/wanderbooks, or the file WB_PROGRAM names:
# a package must run on the oldest system it claims, and a program linked
# on a newer one (this machine's glibc) fails there with "GLIBC_2.43 not
# found". CI builds on ubuntu-24.04; `just test-linux-packages` builds in a
# Debian trixie container.
set -euo pipefail
own=$(sed -n 's/^version = "\(.*\)"/\1/p' crates/ledger-cli/Cargo.toml | head -n1)
version=${1:-$own}
formats=${2:-deb,appimage,pacman}
out="target/packages/$version"
program=${WB_PROGRAM:-target/release/wanderbooks}
test -x "$program" || { echo "build the release program first: cargo build --release -p ledger-cli" >&2; exit 1; }
rm -rf "$out"; mkdir -p "$out"
stage="target/packages/.stage-$version"
rm -rf "$stage"; mkdir -p "$stage"
cp "$program" "$stage/wanderbooks"
config=$(mktemp --suffix=.toml)
trap 'rm -f "$config"' EXIT
# cargo-packager reads paths relative to the config file, which is in /tmp
# here, so they are made absolute.
sed "s/^version = \".*\"/version = \"$version\"/
s#^out-dir = .*#out-dir = \"$PWD/$out\"#
s#\"target/release\"#\"$PWD/$stage\"#
s#\"deploy/packaging/#\"$PWD/deploy/packaging/#
s#\"LICENSE-MIT\"#\"$PWD/LICENSE-MIT\"#" \
deploy/packaging/packager.toml >"$config"
# The AppImage tools are themselves AppImages; run them without FUSE.
export APPIMAGE_EXTRACT_AND_RUN=1
cargo packager --config "$config" --formats "$formats" --release
assets='assets = [
{ source = "'$PWD/$stage'/wanderbooks", dest = "/usr/bin/wanderbooks", mode = "0755" },
{ source = "deploy/packaging/install-method-rpm", dest = "/usr/share/wanderbooks/install-method", mode = "0644" },
{ source = "deploy/packaging/wanderbooks-static.desktop", dest = "/usr/share/applications/wanderbooks.desktop", mode = "0644" },
]'
cargo generate-rpm -p crates/ledger-cli --set-metadata "version=\"$version\"" --set-metadata "$assets" -o "$out/"
rm -rf "$stage"
ls -l "$out"
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+1
View File
@@ -0,0 +1 @@
aur
+1
View File
@@ -0,0 +1 @@
deb
+1
View File
@@ -0,0 +1 @@
rpm
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# A release folder on this machine, for trying `wanderbooks update` without
# a network or the owner's keys (P11a step 2; docs/packaging.md).
#
# local-release.sh FILE VERSION OUTDIR
#
# Makes a throwaway signing key in OUTDIR/key (never a release key), signs
# FILE with it through cargo-packager, copies it beside a manifest.json in
# the shape `wanderbooks update` reads, and leaves the key's public half in
# OUTDIR/release.pub. Then:
#
# wanderbooks update --feed file://OUTDIR/manifest.json \
# --trusted-key OUTDIR/release.pub
set -euo pipefail
file=${1:?usage: local-release.sh FILE VERSION OUTDIR}
version=${2:?usage: local-release.sh FILE VERSION OUTDIR}
out=${3:?usage: local-release.sh FILE VERSION OUTDIR}
mkdir -p "$out"
if [ ! -f "$out/key" ]; then
cargo packager signer generate --path "$out/key" --password "" --ci >/dev/null
cp "$out/key.pub" "$out/release.pub"
fi
case "$(uname -m)" in arm64) arch=aarch64 ;; *) arch=$(uname -m) ;; esac
platform="$(uname -s | tr '[:upper:]' '[:lower:]')-$arch"
name="wanderbooks-$version-$platform"
cp "$file" "$out/$name"
cargo packager signer sign "$out/$name" --private-key "$out/key" --password "" >/dev/null
sha=$(sha256sum "$out/$name" | cut -d' ' -f1)
sig=$(tr -d '\n' <"$out/$name.sig")
printf '{"version":"%s","notes":"a local release","platforms":{"%s":{"url":"%s","sha256":"%s","signature":"%s"}}}\n' \
"$version" "$platform" "$name" "$sha" "$sig" >"$out/manifest.json"
echo "feed: file://$(cd "$out" && pwd)/manifest.json key: $out/release.pub"
+36
View File
@@ -0,0 +1,36 @@
# cargo-packager's description of the Linux bundles (P11a step 3; D-25).
# `just package-linux` builds the release program and runs
# cargo packager --config deploy/packaging/packager.toml
# from the repository root. The .rpm is cargo-generate-rpm's, from the
# [package.metadata.generate-rpm] table of crates/ledger-cli/Cargo.toml.
name = "wanderbooks"
product-name = "Wanderbooks"
version = "0.1.0"
identifier = "ai.wanderbooks.app"
description = "Double-entry bookkeeping your agent keeps and you can read."
long-description = "One program: the viewer, the books server, and the bridge that lets Claude Code or Codex keep the books."
homepage = "https://wanderbooks.ai"
authors = ["Stephen Waits"]
license-file = "LICENSE-MIT"
category = "Finance"
binaries-dir = "target/release"
out-dir = "target/packages"
icons = ["deploy/packaging/icons/512x512.png"]
formats = ["deb", "appimage", "pacman"]
binaries = [{ path = "wanderbooks", main = true }]
[deb]
section = "utils"
# A deb marks itself package-managed: `wanderbooks update` refuses on it
# and says to use apt (plan 3.9).
desktop-template = "deploy/packaging/wanderbooks.desktop"
depends = ["libc6", "libgcc-s1"]
[deb.files]
"deploy/packaging/install-method-deb" = "usr/share/wanderbooks/install-method"
[pacman]
depends = ["gcc-libs", "glibc"]
[pacman.files]
"deploy/packaging/install-method-aur" = "usr/share/wanderbooks/install-method"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# The apt and dnf repository indexes (P11a step 3), served from GitHub
# Pages (D-26). Unsigned until step 5 gives them the release key.
#
# repo-indexes.sh apt DIR DIR/pool/*.deb -> DIR/dists/stable/...
# repo-indexes.sh dnf DIR DIR/*.rpm -> DIR/repodata/
#
# Every release stays in the pool: a downgrade is the package manager's
# (plan 3.9). apt needs apt-utils (`apt-ftparchive`), dnf needs
# createrepo_c.
set -euo pipefail
kind=${1:?usage: repo-indexes.sh apt|dnf DIR}
dir=${2:?usage: repo-indexes.sh apt|dnf DIR}
case "$kind" in
apt)
cd "$dir"
arch=binary-amd64
mkdir -p "dists/stable/main/$arch"
apt-ftparchive packages pool >"dists/stable/main/$arch/Packages"
gzip -9 -kf "dists/stable/main/$arch/Packages"
apt-ftparchive \
-o APT::FTPArchive::Release::Origin=wanderbooks \
-o APT::FTPArchive::Release::Suite=stable \
-o APT::FTPArchive::Release::Codename=stable \
-o APT::FTPArchive::Release::Architectures=amd64 \
-o APT::FTPArchive::Release::Components=main \
release dists/stable >dists/stable/Release.tmp
mv dists/stable/Release.tmp dists/stable/Release
;;
dnf)
createrepo_c --update "$dir"
;;
*)
echo "usage: repo-indexes.sh apt|dnf DIR" >&2
exit 2
;;
esac
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# linux_packages_install_and_upgrade (P11a step 3): in a clean Debian,
# Fedora and Arch container, install release 0.1.0 from its package,
# open the books as an ordinary user, upgrade to 0.1.1 as root, and check
# that the books' head is unchanged, that the host the books were open
# under restarted onto the new file, and that `wanderbooks update` refuses
# (a package manager owns the program). The containers run with
# seccomp=unconfined: the host refuses to run under a filter (host.rs
# INVARIANT 2), and Docker's default profile is one. Run from the repository root after
# `just test-linux-packages` built both releases (target/packages/).
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
old=0.1.0 new=0.1.1
pk=$PWD/target/packages
mount=(-v "$pk:/packages:ro")
# The steps every distro shares, run inside the container: `install` and
# `upgrade` are shell snippets for that distro.
common() { cat <<'INNER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
useradd -m tester
as_tester() { su tester -c "$*"; }
install_old
test -x /usr/bin/wanderbooks
grep -q . /usr/share/wanderbooks/install-method
as_tester 'wanderbooks --help' >/dev/null 2>&1 || true
as_tester 'wanderbooks setup' >/dev/null
as_tester 'wanderbooks books create acme' >/dev/null
head_of() { as_tester 'tail -n1 ~/.local/share/wanderbooks/books/acme/journal.jsonl | sha256sum'; }
before=$(as_tester 'wanderbooks books list')
head_before=$(head_of)
echo "before: $before head $head_before"
# The books open: a holder keeps the host and server running.
as_tester 'nohup wanderbooks serve >/tmp/serve.log 2>&1 &'
for _ in $(seq 50); do pgrep -u tester -x wanderbooks -a | grep -q ' host' && break; sleep 0.2; done
host_before=$(pgrep -u tester -f 'wanderbooks host' | head -n1)
test -n "$host_before"
upgrade_new
# The host notices the replaced file and restarts onto it.
for _ in $(seq 150); do
host_after=$(pgrep -u tester -f 'wanderbooks host' | head -n1 || true)
[ -n "$host_after" ] && [ "$host_after" != "$host_before" ] && break
sleep 0.2
done
test -n "$host_after" && test "$host_after" != "$host_before"
! readlink "/proc/$host_after/exe" | grep -q deleted
after=$(as_tester 'wanderbooks books list')
echo "after: $after head $(head_of)"
test "$before" = "$after"
test "$head_before" = "$(head_of)"
if as_tester 'wanderbooks update' >/tmp/update.out 2>&1; then echo "update did not refuse" >&2; exit 1; fi
grep -q 'package manager' /tmp/update.out
as_tester 'wanderbooks close' >/dev/null 2>&1 || true
echo "OK"
INNER
}
run() { # name image install_old upgrade_new
local name=$1 image=$2
echo "== $name ($image)"
docker run --rm --security-opt seccomp=unconfined -e DEBIAN_FRONTEND=noninteractive "${mount[@]}" "$image" bash -c "
$3
$4
$(common)"
}
# Debian and Fedora install from a repository built by repo-indexes.sh
# (the one GitHub Pages serves), the new release added to it afterwards.
mount+=(-v "$PWD/deploy/packaging:/tools:ro")
run debian debian:trixie-slim \
"apt-get update -qq && apt-get install -y -qq apt-utils procps >/dev/null
mkdir -p /repo/apt/pool && cp /packages/$old/*.deb /repo/apt/pool/ && /tools/repo-indexes.sh apt /repo/apt
echo 'deb [trusted=yes] file:///repo/apt stable main' >/etc/apt/sources.list.d/wanderbooks.list
install_old() { apt-get update -qq && apt-get install -y -qq wanderbooks >/dev/null; }" \
"upgrade_new() { cp /packages/$new/*.deb /repo/apt/pool/ && /tools/repo-indexes.sh apt /repo/apt && apt-get update -qq && apt-get install -y -qq --only-upgrade wanderbooks >/dev/null; }"
run fedora fedora:latest \
"dnf install -y -q procps-ng util-linux createrepo_c >/dev/null
mkdir -p /repo/dnf && cp /packages/$old/*.rpm /repo/dnf/ && /tools/repo-indexes.sh dnf /repo/dnf
printf '[wanderbooks]\nname=wanderbooks\nbaseurl=file:///repo/dnf\ngpgcheck=0\n' >/etc/yum.repos.d/wanderbooks.repo
install_old() { dnf install -y -q wanderbooks >/dev/null; }" \
"upgrade_new() { cp /packages/$new/*.rpm /repo/dnf/ && /tools/repo-indexes.sh dnf /repo/dnf && dnf upgrade -y -q --refresh wanderbooks >/dev/null; }"
# Arch: the AUR's own PKGBUILD (deploy/packaging/aur), its source pointed at
# the tarball just built instead of the release URL, makes the package as
# an ordinary user.
arch_build='build() { rm -rf /build/$1 && mkdir -p /build/$1 && cp /packages/$1/*.tar.gz /build/$1/ && cp /tools/aur/PKGBUILD /build/$1/PKGBUILD && sed -i "s/^pkgver=.*/pkgver=$1/; s|^source=.*|source=(wanderbooks_$1_x86_64.tar.gz)|" /build/$1/PKGBUILD && chown -R builder /build/$1 && su builder -c "cd /build/$1 && makepkg -f --nodeps >/dev/null 2>&1"; }'
run arch archlinux:latest \
"useradd -m builder; pacman -Sy --noconfirm --needed base-devel procps-ng >/dev/null 2>&1; $arch_build; install_old() { build $old; pacman -U --noconfirm /build/$old/*.pkg.tar.zst >/dev/null 2>&1; }" \
"upgrade_new() { build $new; pacman -U --noconfirm /build/$new/*.pkg.tar.zst >/dev/null 2>&1; }"
echo "all packages install and upgrade"
@@ -0,0 +1,7 @@
[Desktop Entry]
Categories=Office;Finance;
Comment=Double-entry bookkeeping your agent keeps and you can read.
Exec=wanderbooks
Name=Wanderbooks
Terminal=false
Type=Application
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Categories={{categories}}
{{#if comment}}
Comment={{comment}}
{{/if}}
Exec={{exec}}
Icon={{icon}}
Name={{name}}
Terminal=false
Type=Application
+41 -1
View File
@@ -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
View File
@@ -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)
+76
View File
@@ -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.
+32
View File
@@ -263,3 +263,35 @@ deploy: site
# Deploy a preview of the website and print its URL
deploy-preview: site
{{netlify}} deploy --dir site/dist --no-build
# The Linux packages (P11a step 3), in target/packages/VERSION. Builds the
# release program first; needs the tools `mise install` pins. A VERSION other
# than the program's repackages the same program under it (the upgrade
# test's second release).
# Build the AppImage, .deb, pacman tarball and .rpm
package-linux version="" formats="deb,appimage,pacman":
cargo build --release -p ledger-cli
deploy/packaging/build-linux.sh {{version}} {{formats}}
# A release folder on this machine signed with a throwaway key; then
# `wanderbooks update --feed file://DIR/manifest.json --trusted-key DIR/release.pub`
# Make a local release feed to try `wanderbooks update` against
local-release file version dir:
deploy/packaging/local-release.sh {{file}} {{version}} {{dir}}
# A program linked here does not start on Debian (docs/packaging.md); this
# builds it in rust:1.99-trixie, into target/container. Needs docker.
# Build the release program in a Debian trixie container
release-trixie:
mkdir -p target/container
docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp -e CARGO_HOME=/tmp/cargo-home -e CARGO_TARGET_DIR=/target -v "$PWD:/src:ro" -v "$PWD/target/container:/target" -w /src rust:1.99-trixie cargo build --release --locked -p ledger-cli
# Install each Linux package in a clean Arch, Debian and Fedora container,
# open the books, upgrade to the next release, and check the books' head is
# unchanged and the open books restarted onto the new program. Builds the
# program in a trixie container, then releases 0.1.0 and 0.1.1 of it.
# Install and upgrade the Linux packages in Arch, Debian and Fedora containers
test-linux-packages: release-trixie
WB_PROGRAM=target/container/release/wanderbooks deploy/packaging/build-linux.sh 0.1.0 deb,pacman
WB_PROGRAM=target/container/release/wanderbooks deploy/packaging/build-linux.sh 0.1.1 deb,pacman
deploy/packaging/test-linux-packages.sh
+4
View File
@@ -23,3 +23,7 @@ node = "26"
# A compile cache shared by every jj workspace's target dir: point
# cargo's build.rustc-wrapper at it (~/.cargo/config.toml).
sccache = "0.18.0"
# `just package-linux` (docs/packaging.md): the AppImage and .deb by
# cargo-packager, the .rpm by cargo-generate-rpm (D-25).
"cargo:cargo-packager" = "0.11.8"
"cargo:cargo-generate-rpm" = "0.21.1"