From 8a8b2b17e620fba7e5769b456bba5b76dcf7312b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 25 Jul 2026 18:19:30 -0400 Subject: [PATCH] desktop: prebuilt pacman package + verified .deb (tasks 2022, 2074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native packages the installer can actually fetch, before task 2014 wires up the fetching. Arch (task 2022, re-scoped): the source PKGBUILD is gone — asking every user to install rust+node and compile for minutes isn't distribution. Replaced by desktop/packaging/arch/package-prebuilt.sh, which wraps the binary the Linux job already built into a .pkg.tar.zst. No second Rust build, no Arch CI image: the binary bundles nothing and resolves webkit/gtk/soup by soname, identical on both distros, with SQLite compiled in and glibc used in the safe built-old/run-new direction. CI is Debian and has no pacman, so the step logs .PKGINFO plus the full file listing for audit instead of pretending to verify. Debian (task 2074): install.sh hands the .deb to every Debian/Ubuntu user and nothing had ever inspected it. tauri.conf.json now declares libwebkit2gtk-4.1-0 + libgtk-3-0 explicitly rather than trusting inference — and deliberately declares no appindicator or sqlite dep, since tauri is built with features=[] and rusqlite is "bundled". desktop/packaging/deb/verify.sh prints the generated control file, cross-checks it against what the ELF actually needs via dpkg-shlibdeps, confirms every declared dep exists in apt, and clean-container installs when a docker CLI is available. Both artifacts join the run artifact and the tagged release; install.sh grows a pacman branch so Arch/CachyOS gets a native install instead of the AppImage fallback. Still no release cut (rule 2). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi --- .forgejo/workflows/desktop.yml | 19 +++ ci-requirements.md | 17 ++- desktop/packaging/arch/PKGBUILD | 68 --------- desktop/packaging/arch/README.md | 61 +++++--- desktop/packaging/arch/package-prebuilt.sh | 155 +++++++++++++++++++++ desktop/packaging/deb/verify.sh | 145 +++++++++++++++++++ desktop/packaging/install.sh | 50 +++++-- desktop/packaging/publish-release.sh | 4 +- desktop/src-tauri/tauri.conf.json | 7 +- 9 files changed, 428 insertions(+), 98 deletions(-) delete mode 100644 desktop/packaging/arch/PKGBUILD create mode 100755 desktop/packaging/arch/package-prebuilt.sh create mode 100755 desktop/packaging/deb/verify.sh diff --git a/.forgejo/workflows/desktop.yml b/.forgejo/workflows/desktop.yml index 02e6347..3bb1877 100644 --- a/.forgejo/workflows/desktop.yml +++ b/.forgejo/workflows/desktop.yml @@ -78,6 +78,24 @@ jobs: - name: De-bundle AppImage graphics libraries run: bash desktop/packaging/appimage/debundle-graphics.sh + # install.sh hands the .deb to every Debian/Ubuntu user, so the package's + # Depends must be right BEFORE a release exists. Prints the generated + # control file and cross-checks it against what the ELF actually needs + # (dpkg-shlibdeps). tauri.conf.json declares libwebkit2gtk-4.1-0 + libgtk-3-0 + # explicitly rather than trusting inference alone; there's deliberately no + # appindicator or sqlite dep (tauri features = [], rusqlite is "bundled"). + - name: Verify the .deb + run: bash desktop/packaging/deb/verify.sh + + # Repackage the binary just built into a native pacman package, so Arch / + # CachyOS gets a real native install from install.sh instead of the AppImage + # fallback — without a second Rust build or an Arch CI image. Safe because + # nothing is bundled: the binary resolves webkit/gtk by soname, which is + # identical across the two distros. Can't be pacman-tested here (Debian + # runner), so the step logs .PKGINFO + the full file listing for audit. + - name: Package for Arch (pacman) + run: bash desktop/packaging/arch/package-prebuilt.sh + # Make the built .deb + .AppImage downloadable from the run (for hand-testing). # continue-on-error: the Forgejo artifact backend may not be configured yet; a # failed upload must not fail the build itself. @@ -91,6 +109,7 @@ jobs: path: | desktop/src-tauri/target/release/bundle/appimage/*.AppImage desktop/src-tauri/target/release/bundle/deb/*.deb + desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* if-no-files-found: warn # Tag builds only: publish a real, versioned Fabled-Git Release with the diff --git a/ci-requirements.md b/ci-requirements.md index 641d06d..cc768a0 100644 --- a/ci-requirements.md +++ b/ci-requirements.md @@ -59,8 +59,23 @@ backend/frontend push. - **Steps:** build the shared frontend (embedded by `generate_context!`) → `cargo tauri icon app-icon.png` (platform icon set from the committed 1024px source) → `cargo fmt --check` → `cargo clippy -D warnings` → `cargo test` → - `cargo tauri build` (produces `.deb` + `.AppImage`). + `cargo tauri build` (produces `.deb` + `.AppImage`) → de-bundle the AppImage's + graphics libs → verify the `.deb` → repackage for pacman. - **`APPIMAGE_EXTRACT_AND_RUN=1`** is set: AppImage tooling FUSE-mounts by default and CI containers have no `/dev/fuse`. +- **Packaging tools used from the image** (none installed at job time, rule 5): + `dpkg-deb` / `dpkg-query` / `apt-cache` and `dpkg-shlibdeps` (from `dpkg-dev`, + pulled in by `build-essential`) for `desktop/packaging/deb/verify.sh`; `tar` + + a compressor for `desktop/packaging/arch/package-prebuilt.sh`. Both scripts + degrade gracefully rather than hard-failing on an absent optional tool: + `bsdtar` (`libarchive-tools`) is used for the pacman package's `.MTREE` when + present and skipped when not, compression falls back zstd → xz → gzip, and the + `.deb` clean-container install test runs only if a docker CLI is available. + Adding `libarchive-tools` + `zstd` + a docker CLI to `ci-tauri` would upgrade + those degraded paths, but none of them block a green build. +- **Not verifiable in CI:** the runner is Debian, so the pacman package cannot be + `pacman -U`-tested here. That step logs `.PKGINFO` + the full file listing so + the package is auditable from the run log; a real Arch install is the operator's + confirm. - No Postgres lane (unchanged): the desktop app's local store + sync behavior is verified on the operator's machine, not in CI. diff --git a/desktop/packaging/arch/PKGBUILD b/desktop/packaging/arch/PKGBUILD deleted file mode 100644 index 67d852d..0000000 --- a/desktop/packaging/arch/PKGBUILD +++ /dev/null @@ -1,68 +0,0 @@ -# Maintainer: bvandeusen -# -# ThoughtSync desktop (Tauri v2) — native Arch package. -# -# Built from THIS repo checkout, from SOURCE, linked against your SYSTEM libraries. -# That is the whole point: it uses the host's graphics stack (webkit2gtk-4.1 + your -# GPU driver), which avoids the bundled-AppImage "EGL_BAD_PARAMETER" black window. -# -# Usage (from a clone of the thoughtsync repo): -# cd desktop/packaging/arch -# makepkg -si -# -pkgname=thoughtsync-desktop -pkgver=0.1.0 -pkgrel=1 -pkgdesc="ThoughtSync desktop — local-first Keep-style thought capture" -arch=('x86_64') -url="https://git.fabledsword.com/bvandeusen/thoughtsync" -license=('MIT') -depends=( - 'webkit2gtk-4.1' - 'gtk3' - 'cairo' - 'gdk-pixbuf2' - 'glib2' - 'libsoup3' - 'pango' - 'hicolor-icon-theme' - 'desktop-file-utils' - 'librsvg' -) -makedepends=('rust' 'nodejs' 'npm' 'git' 'pkgconf') -# Cargo's release profile already does its own LTO; don't let makepkg inject C-level -# LTO flags into the crates that build native code. -options=('!lto') - -# Locate the repo root this PKGBUILD lives inside (desktop/packaging/arch/). -_repo() { git -C "$startdir" rev-parse --show-toplevel; } - -pkgver() { - cd "$startdir" - printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" -} - -build() { - local repo; repo="$(_repo)" - - # 1) Build the shared Vue frontend — it's embedded into the binary at compile time - # (tauri generate_context!), so it must exist before the cargo build. - cd "$repo/frontend" - npm ci - npm run build - - # 2) Compile the native binary against SYSTEM libraries (no bundling, no tauri-cli). - cd "$repo/desktop/src-tauri" - cargo build --release -} - -package() { - local repo; repo="$(_repo)" - - install -Dm755 "$repo/desktop/src-tauri/target/release/thoughtsync-desktop" \ - "$pkgdir/usr/bin/thoughtsync" - install -Dm644 "$repo/desktop/src-tauri/app-icon.png" \ - "$pkgdir/usr/share/pixmaps/thoughtsync.png" - install -Dm644 "$startdir/thoughtsync.desktop" \ - "$pkgdir/usr/share/applications/thoughtsync.desktop" -} diff --git a/desktop/packaging/arch/README.md b/desktop/packaging/arch/README.md index 0ae6b92..4ce6033 100644 --- a/desktop/packaging/arch/README.md +++ b/desktop/packaging/arch/README.md @@ -1,31 +1,60 @@ # ThoughtSync desktop — Arch package -A native Arch/pacman package, built **from source** so it links against your -**system** libraries (webkit2gtk-4.1 + your GPU driver). This is the robust install -on Arch/CachyOS: it avoids the bundled-AppImage `EGL_BAD_PARAMETER` black-window -problem entirely, and it installs a proper application-menu entry. +A **prebuilt** native pacman package (`.pkg.tar.zst`), published as an asset on +every ThoughtSync release. Nothing to compile, no toolchain to install. + +Installing natively on Arch matters for more than tidiness: pacman pulls +`webkit2gtk-4.1` itself and the app runs against your system's graphics stack, +which is what keeps the bundled-library `EGL_BAD_PARAMETER` black window +(issue 2021) from coming back. It also means the app is package-manager tracked +and uninstalls cleanly. ## Install -From a clone of the `thoughtsync` repo: +Easiest — the one-command installer picks this package automatically on any +pacman system: ```sh -cd desktop/packaging/arch -makepkg -si +curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/main/desktop/packaging/install.sh | sh ``` -`makepkg` will pull the build tools (`rust`, `nodejs`, `npm`) and runtime deps -(`webkit2gtk-4.1`, `gtk3`, …) via pacman, compile the frontend + the Rust binary, -and install: +Or grab the `.pkg.tar.zst` from the +[latest release](https://git.fabledsword.com/bvandeusen/thoughtsync/releases/latest) +and install it directly: + +```sh +sudo pacman -U thoughtsync-desktop-*-x86_64.pkg.tar.zst +``` + +Either way you get: - `/usr/bin/thoughtsync` — the app - `/usr/share/applications/thoughtsync.desktop` — the menu entry -- `/usr/share/pixmaps/thoughtsync.png` — the icon +- `/usr/share/icons/hicolor/*/apps/thoughtsync.png` — themed icons -Then launch **ThoughtSync** from your app menu (or run `thoughtsync`). +Launch **ThoughtSync** from your app menu, or run `thoughtsync`. -## Notes +Uninstall: `sudo pacman -R thoughtsync-desktop`. -- Builds in-place from the checkout (`makepkg` reads the committed state via git). -- The compile takes a few minutes the first time (Rust release build). -- Uninstall: `sudo pacman -R thoughtsync-desktop`. +## How the package is built + +`package-prebuilt.sh` runs in CI (`.forgejo/workflows/desktop.yml`) and wraps the +binary the Linux build already produced into a pacman package — it does not +compile anything a second time. + +Packaging a Debian-compiled binary for Arch is safe here because the binary +bundles nothing: it resolves `libwebkit2gtk-4.1.so.0`, `libgtk-3.so.0` and +`libsoup-3.0.so.0` by SONAME at runtime and those are identical on both distros, +SQLite is compiled in (`rusqlite` "bundled"), and glibc's forward compatibility +means building on Debian's older glibc and running on Arch's newer one is the +safe direction. + +CI is Debian and has no `pacman`, so it cannot install-test the result. The build +step instead logs the package's `.PKGINFO` and complete file listing, so the +package is auditable from the run log; a real `pacman -U` is the final proof. + +## Previously + +This directory used to hold a source-build `PKGBUILD` requiring `makepkg -si` +with `rust` + `nodejs` + `npm` installed. It was removed once prebuilt packages +shipped — asking every user to install a compiler toolchain isn't distribution. diff --git a/desktop/packaging/arch/package-prebuilt.sh b/desktop/packaging/arch/package-prebuilt.sh new file mode 100755 index 0000000..f4ddf1c --- /dev/null +++ b/desktop/packaging/arch/package-prebuilt.sh @@ -0,0 +1,155 @@ +#!/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-desktop" +OUT_DIR="${1:-$SRC_TAURI/target/release/bundle/arch}" + +PKGNAME="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 tauri.conf.json value the +# .deb and the AppImage are stamped with, so all three artifacts on a release +# always agree. Plain grep — jq is not guaranteed in the CI image. +# `|| true` so a miss falls through to the explicit error below rather than +# aborting on pipefail with no explanation. +PKGVER="$(grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' "$SRC_TAURI/tauri.conf.json" | + head -1 | sed -E 's/.*"([^"]+)"$/\1/' || true)" +[ -n "$PKGVER" ] || { echo "ERROR: could not read version from tauri.conf.json" >&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 (not thoughtsync-desktop): matches the CLI name the +# AppImage installer symlinks into ~/.local/bin, so the command is the same +# whichever way the app was installed. +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 +} >"$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")" diff --git a/desktop/packaging/deb/verify.sh b/desktop/packaging/deb/verify.sh new file mode 100755 index 0000000..80ba38a --- /dev/null +++ b/desktop/packaging/deb/verify.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# +# Verify the .deb that `cargo tauri build` produced is a correct, installable +# native package — BEFORE the one-command installer starts handing it to people. +# +# WHY: install.sh prefers the .deb on every Debian/Ubuntu machine, but until now +# nothing had ever inspected it, let alone installed it. Its Depends line was +# entirely whatever tauri inferred, unread. The failure mode we're guarding is a +# user running install.sh and getting either an apt resolution error or an app +# that installs and then won't launch because a library it needs was never +# declared. +# +# Four checks, cheapest first: +# 1. Print the control file + contents — the generated metadata becomes ground +# truth in the build log instead of an assumption. +# 2. dpkg-shlibdeps: the canonical Debian answer for "what does this ELF +# actually need". Compared against what the package declares. +# 3. Every declared dependency resolves to a real package in apt (catches a +# typo in the hand-written list, which would break install for everyone). +# 4. If a docker CLI is present, install into a clean debian container — the +# highest-fidelity check, since the build image already has the -dev +# packages installed and so can't prove resolution on its own. +# +# Check 4 is opportunistic on purpose: the build image is not guaranteed to carry +# a docker CLI, and adding one at job time would violate "the image is the +# toolchain" (rule 5). Checks 1-3 are self-contained and always run. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +DEB_DIR="$REPO_ROOT/desktop/src-tauri/target/release/bundle/deb" + +shopt -s nullglob +DEBS=("$DEB_DIR"/*.deb) +[ ${#DEBS[@]} -gt 0 ] || { echo "ERROR: no .deb under $DEB_DIR — did the tauri build run?" >&2; exit 1; } +DEB="${DEBS[0]}" +echo "==> Verifying $(basename "$DEB")" + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT INT TERM + +fail=0 +note_fail() { echo "FAIL: $1" >&2; fail=1; } + +# --- 1. control metadata + contents ----------------------------------------- +echo +echo "--- control ---" +dpkg-deb -I "$DEB" | sed 's/^/ /' +echo "--- contents ---" +dpkg-deb -c "$DEB" | awk '{print $1, $6}' | sed 's/^/ /' + +# Declared runtime deps, normalised: strip version constraints, split +# alternatives ("a | b" both count as declared), one package name per line. +declared="$(dpkg-deb -f "$DEB" Depends 2>/dev/null | + tr ',|' '\n\n' | sed -E 's/\(.*\)//; s/^[[:space:]]*//; s/[[:space:]]*$//' | + grep -v '^$' | sort -u || true)" +echo +echo "--- declared Depends ---" +printf '%s\n' "$declared" | sed 's/^/ /' + +dpkg-deb -x "$DEB" "$WORK/root" +BIN="$(find "$WORK/root" -type f -path '*/bin/*' -print -quit)" +[ -n "$BIN" ] || { echo "ERROR: no binary found under */bin/ in the package" >&2; exit 1; } +echo " (binary: ${BIN#"$WORK/root"})" + +# --- 2. what the ELF actually needs ----------------------------------------- +if command -v dpkg-shlibdeps >/dev/null 2>&1; then + # dpkg-shlibdeps insists on a debian/control in the working directory even with + # -O (write to stdout); a stub is enough to let it do the ELF analysis. + mkdir -p "$WORK/deb-stub/debian" + printf 'Source: thoughtsync\n\nPackage: thoughtsync\nArchitecture: amd64\n' \ + >"$WORK/deb-stub/debian/control" + # `|| true`: pipefail is on and dpkg-shlibdeps still exits non-zero on some + # symbol warnings even with --ignore-missing-info. An empty result degrades to + # "check skipped" below — it must not abort the whole verification. + required="$(cd "$WORK/deb-stub" && + dpkg-shlibdeps -O --ignore-missing-info "$BIN" 2>/dev/null | + sed -E 's/^shlibs:Depends=//' | tr ',' '\n' | + sed -E 's/\(.*\)//; s/^[[:space:]]*//; s/[[:space:]]*$//' | grep -v '^$' | sort -u || true)" + + echo + echo "--- required by the binary (dpkg-shlibdeps) ---" + printf '%s\n' "$required" | sed 's/^/ /' + + # A package may be satisfied transitively — we deliberately declare only + # webkit2gtk + gtk3 and let glib/cairo/pango/gdk-pixbuf/libsoup3 arrive through + # them, so "required but not declared" is only a real problem if it's also + # unreachable from the declared set. Ask apt for that closure. + closure="$(apt-cache depends --recurse --no-recommends --no-suggests \ + --no-conflicts --no-breaks --no-replaces --no-enhances -i $declared 2>/dev/null | + grep -E '^[a-zA-Z0-9]' | sed -E 's/[<>]//g' | sort -u || true)" + + if [ -z "$required" ]; then + echo "WARN: dpkg-shlibdeps returned nothing — skipping the coverage check." >&2 + elif [ -z "$closure" ]; then + echo "WARN: apt has no package index here — skipping the transitive-coverage check." >&2 + else + for pkg in $required; do + # Essential / required-priority packages (libc6, libgcc-s1 …) are present on + # every Debian system by definition; declaring them is noise, not safety. + prio="$(dpkg-query -W -f='${Priority} ${Essential}' "$pkg" 2>/dev/null || true)" + case "$prio" in *required*|*important*|*yes*) continue ;; esac + printf '%s\n' "$closure" | grep -qx "$pkg" && continue + printf '%s\n' "$declared" | grep -qx "$pkg" && continue + note_fail "$pkg is needed by the binary but is neither declared nor reachable from the declared deps." + done + [ "$fail" -eq 0 ] && echo "OK: every non-essential library the binary needs is covered." + fi +else + echo "WARN: dpkg-shlibdeps unavailable — skipping the ELF dependency check." >&2 +fi + +# --- 3. declared deps are real packages ------------------------------------- +if apt-cache policy dpkg >/dev/null 2>&1 && [ -n "$(apt-cache policy dpkg 2>/dev/null)" ]; then + for pkg in $declared; do + if [ -z "$(apt-cache policy "$pkg" 2>/dev/null)" ]; then + note_fail "declared dependency '$pkg' does not exist in apt — install would fail for every user." + fi + done + [ "$fail" -eq 0 ] && echo "OK: every declared dependency exists in apt." +fi + +# --- 4. clean-container install (opportunistic) ----------------------------- +# The build image already has libwebkit2gtk-4.1-dev etc. installed, so installing +# here would pass no matter what we declared. Only a pristine container proves +# apt can actually resolve the package for a real user. +if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + echo + echo "==> Clean-container install test (debian:bookworm)" + if docker run --rm -v "$DEB:/tmp/$(basename "$DEB"):ro" debian:bookworm \ + sh -c "apt-get update -qq && apt-get install -y --no-install-recommends '/tmp/$(basename "$DEB")'"; then + echo "OK: the .deb installs cleanly on a stock debian:bookworm." + else + note_fail "the .deb did NOT install on a stock debian:bookworm." + fi +else + echo + echo "NOTE: no usable docker CLI — skipping the clean-container install test." + echo " Checks 1-3 above still gate the package; a real-machine install is the operator's confirm." +fi + +echo +[ "$fail" -eq 0 ] && { echo "==> .deb verification passed."; exit 0; } +echo "==> .deb verification FAILED." >&2 +exit 1 diff --git a/desktop/packaging/install.sh b/desktop/packaging/install.sh index cf6eb63..dfc9cbd 100755 --- a/desktop/packaging/install.sh +++ b/desktop/packaging/install.sh @@ -6,8 +6,9 @@ # # Fetches the LATEST published release for this machine's architecture and # installs it, ending with a working app + menu entry. Native-first: -# * Debian/Ubuntu (dpkg+apt) -> the native .deb (system libs; needs sudo). -# * everything else (Arch/CachyOS/Fedora/…) -> the de-bundled AppImage, +# * Arch/CachyOS (pacman) -> the native .pkg.tar.zst (system libs; needs sudo). +# * Debian/Ubuntu (dpkg+apt) -> the native .deb (system libs; needs sudo). +# * everything else (Fedora/openSUSE/…) -> the de-bundled AppImage, # installed user-locally (no sudo). The AppImage's graphics libs are # stripped in CI (see debundle-graphics.sh), so it uses the host GPU stack # and renders where a stock Tauri AppImage would black-window (issue 2021). @@ -45,25 +46,52 @@ json="$(curl -fsSL "$API/releases/latest" 2>/dev/null)" || die \ # .AppImage/.deb URLs present are the asset download links. appimage_url="$(printf '%s' "$json" | grep -oE 'https?://[^"]+\.AppImage' | head -1 || true)" deb_url="$(printf '%s' "$json" | grep -oE 'https?://[^"]+\.deb' | head -1 || true)" +pkg_url="$(printf '%s' "$json" | grep -oE 'https?://[^"]+\.pkg\.tar\.[a-z]+' | head -1 || true)" version="$(printf '%s' "$json" | grep -oE '"tag_name":"[^"]+"' | head -1 | sed -E 's/.*:"([^"]+)".*/\1/')" -[ -n "$appimage_url" ] || [ -n "$deb_url" ] || die "the latest release has no installable Linux asset." +[ -n "$appimage_url" ] || [ -n "$deb_url" ] || [ -n "$pkg_url" ] || + die "the latest release has no installable Linux asset." say "Latest release: ${version:-unknown}" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT INT TERM +# Both native paths install system-wide, so they need root. Resolved once here +# rather than duplicated per branch; the AppImage path below never calls this. +need_root() { + if [ "$(id -u)" -eq 0 ]; then sudo=""; else + have sudo || die "a native install needs root; re-run as root or install sudo." + sudo="sudo" + fi + say "Installing (you may be prompted for your password)…" +} + +# --- native pacman path (Arch/CachyOS/Manjaro) ------------------------------ +# Preferred over the AppImage on Arch: pacman pulls webkit2gtk-4.1 itself and the +# app then runs against the host graphics stack, which is what keeps the +# EGL_BAD_PARAMETER black window (issue 2021) from coming back. It also means the +# app is tracked by the package manager and uninstalls cleanly. +if have pacman && [ -n "$pkg_url" ]; then + say "Arch-family system detected — installing the native pacman package" + # Keep the published filename: pacman -U expects a *.pkg.tar.* name and refuses + # a file that doesn't look like a package, whatever its actual contents. + pkg_file="$tmp/$(basename "$pkg_url")" + curl -fSL -o "$pkg_file" "$pkg_url" + need_root + $sudo pacman -U --noconfirm "$pkg_file" + say "Done. Launch ThoughtSync from your application menu, or run thoughtsync." + exit 0 +fi + # --- native .deb path (Debian/Ubuntu) --------------------------------------- if have dpkg && have apt-get && [ -n "$deb_url" ]; then say "Debian-family system detected — installing the native .deb" curl -fSL -o "$tmp/thoughtsync.deb" "$deb_url" - if [ "$(id -u)" -eq 0 ]; then sudo=""; else - have sudo || die "installing the .deb needs root; re-run as root or install sudo." - sudo="sudo" - fi - say "Installing (you may be prompted for your password)…" - # apt-get resolves the .deb's dependencies (webkit2gtk etc.); dpkg is the - # fallback if this apt is too old for local-file installs. - $sudo apt-get install -y "$tmp/thoughtsync.deb" || $sudo dpkg -i "$tmp/thoughtsync.deb" + need_root + # apt-get resolves the .deb's dependencies (webkit2gtk etc.). dpkg is the + # fallback if this apt is too old for local-file installs — it leaves the deps + # unconfigured, so `apt-get -f install` is what actually completes that path. + $sudo apt-get install -y "$tmp/thoughtsync.deb" || + { $sudo dpkg -i "$tmp/thoughtsync.deb" || true; $sudo apt-get -f install -y; } say "Done. Launch ThoughtSync from your application menu." exit 0 fi diff --git a/desktop/packaging/publish-release.sh b/desktop/packaging/publish-release.sh index 9c7a79e..ccdad23 100755 --- a/desktop/packaging/publish-release.sh +++ b/desktop/packaging/publish-release.sh @@ -16,6 +16,7 @@ # The build + de-bundle steps run first; this consumes their output: # desktop/src-tauri/target/release/bundle/appimage/*.AppImage (de-bundled) # desktop/src-tauri/target/release/bundle/deb/*.deb +# desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman) # # Instance-agnostic: server + repo come from the runner's github.* context # (Forgejo populates them for compatibility), so nothing is hardcoded to one host. @@ -42,6 +43,7 @@ shopt -s nullglob ASSETS=( "$BUNDLE_ROOT"/appimage/*.AppImage "$BUNDLE_ROOT"/deb/*.deb + "$BUNDLE_ROOT"/arch/*.pkg.tar.* ) if [ ${#ASSETS[@]} -eq 0 ]; then echo "ERROR: no bundles under $BUNDLE_ROOT — did the tauri build run?" >&2 @@ -74,7 +76,7 @@ first_id() { grep -oE '"id"[[:space:]]*:[[:space:]]*[0-9]+' | head -1 | grep -oE echo "==> Creating release for $TAG" BODY=$(cat <