Files
thoughtsync/desktop/packaging/install.sh
T
bvandeusen edf52da97f
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m19s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m3s
Desktop (Tauri) / Update manifest (push) Successful in 4s
desktop: the installer's channel choice now reaches the app (issue 2183)
`install.sh --channel dev` set the channel in the installer and nowhere
else. The app kept its `stable` default, stable advertises 0.1.0, and
0.1.0 is older than any dev build — so every update check said "up to
date", forever, and the user had to know to go set it themselves.

The installer now records the channel as a plain file in the app-data
dir; the app adopts it at startup. A file rather than a write into the
app's SQLite store, because shell has no business knowing that schema.

Adoption compares against the value last adopted, not against "is the
pref unset". Seeding only when unset would have fixed the first install
and left the second silently wrong: install stable, then install dev,
and the pref is already set so dev never takes. Comparing to the last
marker makes both directions work — an in-app channel switch survives
the next launch, and re-running the installer on a different channel is
honoured.

An unreadable marker is ignored rather than read as `stable`, so a
truncated file can't move someone off the channel they're on.
2026-08-15 21:38:58 -04:00

267 lines
12 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
# 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
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