#!/bin/sh # # The marker above is for scripts/sync-versions.mjs, which treats this file as # source alongside the HTML. It is a shell comment here and an HTML comment # there, which is the whole trick. Without it the ZYRN_VERSION examples below get # rewritten to the headline version on a release where Linux did not move, and a # copy-pasted example then downloads a package that was never built. # # ZYRNTOPO Linux installer. # # curl -fsSL https://zyrntopo.com/install.sh | sh # # Picks the right package for your distribution and CPU architecture, verifies its # SHA-256 against the published checksum file, and installs it. Falls back to a # self-contained AppImage (no root, no package manager) on distributions we do not # ship a native package for. Re-running it upgrades an existing install. # # Architectures: x86_64 (amd64) and arm64 (aarch64). There is no 32-bit build for # either x86 or ARM — see the architecture block below for why. # # Commands: # install (default) install, or upgrade an existing install in place # uninstall remove ZYRNTOPO, whichever way it was installed # version print the installed and the current published version # help this text # # Options (env vars): # ZYRN_VERSION=1.15.15 pin a version (only the current release is hosted) # ZYRN_METHOD=appimage force the AppImage path even on deb/rpm/arch systems # ZYRN_PREFIX=~/.local where the AppImage install goes (default ~/.local) # # POSIX sh on purpose: this has to run under dash/busybox on minimal systems, # so no bashisms, no arrays, no [[ ]]. set -eu BASE_URL="https://pois.zyrntopo.com/downloads" VERSION_URL="https://zyrntopo.com/version.json" PREFIX="${ZYRN_PREFIX:-$HOME/.local}" RED=''; GRN=''; YLW=''; DIM=''; RST='' if [ -t 1 ] && [ "${TERM:-dumb}" != "dumb" ]; then RED=$(printf '\033[31m'); GRN=$(printf '\033[32m'); YLW=$(printf '\033[33m') DIM=$(printf '\033[2m'); RST=$(printf '\033[0m') fi say() { printf '%s\n' "$*"; } info() { printf '%s==>%s %s\n' "$GRN" "$RST" "$*"; } warn() { printf '%s warning:%s %s\n' "$YLW" "$RST" "$*" >&2; } die() { printf '%s error:%s %s\n' "$RED" "$RST" "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1; } ACTION="${ZYRN_ACTION:-${1:-install}}" usage() { cat <<'USAGE' ZYRNTOPO installer First install: curl -fsSL https://zyrntopo.com/install.sh | sh After that, manage it with the zyrntopo command: zyrntopo launch the app zyrntopo update upgrade to the current release zyrntopo version show installed and current versions zyrntopo remove uninstall (keeps your saved maps) zyrntopo help this text Architectures: x86_64 (amd64) and arm64 (aarch64), detected automatically. There is no 32-bit build — Electron dropped 32-bit Linux in 2019. Environment: ZYRN_VERSION=1.15.15 pin a version (only the current release is hosted) ZYRN_METHOD=appimage force the no-root AppImage even on deb/rpm/Arch ZYRN_PREFIX=~/.local where the AppImage install lives (default ~/.local) Native packages are installed directly rather than from a repository, so `apt upgrade` and friends will not see new ZYRNTOPO releases — use `zyrntopo update`. USAGE } case "$ACTION" in help|-h|--help) usage; exit 0 ;; update|upgrade) ACTION=install ;; remove) ACTION=uninstall ;; esac # ── the `zyrntopo` command ─────────────────────────────────────────────────── # Written to PATH at install time so the app is managed the way any other program # is — `zyrntopo update`, not a curl pipeline the user has to keep to hand. Bare # `zyrntopo` still launches the app, so this replaces nothing from the user's side. # # $1 = path to the real executable the bare command should exec. write_cli() { _target="$1"; _dest="$2"; _sudo="${3:-}" $_sudo sh -c "cat > '$_dest'" </dev/null | grep -q .; then printf 'deb %s\n' "$(dpkg-query -W -f='${Version}' zyrntopo-desktop 2>/dev/null)" elif need rpm && rpm -q zyrntopo-desktop >/dev/null 2>&1; then printf 'rpm %s\n' "$(rpm -q --qf '%{VERSION}' zyrntopo-desktop 2>/dev/null)" elif need pacman && pacman -Q zyrntopo-desktop >/dev/null 2>&1; then printf 'pacman %s\n' "$(pacman -Q zyrntopo-desktop 2>/dev/null | awk '{print $2}')" else printf 'not installed\n' fi } # ── uninstall ──────────────────────────────────────────────────────────────── if [ "$ACTION" = "uninstall" ] || [ "$ACTION" = "remove" ]; then REMOVED=0 FAILED=0 # Each removal is failure-tolerant on purpose. Under `set -e` a package manager # that exits non-zero — most commonly because sudo has no TTY to prompt on — # aborted the whole script, so anything listed after it was silently skipped and # the uninstall finished half-done while reporting nothing. if need dpkg-query && dpkg-query -W zyrntopo-desktop >/dev/null 2>&1; then info "Removing the .deb package"; sudo_for if $SUDO apt-get remove -y zyrntopo-desktop 2>/dev/null || $SUDO dpkg -r zyrntopo-desktop then REMOVED=1; else warn "could not remove the .deb package"; FAILED=1; fi fi if need rpm && rpm -q zyrntopo-desktop >/dev/null 2>&1; then info "Removing the .rpm package"; sudo_for if need dnf; then $SUDO dnf remove -y zyrntopo-desktop elif need zypper; then $SUDO zypper --non-interactive remove zyrntopo-desktop else $SUDO rpm -e zyrntopo-desktop fi && REMOVED=1 || { warn "could not remove the .rpm package"; FAILED=1; } fi if need pacman && pacman -Q zyrntopo-desktop >/dev/null 2>&1; then info "Removing the pacman package"; sudo_for if $SUDO pacman -R --noconfirm zyrntopo-desktop then REMOVED=1; else warn "could not remove the pacman package"; FAILED=1; fi fi # The management CLI is installed outside any package, so nothing else removes it. if [ -e /usr/local/bin/zyrntopo ]; then sudo_for; $SUDO rm -f /usr/local/bin/zyrntopo; REMOVED=1 fi if [ -e "$PREFIX/lib/zyrntopo/zyrntopo.AppImage" ] || [ -e "$PREFIX/bin/zyrntopo" ]; then info "Removing the AppImage install from $PREFIX" rm -f "$PREFIX/bin/zyrntopo" "$PREFIX/share/applications/zyrntopo.desktop" rm -rf "$PREFIX/lib/zyrntopo" rm -f "$PREFIX"/share/icons/hicolor/*/apps/zyrntopo.png need update-desktop-database && update-desktop-database "$PREFIX/share/applications" 2>/dev/null || true need gtk-update-icon-cache && gtk-update-icon-cache -qtf "$PREFIX/share/icons/hicolor" 2>/dev/null || true REMOVED=1 fi if [ "$REMOVED" -eq 0 ] && [ "$FAILED" -eq 0 ]; then warn "ZYRNTOPO does not appear to be installed." exit 0 fi if [ "$FAILED" -ne 0 ]; then say "" die "some parts could not be removed (see the warnings above). If sudo could not prompt, re-run it from a terminal: zyrntopo remove" fi say "" info "ZYRNTOPO removed." # Saved maps and downloaded tiles live in the Electron user-data directory and # are deliberately left alone — uninstalling the app must not delete field data. say "${DIM} Your saved maps and offline tiles were kept, in ~/.config/ZYRNTOPO.${RST}" say "${DIM} Delete them too with: rm -rf ~/.config/ZYRNTOPO${RST}" exit 0 fi # ── version ────────────────────────────────────────────────────────────────── VERSION="${ZYRN_VERSION:-}" if [ -z "$VERSION" ]; then info "Resolving the current release…" VERSION_JSON=$(fetch_stdout "$VERSION_URL" 2>/dev/null | tr -d ' \t\r\n') || true # ── Linux can legitimately lag the headline version ────────────────────── # The Linux packages and the Windows installers are built on different # machines (RELEASING.md §4b), so a Windows-only release moves "version" # while every linux-* artifact stays where it was. version.json declares # that lag as platforms.linux, and THAT is what names the file downloaded # below — reading "version" here would build a URL for an AppImage that was # never published and 404 on every Linux install of that release. # # This is not hypothetical: 1.14.3 shipped Windows and the web app while # Linux stayed on 1.14.2. Until then the lagging platform had always been # Windows, which this script does not serve, so the bug could not appear. VERSION=$(printf '%s' "$VERSION_JSON" \ | sed -n 's/.*"platforms":{[^}]*"linux":"\([^"]*\)".*/\1/p') [ -n "$VERSION" ] || VERSION=$(printf '%s' "$VERSION_JSON" \ | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') [ -n "$VERSION" ] || die "could not read the current version from $VERSION_URL. Pin one explicitly: ZYRN_VERSION=1.15.15 sh install.sh" fi if [ "$ACTION" = "version" ]; then say "installed: $(installed_version)" say "published: $VERSION" exit 0 fi info "ZYRNTOPO $VERSION ($ARCH)" # ── pick the package for this distro ───────────────────────────────────────── # Order matters: check the package manager that actually owns the system, not # merely one that happens to be installed (a box can have both dpkg and rpm). METHOD="${ZYRN_METHOD:-}" if [ -z "$METHOD" ]; then if need apt-get || need dpkg; then METHOD=deb elif need pacman; then METHOD=pacman elif need dnf || need yum || need zypper || need rpm; then METHOD=rpm else METHOD=appimage fi fi # The architecture token in the filename depends on the package format as well as # the CPU: electron-builder follows each packaging ecosystem's own convention, so # ONE build is published under several different spellings, and both architectures # split. Do not try to derive these — this table is what the archive actually # holds, copied from electron-builder's getArtifactArchName: # # x86_64 arm64 # deb linux-amd64.deb linux-arm64.deb # rpm linux-x86_64.rpm linux-aarch64.rpm # pacman linux-x64.pacman linux-aarch64.pacman # AppImage linux-x86_64.AppImage linux-arm64.AppImage if [ "$ZARCH" = "arm64" ]; then DEB_ARCH=arm64; RPM_ARCH=aarch64; PAC_ARCH=aarch64; APP_ARCH=arm64 else DEB_ARCH=amd64; RPM_ARCH=x86_64; PAC_ARCH=x64; APP_ARCH=x86_64 fi case "$METHOD" in deb) FILE="zyrntopo-$VERSION-linux-$DEB_ARCH.deb" ;; rpm) FILE="zyrntopo-$VERSION-linux-$RPM_ARCH.rpm" ;; pacman) FILE="zyrntopo-$VERSION-linux-$PAC_ARCH.pacman" ;; appimage) FILE="zyrntopo-$VERSION-linux-$APP_ARCH.AppImage" ;; *) die "unknown ZYRN_METHOD '$METHOD' (expected deb, rpm, pacman or appimage)." ;; esac TMP=$(mktemp -d "${TMPDIR:-/tmp}/zyrntopo.XXXXXXXX") || die "could not create a temp directory." cleanup() { rm -rf "$TMP"; } trap cleanup EXIT INT TERM # ── download ───────────────────────────────────────────────────────────────── info "Downloading $FILE" say "${DIM} $BASE_URL/$FILE${RST}" fetch "$BASE_URL/$FILE" "$TMP/$FILE" \ || die "download failed. Is $VERSION a published release? Browse what is available: https://zyrntopo.com/#download" # ── verify ─────────────────────────────────────────────────────────────────── # A tampered or truncated download is worse than no download, and this script is # routinely piped straight into a shell — so the checksum is not optional. It is # only skipped when the machine genuinely has no sha256 tool. if fetch "$BASE_URL/SHA256SUMS-$VERSION.txt" "$TMP/SHA256SUMS" 2>/dev/null; then WANT=$(sed -n "s/^\([0-9a-f]\{64\}\) *$FILE\$/\1/p" "$TMP/SHA256SUMS" | head -n1) if [ -n "$WANT" ]; then if need sha256sum; then GOT=$(sha256sum "$TMP/$FILE" | cut -d' ' -f1) elif need shasum; then GOT=$(shasum -a 256 "$TMP/$FILE" | cut -d' ' -f1) else GOT=""; warn "no sha256sum/shasum on this system — skipping verification." fi if [ -n "$GOT" ]; then [ "$GOT" = "$WANT" ] || die "CHECKSUM MISMATCH for $FILE. expected $WANT got $GOT The download was corrupted or tampered with. Nothing was installed." info "Checksum verified." fi else warn "no checksum listed for $FILE — continuing unverified." fi else warn "checksum file unavailable — continuing unverified." fi # ── privilege ──────────────────────────────────────────────────────────────── SUDO="" [ "$METHOD" = "appimage" ] || sudo_for # ── install ────────────────────────────────────────────────────────────────── case "$METHOD" in deb) info "Installing with apt" if need apt-get; then $SUDO apt-get install -y "$TMP/$FILE" else $SUDO dpkg -i "$TMP/$FILE" || { $SUDO apt-get -f install -y && $SUDO dpkg -i "$TMP/$FILE"; } fi ;; rpm) if need dnf; then info "Installing with dnf"; $SUDO dnf install -y "$TMP/$FILE" elif need zypper; then info "Installing with zypper"; $SUDO zypper --non-interactive install --allow-unsigned-rpm "$TMP/$FILE" elif need yum; then info "Installing with yum"; $SUDO yum install -y "$TMP/$FILE" else info "Installing with rpm"; $SUDO rpm -Uvh --replacepkgs "$TMP/$FILE" fi ;; pacman) info "Installing with pacman" $SUDO pacman -U --noconfirm "$TMP/$FILE" ;; appimage) # No package manager involved: drop the payload in PREFIX/lib, put a launcher # in PREFIX/bin, and register a .desktop entry so it appears in the # application menu like a real install. info "Installing the AppImage into $PREFIX" mkdir -p "$PREFIX/bin" "$PREFIX/lib/zyrntopo" "$PREFIX/share/applications" "$PREFIX/share/icons/hicolor/512x512/apps" install -m755 "$TMP/$FILE" "$PREFIX/lib/zyrntopo/zyrntopo.AppImage" # The .desktop entry below says Icon=zyrntopo, which resolves only if an # icon of that name sits in a standard hicolor size directory. Unpack the # icon tree the AppImage already carries and copy every size across, so the # launcher, the dock and the alt-tab switcher each pick the size they want. # # Not .DirIcon: that is a symlink into usr/share/icons, and copying it # verbatim installs a dangling link rather than an image. # curl/wget leave the download non-executable, so the AppImage cannot run # its own --appimage-extract until this chmod. Without it the extraction # below fails silently and every install lands with a generic icon. chmod +x "$TMP/$FILE" ( cd "$TMP" && "./$FILE" --appimage-extract 'usr/share/icons/*' >/dev/null 2>&1 ) || true ICONS_FOUND=0 for src in "$TMP"/squashfs-root/usr/share/icons/hicolor/*/apps/zyrntopo.png; do [ -f "$src" ] || continue dim=$(basename "$(dirname "$(dirname "$src")")") mkdir -p "$PREFIX/share/icons/hicolor/$dim/apps" install -m644 "$src" "$PREFIX/share/icons/hicolor/$dim/apps/zyrntopo.png" ICONS_FOUND=$((ICONS_FOUND + 1)) done if [ "$ICONS_FOUND" -gt 0 ]; then need gtk-update-icon-cache && \ gtk-update-icon-cache -qtf "$PREFIX/share/icons/hicolor" 2>/dev/null || true else warn "could not extract the application icon — the launcher will use a generic one." fi # An AppImage mounts itself with FUSE 2, which most current distributions no # longer ship (Arch, Fedora 38+, Ubuntu 22.04+ all dropped libfuse2). Running # the raw AppImage there dies with "AppImages require FUSE to run", which is a # miserable first experience. AppImage's own --appimage-extract-and-run needs # no FUSE at all, so the launcher falls back to it when libfuse2 is missing — # FUSE stays the default because extract-and-run unpacks on every start. cat > "$PREFIX/lib/zyrntopo/launch" <<'LAUNCHER' #!/bin/sh APPIMAGE="$(dirname "$(readlink -f "$0")")/zyrntopo.AppImage" if (ldconfig -p 2>/dev/null | grep -q 'libfuse\.so\.2') \ || [ -e /usr/lib/libfuse.so.2 ] || [ -e /usr/lib64/libfuse.so.2 ] \ || [ -e /usr/lib/x86_64-linux-gnu/libfuse.so.2 ] \ || [ -e /usr/lib/aarch64-linux-gnu/libfuse.so.2 ]; then exec "$APPIMAGE" "$@" fi exec "$APPIMAGE" --appimage-extract-and-run "$@" LAUNCHER chmod 755 "$PREFIX/lib/zyrntopo/launch" write_cli "$PREFIX/lib/zyrntopo/launch" "$PREFIX/bin/zyrntopo" cat > "$PREFIX/share/applications/zyrntopo.desktop" </dev/null || true case ":$PATH:" in *":$PREFIX/bin:"*) ;; *) warn "$PREFIX/bin is not on your PATH — add it to run 'zyrntopo' from a shell: echo 'export PATH=\"$PREFIX/bin:\$PATH\"' >> ~/.profile" ;; esac ;; esac # Native packages put the Electron binary on PATH themselves (/usr/bin/zyrntopo), # which would shadow the management CLI. /usr/local/bin precedes /usr/bin on every # mainstream distribution, so the wrapper wins there and still execs the real # binary for a bare `zyrntopo`. if [ "$METHOD" != "appimage" ]; then for real in /opt/ZYRNTOPO/zyrntopo /usr/lib/zyrntopo-desktop/zyrntopo; do [ -x "$real" ] || continue $SUDO mkdir -p /usr/local/bin write_cli "$real" /usr/local/bin/zyrntopo "$SUDO" break done fi say "" info "ZYRNTOPO $VERSION installed." say "${DIM} zyrntopo update / version / remove — manage it from the terminal${RST}" say "${DIM} Launch it from your application menu, or run: zyrntopo${RST}" say "${DIM} Docs: https://zyrntopo.com/documentation${RST}" #