Files
inkwell/desktop/packaging/deb/verify.sh
T
bvandeusenandClaude Opus 5.5 01b53118b9
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Web typecheck and unit tests (push) Successful in 8s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Successful in 1m3s
CI & Build / integration (push) Successful in 1m37s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m14s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m3s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m42s
Android / Build the server image (push) Successful in 1s
Core and desktop comments describe the code to a stranger (#6224)
Milestone 546 "Bring the repo to the comment standard (rule 32) and
compose defaults (rule 198)", step #6224 "Comment standard sweep: Rust
core and desktop client".

Doc comments and comments across inkwell-core, the Tauri app and
desktop/packaging now say what the code does and the non-obvious why in
the present tense. Gone: milestone tags (M10.x, M11.3, M13, M304, M315,
M314, milestone 325), issue and Scribe-note numbers, family-idea and
family-rule citations, the run numbers and commit hashes behind past
bugs, and the narration of what used to be (the stable channel before it
held its own bundles, CARGO_PKG_VERSION as the shown version, the
MANIFEST_TAG split, the store's missing tests, the old PKGBUILD). The
schema's migration notes keep what each step does and why, without
"until now". The forge is called the forge rather than Forgejo.

Hazards stay as facts: the dev-rolling tag that must not shadow the
branch, the manifest matched on this build's version, the APK's fixed
name surviving the prune, the LIKE guard in v9, -wal/-shm moving with
the renamed store, panic = abort ending the app.

34 files, comments only: a comment-stripping diff of every changed file
against HEAD is identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:30:37 -04:00

188 lines
9.1 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, and its Depends
# line is whatever tauri infers. 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. This 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". 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, so a productName with a hump
# ("ThoughtSync") splits into `thought-sync`. "Inkwell" has none, so the package
# and the command agree; a productName that brought a 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 — the drift that groups the app 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