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

+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