Files
thoughtsync/desktop/packaging/install.sh
T
Bryan Van Deusen 544cf72735
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
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Successful in 29s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m25s
Desktop (Tauri) / Update manifest (push) Successful in 4s
install: the stable fallback is dead now that stable publishes its own bundles
It existed for one window: `stable` was a manifest-only pointer at whatever `v*`
tag had last been cut, and `stable` is the DEFAULT channel, so without the
fallback `curl … | sh` was broken for everyone between step 3 landing and the
first merge to `main`. That merge happened (`b6673c6`), and `stable` now holds
its own signed bundles at 1.0.3503145 — AppImage, deb and pacman, all resolving
by the one lookup both channels share.

Kept as a fallback it stops being a safety net and becomes a mask: the branch
only runs when `stable` has no bundles, which from here on means something is
broken, and chasing a `v*` release instead of saying so is the wrong answer.

The header now says the transition is finished and that neither channel should
be special-cased again, because the shape of that code invites re-adding it.
2026-08-29 16:59:44 -04:00

255 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 rolling build from every merge to `main`.
# dev — the rolling build from every green push to `dev`.
# Both are fixed-tag releases: the tag never moves and the assets are pruned to the
# current build, so the tag alone names the newest one. `stable` only became one in
# M314 step 3, when `main` started publishing — before that it was a manifest-only
# pointer at whatever `v*` tag somebody had last cut, and this script carried a
# fallback that chased the `v*` release its manifest named. That came out once
# `main` had published to `stable` for real (`b6673c6`); the two channels are the
# same shape now and nothing here should special-case one of them again.
# 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 build from main (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…"
# ONE lookup, both channels. Each is a release whose tag never moves and whose assets
# are pruned to the current build, so the tag alone names the newest build on that
# channel — which is exactly what an installer wants and what the in-app updater
# already reads.
json="$(curl -fsSL "$API/releases/tags/$channel" 2>/dev/null)" ||
die "the $channel channel has nothing published yet."
# 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
# Tell the app which channel it was installed from. The installer is the only thing
# that knows, and without this the app kept its own `stable` default and a dev install
# checked the stable feed — which advertises an OLDER version — reporting "up to date"
# forever (issue 2183).
#
# A plain file rather than a write into the app's SQLite store: shell has no business
# knowing that schema, and a file it can't misread is the narrowest possible contract.
# The app reads it at startup (src-tauri/src/update.rs, INSTALL_MARKER) and only acts
# when the value CHANGED, so switching channel in the app isn't undone on next launch.
#
# The directory is Tauri's app-data dir for identifier com.fabledsword.thoughtsync;
# both sides hardcode it, so a change to the identifier has to change both.
record_channel() {
marker_dir="${XDG_DATA_HOME:-$HOME/.local/share}/com.fabledsword.thoughtsync"
# Best-effort: a failure here costs the channel setting, not the install, and a
# native install run as root would only be writing into root's home anyway.
mkdir -p "$marker_dir" 2>/dev/null && printf '%s\n' "$channel" > "$marker_dir/install-channel" 2>/dev/null || true
}
# 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"
record_channel
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; }
record_channel
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
# StartupWMClass is the BINARY name, not the product name and not the AppImage
# filename: the AppImage's AppRun execs usr/bin/thoughtsync, and GTK derives
# WM_CLASS from whatever it ends up running. Anything else here means the window
# never associates with this entry and the taskbar shows a second, generic icon.
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"
record_channel
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'
# This is the one path where the app can update itself, so say what it will follow.
if [ "$channel" = "dev" ]; then
printf ' In-app updates will follow the \033[1mdev\033[0m channel.'
printf ' Change it in Sync → App updates.\n'
fi