#!/bin/sh # Xeonstrap installer for Linux and macOS. # # curl -fsSL https://xeonstrapp.pages.dev/install.sh | sh # curl -fsSL https://xeonstrapp.pages.dev/install.sh | sh -s -- --format deb # curl -fsSL https://xeonstrapp.pages.dev/install.sh | sh -s -- --uninstall # # Environment: # XEONSTRAP_FORMAT same as --format (auto, appimage, deb, rpm, tar, dmg, zip) # XEONSTRAP_VERSION same as --version, a release tag such as v1.2.3 # XEONSTRAP_DIR same as --dir, where the launcher is written (default ~/.local/bin) # # The installer only writes inside your home directory unless you ask for a # distribution package, and it always downloads release files published on # GitHub for XeonCorporationOfficial/Xeonstrap. set -eu REPO="XeonCorporationOfficial/Xeonstrap" API="https://api.github.com/repos/$REPO/releases" SITE="https://xeonstrapp.pages.dev" APP_ID="io.github.XeonCorporationOfficial.Xeonstrap" DESKTOP_NAME="$APP_ID.desktop" BIN_DIR="${XEONSTRAP_DIR:-${XDG_BIN_HOME:-$HOME/.local/bin}}" DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}" APPS_DIR="$DATA_DIR/applications" ICON_DIR="$DATA_DIR/icons/hicolor/256x256/apps" LOCAL_LIB="$DATA_DIR/xeonstrap" CONFIG_DIR="$DATA_DIR/Xeonstrap" FORMAT="${XEONSTRAP_FORMAT:-auto}" TAG="${XEONSTRAP_VERSION:-}" ACTION="install" PURGE=0 QUIET=0 RELEASE_JSON="" DOWNLOAD_DIR="" usage() { cat <<'EOF' Xeonstrap installer Usage: install.sh [options] Options: --format Package to install: auto, appimage, deb, rpm, tar, dmg, zip --version Install a specific release tag instead of the latest one --dir Directory for the launcher (default: ~/.local/bin) --uninstall Remove the files this installer created --purge Also delete Xeonstrap settings (used with --uninstall) --quiet Only print errors --help Show this message Examples: curl -fsSL https://xeonstrapp.pages.dev/install.sh | sh curl -fsSL https://xeonstrapp.pages.dev/install.sh | sh -s -- --format tar curl -fsSL https://xeonstrapp.pages.dev/install.sh | sh -s -- --uninstall --purge EOF } say() { [ "$QUIET" = 1 ] || printf '%s\n' "$*"; } warn() { printf 'warning: %s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } cleanup() { if [ -n "$DOWNLOAD_DIR" ] && [ -d "$DOWNLOAD_DIR" ]; then rm -rf "$DOWNLOAD_DIR" fi } while [ $# -gt 0 ]; do case "$1" in --format) [ $# -ge 2 ] || die "--format needs a value"; FORMAT="$2"; shift 2 ;; --format=*) FORMAT="${1#*=}"; shift ;; --version) [ $# -ge 2 ] || die "--version needs a value"; TAG="$2"; shift 2 ;; --version=*) TAG="${1#*=}"; shift ;; --dir) [ $# -ge 2 ] || die "--dir needs a value"; BIN_DIR="$2"; shift 2 ;; --dir=*) BIN_DIR="${1#*=}"; shift ;; --uninstall|-u) ACTION="uninstall"; shift ;; --purge) PURGE=1; shift ;; --quiet|-q) QUIET=1; shift ;; --help|-h) usage; exit 0 ;; *) printf 'error: unknown option %s\n\n' "$1" >&2; usage >&2; exit 2 ;; esac done # ---------- download tooling ---------- CURL="" WGET="" if command -v curl >/dev/null 2>&1; then CURL="curl" elif command -v wget >/dev/null 2>&1; then WGET="wget" else die "curl or wget is required" fi fetch() { if [ -n "$CURL" ]; then "$CURL" -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "$1" else "$WGET" -qO- --tries=3 --timeout=20 "$1" fi } download() { if [ -n "$CURL" ]; then "$CURL" -fSL --retry 3 --retry-delay 2 --connect-timeout 20 --output "$2" "$1" else "$WGET" -q --tries=3 --timeout=20 -O "$2" "$1" fi } # ---------- platform ---------- OS_NAME="$(uname -s)" OS_ARCH="$(uname -m)" PLATFORM="" MUSL=0 case "$OS_NAME" in Linux) PLATFORM="linux" ;; Darwin) PLATFORM="macos" ;; *) die "unsupported operating system: $OS_NAME. Install the Windows or Android build instead." ;; esac if [ "$PLATFORM" = "linux" ]; then case "$OS_ARCH" in x86_64|amd64) LINUX_ARCH="x86_64"; RID="linux-x64"; DEB_ARCH="amd64" ;; aarch64|arm64) LINUX_ARCH="aarch64"; RID="linux-arm64"; DEB_ARCH="arm64" ;; *) die "unsupported architecture: $OS_ARCH" ;; esac if ls /lib/ld-musl-* >/dev/null 2>&1; then MUSL=1 RID="linux-musl-${RID#linux-}" fi else case "$OS_ARCH" in arm64|aarch64) MAC_RID="osx-arm64" ;; x86_64|amd64) MAC_RID="osx-x64" ;; *) die "unsupported architecture: $OS_ARCH" ;; esac fi # ---------- release data ---------- json_value() { printf '%s' "$RELEASE_JSON" | sed -n 's/.*"'"$1"'":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1 } asset_url() { printf '%s' "$RELEASE_JSON" \ | grep -o '"browser_download_url":[[:space:]]*"[^"]*"' \ | sed 's/.*"\(https[^"]*\)"$/\1/' \ | grep -E "$1" \ | head -n 1 || true } load_release() { api_url="$API/latest" [ -n "$TAG" ] && api_url="$API/tags/$TAG" say "Looking up the release information on GitHub..." RELEASE_JSON="$(fetch "$api_url" 2>/dev/null || true)" if [ -z "$RELEASE_JSON" ] && [ -n "$TAG" ]; then RELEASE_JSON="$(fetch "$API/latest" 2>/dev/null || true)" fi if [ -z "$RELEASE_JSON" ]; then warn "GitHub did not answer the release lookup, so the newest assets are used directly" return 1 fi return 0 } latest_download() { # $1 = file name, works without the GitHub API as long as the name is stable printf 'https://github.com/%s/releases/latest/download/%s' "$REPO" "$1" } releases_page() { printf 'https://github.com/%s/releases/latest' "$REPO" } # ---------- writers ---------- write_launcher() { launcher="$BIN_DIR/xeonstrap" target="$1" mkdir -p "$BIN_DIR" cat > "$launcher" </dev/null 2>&1 \ || warn "the app icon could not be downloaded, the menu entry stays text only" fi } write_desktop_entry() { command_path="$1" mkdir -p "$APPS_DIR" cat > "$APPS_DIR/$DESKTOP_NAME" </dev/null 2>&1; then update-desktop-database "$APPS_DIR" >/dev/null 2>&1 || true fi } have_sudo() { command -v sudo >/dev/null 2>&1; } report_path() { case ":$PATH:" in *":$BIN_DIR:"*) return 0 ;; esac warn "$BIN_DIR is not in your PATH, so the 'xeonstrap' command is not available yet" say "Add this to your shell profile: export PATH=\"$BIN_DIR:\$PATH\"" } # ---------- Linux installers ---------- install_appimage() { url="$1" target="$BIN_DIR/xeonstrap.AppImage" mkdir -p "$BIN_DIR" say "Downloading the AppImage..." download "$url" "$target.part" [ -s "$target.part" ] || die "the AppImage download failed" mv "$target.part" "$target" chmod 755 "$target" write_launcher "$target" write_desktop_entry "$BIN_DIR/xeonstrap" install_icon say "Installed Xeonstrap to $target" say "Run it with: $BIN_DIR/xeonstrap" if [ ! -e /dev/fuse ]; then warn "FUSE is not available, so AppImages may refuse to start" say "In that case run it with: XEONSTRAP_APPIMAGE_EXTRACT=1 $BIN_DIR/xeonstrap" fi report_path } install_linux_package() { url="$1" kind="$2" file="$DOWNLOAD_DIR/xeonstrap.$kind" say "Downloading the .$kind package..." download "$url" "$file" if ! have_sudo || [ ! -r /dev/tty ]; then warn "the package cannot be installed without a terminal for sudo" say "The package is saved at $file" say "Install it yourself with: sudo dpkg -i '$file' (or sudo rpm -U --replacepkgs '$file')" return 1 fi say "Installing the package, sudo may ask for your password..." if [ "$kind" = "deb" ] && command -v dpkg >/dev/null 2>&1; then sudo dpkg -i "$file" /dev/null 2>&1; then sudo rpm -U --replacepkgs "$file" /dev/null || true write_launcher "$binary" write_desktop_entry "$BIN_DIR/xeonstrap" install_icon say "Installed Xeonstrap to $binary" say "Run it with: $BIN_DIR/xeonstrap" report_path } # ---------- macOS installer ---------- install_macos_image() { url="$1" extension="$2" archive="$DOWNLOAD_DIR/Xeonstrap.$extension" say "Downloading the $extension..." download "$url" "$archive" if [ -d /Applications ] && [ -w /Applications ]; then app_destination="/Applications/Xeonstrap.app" else mkdir -p "$HOME/Applications" app_destination="$HOME/Applications/Xeonstrap.app" fi app_source="" mount_point="" if [ "$extension" = "dmg" ]; then mount_point="$(mktemp -d "${TMPDIR:-/tmp}/xeonstrap-mount.XXXXXX")" hdiutil attach "$archive" -nobrowse -readonly -mountpoint "$mount_point" >/dev/null \ || die "the disk image could not be mounted" if [ -d "$mount_point/Xeonstrap.app" ]; then app_source="$mount_point/Xeonstrap.app" else app_source="$(find "$mount_point" -maxdepth 1 -name '*.app' | head -n 1)" fi [ -n "$app_source" ] || die "Xeonstrap.app was not found in the disk image" else extract_dir="$DOWNLOAD_DIR/extracted" mkdir -p "$extract_dir" ditto -x -k "$archive" "$extract_dir" >/dev/null 2>&1 || unzip -q "$archive" -d "$extract_dir" app_source="$(find "$extract_dir" -maxdepth 2 -name 'Xeonstrap.app' | head -n 1)" [ -n "$app_source" ] || die "Xeonstrap.app was not found in the archive" fi rm -rf "$app_destination" cp -R "$app_source" "$app_destination" if [ -n "$mount_point" ]; then hdiutil detach "$mount_point" >/dev/null 2>&1 || true rmdir "$mount_point" 2>/dev/null || true fi # The build is not notarized by Apple, so the quarantine flag would block it. xattr -dr com.apple.quarantine "$app_destination" >/dev/null 2>&1 || true say "Installed Xeonstrap to $app_destination" say "If macOS still refuses to open it, right click the app and choose Open." } # ---------- uninstall ---------- uninstall() { rm -f "$BIN_DIR/xeonstrap" "$BIN_DIR/xeonstrap.AppImage" rm -f "$APPS_DIR/$DESKTOP_NAME" rm -f "$ICON_DIR/$APP_ID.png" rm -rf "$LOCAL_LIB" if [ "$PLATFORM" = "macos" ]; then rm -rf "$HOME/Applications/Xeonstrap.app" 2>/dev/null || true rm -rf /Applications/Xeonstrap.app 2>/dev/null || true fi say "Removed the Xeonstrap launcher and menu entry" if [ "$PURGE" = 1 ]; then rm -rf "$CONFIG_DIR" say "Removed your Xeonstrap settings at $CONFIG_DIR" else say "Your settings were kept at $CONFIG_DIR. Add --purge to delete them too." fi } # ---------- main ---------- if [ "$ACTION" = "uninstall" ]; then uninstall exit 0 fi DOWNLOAD_DIR="$(mktemp -d "${TMPDIR:-/tmp}/xeonstrap-install.XXXXXX")" trap cleanup EXIT HAVE_RELEASE=0 if load_release; then HAVE_RELEASE=1 fi if [ "$PLATFORM" = "linux" ]; then if [ "$FORMAT" = "auto" ]; then if [ "$MUSL" = 1 ]; then FORMAT="tar" else FORMAT="appimage" fi fi case "$FORMAT" in appimage) url="$(asset_url "_${LINUX_ARCH}\.AppImage$")" [ -n "$url" ] || die "no AppImage was found for this architecture, check $(releases_page)" install_appimage "$url" ;; deb) url="$(asset_url "${DEB_ARCH}\.deb$")" [ -n "$url" ] || die "no .deb package was found for this architecture, check $(releases_page)" install_linux_package "$url" deb ;; rpm) url="$(asset_url "_${LINUX_ARCH}\.rpm$")" [ -n "$url" ] || die "no .rpm package was found for this architecture, check $(releases_page)" install_linux_package "$url" rpm ;; tar|tarball|archive) url="$(asset_url "${RID}\.tar\.gz$")" [ -n "$url" ] || url="$(asset_url "\.tar\.gz$")" [ -n "$url" ] || die "no tarball was found for this architecture, check $(releases_page)" install_tarball "$url" ;; *) die "unsupported format for Linux: $FORMAT (try appimage, deb, rpm or tar)" ;; esac else if [ "$FORMAT" = "auto" ]; then FORMAT="dmg" fi case "$FORMAT" in dmg) url="$(asset_url "${MAC_RID}\.dmg$")" if [ -z "$url" ] && [ "$HAVE_RELEASE" = 0 ]; then url="$(latest_download "Xeonstrap-${MAC_RID}.dmg")" fi [ -n "$url" ] || die "no disk image was found for this Mac" install_macos_image "$url" dmg ;; zip) url="$(asset_url "${MAC_RID}\.zip$")" if [ -z "$url" ] && [ "$HAVE_RELEASE" = 0 ]; then url="$(latest_download "Xeonstrap-${MAC_RID}.zip")" fi [ -n "$url" ] || die "no archive was found for this Mac" install_macos_image "$url" zip ;; *) die "unsupported format for macOS: $FORMAT (try dmg or zip)" ;; esac fi say "" say "Done. Thanks for using Xeonstrap." say "Website: $SITE ยท Releases: https://github.com/$REPO/releases"