#!/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="$SRC_TAURI/target/release/thoughtsync" OUT_DIR="${1:-$SRC_TAURI/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 " # 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. PKGVER="$(sh "$SCRIPT_DIR/../build-version.sh" || 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")"