Files
thoughtsync/desktop/packaging/install.sh
T
bvandeusenandClaude Opus 5 d8b0cd9b96
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 2m36s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 4m32s
Desktop (Tauri) / Update manifest (push) Has been skipped
packaging: the installer learns the same two channels the app updates on
install.sh asked /releases/latest and installed whatever came back. That is
v0.1.0 today, which predates the updater, and it was about to get worse: the
`stable` pointer release write-manifest.sh creates is non-prerelease and holds
only latest.json, so from the next v* tag onward it would have WON
/releases/latest and the installer would have found nothing to install.

So resolve a channel instead of a "latest". `--channel stable|dev` (or
TS_CHANNEL), default stable, named to match update.rs's Channel exactly. dev
reads /releases/tags/dev. stable reads the pointer's own latest.json, takes its
version, and installs that v* release — the same file the app reads, so the
installer and the updater cannot disagree about what stable means.

Two things found on the way. The dev release's description still told people to
run the stable command, and always would have: publish-release.sh writes a body
only when it CREATES a release, and a fixed-tag release is only created once, so
the text froze at the first build. The 409 path now PATCHes it. And the asset
greps were unanchored, so a .AppImage.sig URL could match as the bundle URL —
harmless by coincidence, not by construction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MKsUY9Z45KQd34V956hZ9Q
2026-07-27 23:03:12 -04:00

245 lines
11 KiB
Bash
Executable File

