CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Canceled after 13s
CI & Build / integration (push) Canceled after 13s
CI & Build / Build & push image (push) Canceled after 0s
Android / Kotlin + Rust (APK) (push) Failing after 14s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m19s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m24s
Desktop (Tauri) / Update manifest (push) Failing after 4s
Step 4 of M314. `desktop/packaging/build-version.sh` was one generator feeding the desktop bundles AND the Android APK off `GITHUB_RUN_NUMBER`, so a Kotlin-only commit re-versioned the desktop and a Rust-only commit re-versioned the phone. Note 3127 §3 cites this repo as its example of that failure. It is replaced by `packaging/version.sh` — one definition of HOW to derive, three file sets, and the sets in one place. Lives at the repo root rather than under desktop/, because it serves three artifacts now and a shared thing filed under one consumer ends up owned by it. ## Two values, and the clock chosen per value (§2) desktop key 1.0.<minutes since 2020-01-01> commit time desktop display 2026.08.28.0900 commit time (#3181 shows it) android versionName commit time android versionCode <minutes since 2020> BUILD time server version 2026.08.28.0900 commit time, no ordering key Every human-readable version in the repo is now one shape. The two exceptions are not version names at all — they are bare monotonic integers a comparator reads and nobody quotes. The desktop needs a separate key because Tauri parses `latest.json` with the semver crate and `2026.08.28.0900` fails it twice (four segments, and `08` is a leading zero). `1.0.` and not `0.0.`: the minor has to clear the installed `0.2.466` line or every dev user is stranded on "up to date" permanently. Android's code comes from BUILD time while the desktop's key comes from COMMIT time, deliberately. Android hard-fails a downgrade with INSTALL_FAILED_VERSION_DOWNGRADE and leaves a channel you cannot get out of, so its key must be monotonic by construction; the desktop merely declines to offer an update, which a guard can catch. ## The bug this found in itself The shallow-clone guard `exit 1`-ed inside a function called as `$(...)` — which ends the SUBSHELL, not the script. `display` still failed, but only because `date` then choked on the empty string. `key` printed the error to stderr, emitted `1.0.-26297280`, and exited ZERO. That is precisely the failure the guard exists to prevent: a too-low version on a green lane, and too-low is the direction you cannot recover from. It resolves into a global in the parent shell now. The test is parametrized over both requests, because one path was covered and the other was broken in exactly the way the covered one was meant to rule out. ## Also `fetch-depth: 0` on every job that derives — four of them, and only ci.yml's gate had it. Depth-1 is silently wrong rather than loudly broken (§6.1). The file sets include each artifact's BUILD RECIPE (its workflow, and `packaging/`). A workflow file is not shipped, but change a Gradle flag and the bytes change while the source does not — and once step 6 skips a build whose version already exists, that serves the OLD artifact on a green run. The base images are deliberately NOT resolved at derive time: that is an external lookup, which §7's corollary forbids. `Dockerfile` is already in the server's set, so pinning `FROM` by digest in step 6 puts the base inside the set for free. `build-version.sh` is deleted, its last consumer (the pacman packager) moved over, and the one finding worth keeping out of its header — why not a `-dev.N` prerelease — is preserved in the successor. #3144 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
170 lines
8.0 KiB
Bash
Executable File
170 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Build a PREBUILT Arch package (.pkg.tar.zst) from the binary the desktop CI job
|
|
# already compiled — no makepkg, no Arch container, no second Rust build.
|
|
#
|
|
# WHY prebuilt: the source PKGBUILD this replaces required every user to install
|
|
# rust + nodejs + npm and compile for minutes before they could open the app.
|
|
# Distribution means shipping a package people can just install, so the one-command
|
|
# installer (install.sh) can `pacman -U` a real native package on Arch/CachyOS
|
|
# instead of falling back to the AppImage.
|
|
#
|
|
# WHY it's safe to package a Debian-compiled binary for Arch: the binary bundles
|
|
# nothing. It resolves libwebkit2gtk-4.1.so.0 / libgtk-3.so.0 / libsoup-3.0.so.0 by
|
|
# SONAME at runtime, and those sonames are identical on both distros; SQLite is
|
|
# compiled in (rusqlite "bundled"). glibc symbol versioning is forward-compatible,
|
|
# and we build on Debian's older glibc and run on Arch's newer one — the safe
|
|
# direction. Using the host's graphics stack is also exactly what keeps issue 2021
|
|
# (bundled-lib EGL_BAD_PARAMETER black window) fixed.
|
|
#
|
|
# A .pkg.tar.* is just a tar whose FIRST entry is .PKGINFO, so we assemble it
|
|
# directly rather than nesting an Arch container over the docker socket.
|
|
#
|
|
# CI cannot verify this end-to-end: the runner is Debian and has no pacman. The
|
|
# step logs .PKGINFO and the full file listing so the package is auditable from the
|
|
# build log; `pacman -U` on a real Arch box is the final proof.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
|
|
SRC_TAURI="$REPO_ROOT/desktop/src-tauri"
|
|
BINARY="$REPO_ROOT/target/release/thoughtsync"
|
|
OUT_DIR="${1:-$REPO_ROOT/target/release/bundle/arch}"
|
|
|
|
PKGNAME="thoughtsync"
|
|
# The name this package used to ship under. pacman needs both to retire it: without
|
|
# them a `pacman -U` of the renamed package installs ALONGSIDE the old one, and two
|
|
# packages both own /usr/bin/thoughtsync (issue 2075).
|
|
REPLACES=(thoughtsync-desktop)
|
|
PKGREL=1
|
|
PKGDESC="ThoughtSync desktop — local-first Keep-style thought capture"
|
|
URL="https://git.fabledsword.com/bvandeusen/thoughtsync"
|
|
LICENSE="MIT"
|
|
PACKAGER="ThoughtSync CI <noreply@fabledsword.com>"
|
|
|
|
# Runtime deps. Deliberately minimal and grounded in what the binary actually
|
|
# links: tauri is built with features=[] (no tray -> no libayatana-appindicator)
|
|
# and rusqlite is "bundled" (no sqlite package). Everything else — glib2, cairo,
|
|
# pango, gdk-pixbuf2, libsoup3 — arrives transitively via these two, so listing
|
|
# them would just be noise that can drift out of date.
|
|
DEPENDS=(webkit2gtk-4.1 gtk3)
|
|
|
|
[ -f "$BINARY" ] || {
|
|
echo "ERROR: no binary at $BINARY — run the tauri/cargo build first." >&2
|
|
exit 1
|
|
}
|
|
|
|
# Single source of truth for the version: the SAME helper the bundle build uses.
|
|
#
|
|
# It used to read tauri.conf.json directly, which was right until dev builds began
|
|
# overriding the version on the command line (M10.9) — the file still says 0.1.0, so
|
|
# the pacman package came out stamped 0.1.0 around a binary reporting 0.1.132. A
|
|
# package that lies about its version is exactly what makes a later "which build is
|
|
# this?" question unanswerable.
|
|
# `|| true` so a miss falls through to the explicit error below rather than
|
|
# aborting on pipefail with no explanation.
|
|
# The ORDERING KEY: pacman compares this, and it must match the filename the
|
|
# bundle build produced (write-manifest.sh selects on it).
|
|
PKGVER="$(sh "$SCRIPT_DIR/../../../packaging/version.sh" key desktop || true)"
|
|
[ -n "$PKGVER" ] || { echo "ERROR: could not determine the build version" >&2; exit 1; }
|
|
|
|
# Reproducible-ish: prefer the commit date over "now" so rebuilding the same
|
|
# commit produces the same builddate.
|
|
BUILDDATE="$(git -C "$REPO_ROOT" log -1 --format=%ct 2>/dev/null || date +%s)"
|
|
|
|
echo "==> Packaging $PKGNAME $PKGVER-$PKGREL (x86_64) for pacman"
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT INT TERM
|
|
|
|
# --- lay out the filesystem tree --------------------------------------------
|
|
# /usr/bin/thoughtsync — the same command name the .deb installs and the AppImage
|
|
# installer symlinks into ~/.local/bin, so it's identical whichever way the app
|
|
# arrived. The binary already carries this name (Cargo `[[bin]]`), which is also
|
|
# what the .desktop entry's StartupWMClass has to match.
|
|
install -Dm755 "$BINARY" "$STAGE/usr/bin/thoughtsync"
|
|
install -Dm644 "$SCRIPT_DIR/thoughtsync.desktop" \
|
|
"$STAGE/usr/share/applications/thoughtsync.desktop"
|
|
|
|
# Themed icons, so the menu entry's `Icon=thoughtsync` resolves at every size the
|
|
# desktop asks for; pixmaps is the legacy fallback for older launchers.
|
|
install -Dm644 "$SRC_TAURI/icons/32x32.png" \
|
|
"$STAGE/usr/share/icons/hicolor/32x32/apps/thoughtsync.png"
|
|
install -Dm644 "$SRC_TAURI/icons/128x128.png" \
|
|
"$STAGE/usr/share/icons/hicolor/128x128/apps/thoughtsync.png"
|
|
install -Dm644 "$SRC_TAURI/icons/128x128@2x.png" \
|
|
"$STAGE/usr/share/icons/hicolor/256x256/apps/thoughtsync.png"
|
|
install -Dm644 "$SRC_TAURI/icons/icon.png" \
|
|
"$STAGE/usr/share/icons/hicolor/512x512/apps/thoughtsync.png"
|
|
install -Dm644 "$SRC_TAURI/icons/icon.png" "$STAGE/usr/share/pixmaps/thoughtsync.png"
|
|
|
|
# Installed size in KiB, as pacman reports it.
|
|
INSTALLED_SIZE="$(du -sb "$STAGE" | cut -f1)"
|
|
|
|
# --- .PKGINFO ---------------------------------------------------------------
|
|
{
|
|
echo "# Generated by desktop/packaging/arch/package-prebuilt.sh"
|
|
echo "pkgname = $PKGNAME"
|
|
echo "pkgbase = $PKGNAME"
|
|
echo "pkgver = $PKGVER-$PKGREL"
|
|
echo "pkgdesc = $PKGDESC"
|
|
echo "url = $URL"
|
|
echo "builddate = $BUILDDATE"
|
|
echo "packager = $PACKAGER"
|
|
echo "size = $INSTALLED_SIZE"
|
|
echo "arch = x86_64"
|
|
echo "license = $LICENSE"
|
|
for d in "${DEPENDS[@]}"; do echo "depend = $d"; done
|
|
# conflict + replaces together: `conflict` is what makes pacman remove the old
|
|
# package rather than refuse the transaction, `replaces` is what makes an upgrade
|
|
# pick this one up under its new name.
|
|
for r in "${REPLACES[@]}"; do echo "conflict = $r"; echo "replaces = $r"; done
|
|
} >"$STAGE/.PKGINFO"
|
|
|
|
# --- .MTREE (optional) ------------------------------------------------------
|
|
# pacman uses .MTREE for `pacman -Qkk` file verification; it is NOT required to
|
|
# install, and generating it needs bsdtar (libarchive-tools), which the Debian CI
|
|
# image may not carry. Emit it when we can, skip loudly when we can't, rather
|
|
# than adding an apt install to the job (rule 5: the image is the toolchain).
|
|
MTREE_ENTRY=()
|
|
if command -v bsdtar >/dev/null 2>&1; then
|
|
( cd "$STAGE" && LANG=C bsdtar -czf .MTREE --format=mtree \
|
|
--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' \
|
|
.PKGINFO usr )
|
|
MTREE_ENTRY=(.MTREE)
|
|
echo " .MTREE generated (bsdtar present)"
|
|
else
|
|
echo " NOTE: bsdtar absent — packaging without .MTREE."
|
|
echo " pacman installs this fine; only 'pacman -Qkk' file verification is degraded."
|
|
fi
|
|
|
|
# --- compress ---------------------------------------------------------------
|
|
# pacman reads whatever libarchive can decompress, so degrade gracefully instead
|
|
# of hard-requiring zstd in the build image. zstd is the modern default.
|
|
if command -v zstd >/dev/null 2>&1; then
|
|
COMP_EXT="zst"; COMP_CMD=(zstd -c -T0 -19 -)
|
|
elif command -v xz >/dev/null 2>&1; then
|
|
COMP_EXT="xz"; COMP_CMD=(xz -c -T0 -)
|
|
else
|
|
COMP_EXT="gz"; COMP_CMD=(gzip -c -)
|
|
fi
|
|
|
|
mkdir -p "$OUT_DIR"
|
|
PKGFILE="$OUT_DIR/$PKGNAME-$PKGVER-$PKGREL-x86_64.pkg.tar.$COMP_EXT"
|
|
|
|
# .PKGINFO MUST be the first entry in the archive — pacman reads it as a stream
|
|
# and gives up if the metadata isn't up front. Listing it first is what guarantees
|
|
# that ordering; `usr` is appended after.
|
|
TAR=tar
|
|
command -v bsdtar >/dev/null 2>&1 && TAR=bsdtar
|
|
( cd "$STAGE" && "$TAR" -cf - .PKGINFO "${MTREE_ENTRY[@]}" usr ) | "${COMP_CMD[@]}" >"$PKGFILE"
|
|
|
|
# --- report (the CI log IS the audit trail — see header) --------------------
|
|
echo "==> Built $(basename "$PKGFILE") ($(du -h "$PKGFILE" | cut -f1), compression: $COMP_EXT)"
|
|
echo "--- .PKGINFO ---"
|
|
sed 's/^/ /' "$STAGE/.PKGINFO"
|
|
echo "--- contents ---"
|
|
"$TAR" -tf "$PKGFILE" | sed 's/^/ /'
|
|
echo "==> Install on Arch with: sudo pacman -U $(basename "$PKGFILE")"
|