Files
bvandeusenandClaude Opus 5.5 fe6f0746b2 rename: the desktop is Inkwell — crates, Tauri identity, data move, packaging
Step 3 of milestone 481 (Scribe note 5071: a full rename).

- crates thoughtsync-{core,desktop,ffi,uniffi-bindgen} → inkwell-*, the
  Cargo.lock entries moved to match (checked with `cargo metadata --locked`)
- Tauri: productName "Inkwell", identifier com.fabledsword.inkwell, binary
  `inkwell`, updater feed on bvandeusen/inkwell, store file inkwell.db
- client agent inkwell-desktop, headers X-Inkwell-Client/-Protocol (the server
  reads neither), capture event inkwell://captured, display-version env
- .deb: conflicts + replaces thought-sync, so the updater's install retires the
  old package instead of colliding on it. kebab-case("Inkwell") is `inkwell`, so
  the package name finally matches the command and verify.sh now asserts it
- pacman: inkwell, conflicting with and replacing thoughtsync and
  thoughtsync-desktop
- AppImage ~/Applications/Inkwell.AppImage, menu entry inkwell.desktop,
  installer, release titles, desktop asset names in fetch-clients.sh

The one shim, chosen by the operator because it is the only copy of a
local-first user's notes: crossover.rs moves the old
com.fabledsword.thoughtsync app-data dir's contents into the new one on startup,
before the store opens, renaming thoughtsync.db and its -wal/-shm with it. It
skips when the new dir already has a store, and anything already in the new dir
wins (the installer writes its channel marker there first). Tested.

Android's Kotlin side (package, applicationId, uniffi class) is step 4. Its
release asset names stay thoughtsync.* until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:27:26 -04:00

190 lines
9.3 KiB
Bash
Executable File

#!/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.
#
# Five checks, cheapest first:
# 1. Print the control file + contents — the generated metadata becomes ground
# truth in the build log instead of an assumption.
# 2. Naming: the binary is /usr/bin/inkwell and the generated .desktop
# entry's StartupWMClass matches it. The app used to identify itself three
# different ways depending on install channel (issue 2075); this is what
# keeps the .deb — the only channel whose entry Tauri generates for us —
# from drifting away from the two we write by hand.
# 3. dpkg-shlibdeps: the canonical Debian answer for "what does this ELF
# actually need". Compared against what the package declares.
# 4. Every declared dependency resolves to a real package in apt (catches a
# typo in the hand-written list, which would break install for everyone).
# 5. 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 5 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-4 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/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. naming is consistent -------------------------------------------------
# CANON is the one name the app answers to everywhere: the binary, the CLI
# command, the icon, and the WM_CLASS the window reports. Hardcoded here on
# purpose — this literal IS the contract the three install channels are held to.
#
# The control file's `Package:` field is held to it too. tauri-bundler derives it
# as kebab-case(productName) with no override, which split "ThoughtSync" into
# `thought-sync` (issue 2075). "Inkwell" has no hump to split, so the package and
# the command finally agree, and a productName that brought the split back would
# also break the `conflicts`/`replaces` retiring the old package.
CANON="inkwell"
package="$(dpkg-deb -f "$DEB" Package 2>/dev/null || true)"
[ "$package" = "$CANON" ] ||
note_fail "the package is named '${package:-<unset>}', expected '$CANON' (kebab-case of productName)."
installed_bin="${BIN#"$WORK/root"}"
[ "$installed_bin" = "/usr/bin/$CANON" ] ||
note_fail "binary is at $installed_bin, expected /usr/bin/$CANON (mainBinaryName in tauri.conf.json)."
# Tauri generates this entry from the binary name, so a mismatch means the config
# and the bundler have diverged — exactly the drift that made the app group under
# a different taskbar icon depending on how it was installed.
entry="$(find "$WORK/root/usr/share/applications" -name '*.desktop' -print -quit 2>/dev/null || true)"
if [ -z "$entry" ]; then
note_fail "no .desktop entry in the package — the app would not appear in any menu."
else
echo "--- desktop entry ($(basename "$entry")) ---"
sed 's/^/ /' "$entry"
wmclass="$(sed -n 's/^StartupWMClass=//p' "$entry" | head -1)"
[ "$wmclass" = "$CANON" ] ||
note_fail "StartupWMClass is '${wmclass:-<unset>}', expected '$CANON' — the taskbar icon will not group."
# `%u`/`%U` field codes may follow, so match the first word only.
exec_cmd="$(sed -n 's/^Exec=//p' "$entry" | head -1 | awk '{print $1}')"
[ "$exec_cmd" = "$CANON" ] ||
note_fail "Exec runs '${exec_cmd:-<unset>}', expected '$CANON'."
fi
[ "$fail" -eq 0 ] && echo "OK: binary, Exec and StartupWMClass all agree on '$CANON'."
# --- 3. 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: inkwell\n\nPackage: inkwell\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
# --- 4. 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
# --- 5. 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