#!/bin/sh
#
# ThoughtSync desktop — one-command Linux installer.
#
# curl -fsSL https://git.fabledsword.com/bvandeusen/thoughtsync/raw/branch/dev/desktop/packaging/install.sh | sh
#
# Two channels, the SAME two the app's own updater offers (src-tauri/src/update.rs):
# stable (default) — the newest tagged v* release.
# dev — the rolling build from every green push to `dev`.
# Pick one with `--channel dev` or `TS_CHANNEL=dev`. Through a pipe the options go
# after a `--`: curl -fsSL <url> | sh -s -- --channel dev
#
# Served from `dev` rather than `main`: `main` exists but trails day-to-day work by
# a long way, so the copy there would install an older script. Move the documented
# URL to `main` after a dev→main merge lands, not before.
#
# Fetches the LATEST published release on the chosen channel for this machine's
# architecture and installs it, ending with a working app + menu entry. Native-first:
# * 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).
#
# POSIX sh (dash-safe) so `curl … | sh` works everywhere. Dependency-light and
# auditable on purpose — read it before you pipe it.
set -eu
INSTANCE="https://git.fabledsword.com"
REPO="bvandeusen/thoughtsync"
API="$INSTANCE/api/v1/repos/$REPO"
say() { printf '==> %s\n' "$1"; }
die() { printf 'error: %s\n' "$1" >&2; exit 1; }
have() { command -v "$1" >/dev/null 2>&1; }
usage() {
cat <<'USAGE'
ThoughtSync desktop installer.
install.sh [--channel stable|dev]
--channel stable newest tagged release (default)
--channel dev rolling build from the latest green push to `dev`
-h, --help this text
The channel can also come from TS_CHANNEL. Through a pipe, pass options after
`--`: curl -fsSL <url> | sh -s -- --channel dev
USAGE
}
# --- channel ----------------------------------------------------------------
channel="${TS_CHANNEL:-stable}"
while [ $# -gt 0 ]; do
case "$1" in
--channel)
[ $# -ge 2 ] || die "--channel needs a value (stable or dev)."
channel="$2"; shift 2 ;;
--channel=*) channel="${1#*=}"; shift ;;
-h | --help) usage; exit 0 ;;
*) die "unknown option: $1 (try --help)" ;;
esac
done
case "$channel" in
stable | dev) : ;;
*) die "unknown channel '$channel' — expected stable or dev." ;;
esac
have curl || die "curl is required."
# --- architecture gate ------------------------------------------------------
# Only x86_64 is built today; arm64 will be added when the CI matrix grows. The
# release-asset naming carries the arch, but since only one arch ships now we
# match by file extension below and just guard the arch here.
arch="$(uname -m)"
case "$arch" in
x86_64 | amd64) : ;;
*) die "ThoughtSync ships x86_64 Linux builds only right now (this machine: $arch)." ;;
esac
# --- resolve the release for this channel -----------------------------------
say "Finding the latest ThoughtSync build on the $channel channel…"
if [ "$channel" = "dev" ]; then
# A release whose tag never moves and whose assets are pruned to the current
# build — so the tag alone always names the newest dev build.
json="$(curl -fsSL "$API/releases/tags/dev" 2>/dev/null)" ||
die "the dev channel has nothing published yet."
else
# Ask the stable channel's own manifest which version is current, then install
# THAT release. This is the same file the in-app updater reads, so the installer
# and the updater can never disagree about what `stable` means.
#
# Not `/releases/latest`: that returns the newest non-prerelease release by date,
# and the `stable` pointer release (manifest only, no bundles — see
# write-manifest.sh) is itself a non-prerelease created moments after the
# versioned one. It would win, and it carries nothing installable.
manifest="$(curl -fsSL "$INSTANCE/$REPO/releases/download/stable/latest.json" 2>/dev/null || true)"
stable_version="$(printf '%s' "$manifest" |
grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 |
sed -E 's/.*"([^"]+)"$/\1/')"
if [ -n "$stable_version" ]; then
json="$(curl -fsSL "$API/releases/tags/v$stable_version" 2>/dev/null)" ||
die "the stable channel names $stable_version, but there is no v$stable_version release to install."
else
# No stable pointer yet — the channel predates the updater. Fall back to the
# newest non-prerelease release, which is what stable meant before there was
# a manifest to ask.
json="$(curl -fsSL "$API/releases/latest" 2>/dev/null)" ||
die "no stable release published yet — try --channel dev, or ask the maintainer to tag one."
fi
fi
# Pull asset URLs straight out of the release JSON (no jq). Anchored on the closing
# quote so a `…AppImage.sig` URL can't be truncated into a match of its own.
asset_url() {
printf '%s' "$json" | grep -oE "https?://[^\"]+$1\"" | head -1 | tr -d '"'
}
appimage_url="$(asset_url '\.AppImage')"
deb_url="$(asset_url '\.deb')"
pkg_url="$(asset_url '\.pkg\.tar\.[a-z]+')"
version="$(printf '%s' "$json" | grep -oE '"tag_name":"[^"]+"' | head -1 | sed -E 's/.*:"([^"]+)".*/\1/')"
[ -n "$appimage_url" ] || [ -n "$deb_url" ] || [ -n "$pkg_url" ] ||
die "the $channel release (${version:-unknown}) has no installable Linux asset."
say "Installing ${version:-unknown} from the $channel channel"
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)…"
}
# Both native paths are package-manager-owned, so the app cannot replace itself
# in place (update.rs refuses, by design). Say so at the end of those paths rather
# than letting someone discover it from a greyed-out button.
native_update_note() {
printf ' A package-manager install can'\''t update itself in-app.\n'
if [ "$channel" = "dev" ]; then
printf ' Re-run this script with --channel dev to move to a newer dev build.\n'
else
printf ' Re-run this script to move to a newer release.\n'
fi
}
# --- 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."
native_update_note
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"
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."
native_update_note
exit 0
fi
# --- universal AppImage path (user-local, no sudo) --------------------------
# Install into ~/Applications/ThoughtSync.AppImage — the SAME location the app's
# own self-integration uses (src/integration.rs) — so the running app sees
# itself already installed and never makes a second copy or menu entry.
say "Installing the de-bundled AppImage (user-local, no sudo)"
[ -n "$appimage_url" ] || die "the $channel release (${version:-unknown}) has no AppImage asset."
apps_dir="$HOME/Applications"
dest="$apps_dir/ThoughtSync.AppImage"
mkdir -p "$apps_dir"
say "Downloading $(basename "$appimage_url")…"
curl -fSL -o "$tmp/ThoughtSync.AppImage" "$appimage_url"
chmod +x "$tmp/ThoughtSync.AppImage"
mv -f "$tmp/ThoughtSync.AppImage" "$dest"
# Menu entry — written to match integration.rs verbatim (same paths + fields),
# so the app reports is_integrated=true and won't duplicate it.
apps_menu="$HOME/.local/share/applications"
icons_dir="$HOME/.local/share/icons"
mkdir -p "$apps_menu" "$icons_dir"
# Best-effort: pull the real icon out of the AppImage (.DirIcon) so the menu
# entry looks right immediately. Extraction is a non-GUI unsquash (no FUSE, no
# black-window risk); if it fails we fall back to the themed name and the app
# writes its embedded icon on first launch anyway.
icon_ref="thoughtsync"
if ( cd "$tmp" && "$dest" --appimage-extract .DirIcon >/dev/null 2>&1 ) \
&& cp -L "$tmp/squashfs-root/.DirIcon" "$icons_dir/thoughtsync.png" 2>/dev/null; then
icon_ref="$icons_dir/thoughtsync.png"
fi
rm -rf "$tmp/squashfs-root" 2>/dev/null || true
cat > "$apps_menu/thoughtsync.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=ThoughtSync
Comment=Capture a fleeting thought in a second
Exec=$dest %U
Icon=$icon_ref
Terminal=false
Categories=Utility;Office;
StartupWMClass=ThoughtSync
EOF
have update-desktop-database && update-desktop-database "$apps_menu" >/dev/null 2>&1 || true
# Convenience CLI launcher.
mkdir -p "$HOME/.local/bin"
ln -sf "$dest" "$HOME/.local/bin/thoughtsync"
say "Installed to $dest"
printf ' Launch it from your application menu, or run \033[1mthoughtsync\033[0m'
printf ' (if ~/.local/bin is on your PATH).\n'
# The AppImage CAN update itself, but the app's own channel is a separate,
# stable-by-default setting — installing from dev here does not move it.
if [ "$channel" = "dev" ]; then
printf ' You installed from the \033[1mdev\033[0m channel — set the app to match in\n'
printf ' Sync → App updates → Development, or in-app updates will follow stable.\n'
fi