#!/bin/bash # Managed by @git.zone/cli from assets/templates/asset_binary_installer. Do not edit by hand. set -euo pipefail SHOW_HELP=0 DRY_RUN=0 SPECIFIED_VERSION="" SPECIFIED_MAJOR="" INSTALL_DIR="/opt/moxytool" BIN_DIR="/usr/local/bin" INSTALL_MODE="binary" GITEA_BASE_URL="https://code.foss.global" GITEA_REPO="serve.zone/moxytool" REPO_URL="https://code.foss.global/serve.zone/moxytool.git" SERVICE_RESTART_NAME="moxytool" PRIMARY_PRESERVE_PATH="" # Every transfer is an HTTPS release asset or the Gitea API: no other protocol, also not on a redirect, # at most 10 redirects, and the same limits as the npm postinstall (30 s to connect or without data, # 15 minutes per transfer). CURL_OPTS=(--proto '=https' --proto-redir '=https' --max-redirs 10 --connect-timeout 30 --speed-limit 1 --speed-time 30 --max-time 900) # A release tag is a SemVer 2.0 version with an optional v; nothing else goes into a download URL. RELEASE_TAG_REGEX='^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' SERVICE_NAMES=( ) LEGACY_SERVICE_NAMES=( ) while [[ $# -gt 0 ]]; do case "$1" in -h|--help) SHOW_HELP=1 shift ;; --version) if [[ $# -lt 2 ]]; then echo "Error: --version requires a value" exit 1 fi SPECIFIED_VERSION="$2" shift 2 ;; --dry-run) DRY_RUN=1 shift ;; --install-dir) if [[ $# -lt 2 ]]; then echo "Error: --install-dir requires a value" exit 1 fi INSTALL_DIR="$2" shift 2 ;; *) echo "Unknown option: $1" echo "Use -h or --help for usage information" exit 1 ;; esac done if [[ $SHOW_HELP -eq 1 ]]; then echo "MOXYTOOL Installer Script" echo "Downloads and installs pre-compiled MOXYTOOL release binaries." echo "" echo "Usage: $0 [options]" echo "" echo "Options:" echo " -h, --help Show this help message" echo " --version VERSION Install a specific release: vX.Y.Z or X.Y.Z (latest: the newest)" echo " --install-dir DIR Installation directory (default: /opt/moxytool)" echo " --dry-run Resolve the version and check the release assets; change nothing" echo "" echo "Examples:" echo " curl -sSL https://code.foss.global/serve.zone/moxytool/releases/download//install.sh | sudo bash" echo " curl -sSL https://code.foss.global/serve.zone/moxytool/releases/download//install.sh | sudo bash -s -- --version vX.Y.Z" echo " curl -sSL https://code.foss.global/serve.zone/moxytool/releases/download//install.sh | bash -s -- --dry-run" exit 0 fi # Succeeds when the value matches the extended regular expression in the C locale. Bash compiles the # expression with the current locale's collation, where a range such as [A-Za-z] can match accented letters; # the local LC_ALL applies to this match only and leaves the caller's locale as it was. matches_ascii_pattern() { local LC_ALL=C [[ "$1" =~ $2 ]] } # Prints the release tag of a version (with its v), or fails when it is not a SemVer 2.0 version. normalize_release_tag() { if ! matches_ascii_pattern "$1" "$RELEASE_TAG_REGEX"; then printf 'Error: %s is not a MOXYTOOL release tag: %q\n' "$2" "$1" >&2 echo "Expected a SemVer version such as v1.2.3, 1.2.3 or v1.2.3-beta.1" >&2 exit 1 fi echo "v${1#v}" } # --version latest is the same as no --version: the newest release. if [[ "$SPECIFIED_VERSION" == "latest" ]]; then SPECIFIED_VERSION="" elif [[ -n "$SPECIFIED_VERSION" ]]; then SPECIFIED_VERSION=$(normalize_release_tag "$SPECIFIED_VERSION" "--version argument") || exit 1 fi if [[ -n "$SPECIFIED_VERSION" && -n "$SPECIFIED_MAJOR" ]]; then echo "Error: --version and --major are mutually exclusive" exit 1 fi if [[ -n "$SPECIFIED_MAJOR" ]] && ! matches_ascii_pattern "$SPECIFIED_MAJOR" '^[0-9]+$'; then echo "Error: --major must be a numeric major version, for example: --major 1" exit 1 fi if [[ $DRY_RUN -eq 0 && "$EUID" -ne 0 ]]; then echo "Please run as root (sudo bash install.sh or pipe to sudo bash)" exit 1 fi case "$INSTALL_DIR" in ""|"/") echo "Error: unsafe install directory: $INSTALL_DIR" exit 1 ;; esac require_command() { if ! command -v "$1" >/dev/null 2>&1; then echo "Error: required command not found: $1" exit 1 fi } ensure_pnpm() { if command -v pnpm >/dev/null 2>&1; then return fi if command -v corepack >/dev/null 2>&1; then corepack enable fi if ! command -v pnpm >/dev/null 2>&1; then echo "Error: pnpm is required for source installs. Install Node.js with corepack/pnpm first." exit 1 fi } get_latest_version() { echo "Fetching latest release version from Gitea..." >&2 local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest" local response if ! response=$(curl "${CURL_OPTS[@]}" -fsSL "$api_url" 2>/dev/null); then echo "Error: Failed to fetch latest release information from Gitea API" >&2 echo "URL: $api_url" >&2 exit 1 fi local version version=$(printf '%s' "$response" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p') if [[ -z "$version" ]]; then echo "Error: Could not determine latest version from API response" >&2 exit 1 fi normalize_release_tag "$version" "tag_name from the Gitea releases API" } detect_binary_name() { local os local arch local os_name local arch_name os=$(uname -s) arch=$(uname -m) case "$os" in Linux) os_name="linux" ;; Darwin) os_name="macos" ;; MINGW*|MSYS*|CYGWIN*) os_name="windows" ;; *) echo "Error: Unsupported operating system: $os" >&2 echo "Supported platforms: Linux x64, Linux ARM64, macOS x64, macOS ARM64, Windows x64" >&2 exit 1 ;; esac case "$arch" in x86_64|amd64) arch_name="x64" ;; aarch64|arm64) arch_name="arm64" ;; *) echo "Error: Unsupported architecture: $arch" >&2 echo "Supported platforms: Linux x64, Linux ARM64, macOS x64, macOS ARM64, Windows x64" >&2 exit 1 ;; esac case "${os_name}-${arch_name}" in linux-x64) echo "moxytool-linux-x64" ;; linux-arm64) echo "moxytool-linux-arm64" ;; macos-x64) echo "moxytool-macos-x64" ;; macos-arm64) echo "moxytool-macos-arm64" ;; windows-x64) echo "moxytool-windows-x64.exe" ;; *) echo "Error: Unsupported platform: ${os_name}-${arch_name}" >&2 echo "Supported platforms: Linux x64, Linux ARM64, macOS x64, macOS ARM64, Windows x64" >&2 exit 1 ;; esac } # Sets NOTICE_FILES to the third-party notice files the release ships for an executable. select_notice_files() { NOTICE_FILES=() case "$1" in "moxytool-linux-x64") NOTICE_FILES+=("moxytool-linux-x64.third-party-notices.txt") NOTICE_FILES+=("moxytool-linux-x64.third-party-notices.json") ;; "moxytool-linux-arm64") NOTICE_FILES+=("moxytool-linux-arm64.third-party-notices.txt") NOTICE_FILES+=("moxytool-linux-arm64.third-party-notices.json") ;; "moxytool-macos-x64") NOTICE_FILES+=("moxytool-macos-x64.third-party-notices.txt") NOTICE_FILES+=("moxytool-macos-x64.third-party-notices.json") ;; "moxytool-macos-arm64") NOTICE_FILES+=("moxytool-macos-arm64.third-party-notices.txt") NOTICE_FILES+=("moxytool-macos-arm64.third-party-notices.json") ;; "moxytool-windows-x64.exe") NOTICE_FILES+=("moxytool-windows-x64.exe.third-party-notices.txt") NOTICE_FILES+=("moxytool-windows-x64.exe.third-party-notices.json") ;; esac } # The extra release assets installed beside the executable, as name:mode:repository path. The mode is the # file's mode in the Git index when the installer was generated. EXTRA_FILES=( "uninstall.sh:0755:uninstall.sh" ) # Sets RELEASE_ASSETS to every release file a binary install of the executable downloads. select_release_assets() { local extra select_notice_files "$1" RELEASE_ASSETS=("$1" ${NOTICE_FILES[@]+"${NOTICE_FILES[@]}"}) for extra in ${EXTRA_FILES[@]+"${EXTRA_FILES[@]}"}; do RELEASE_ASSETS+=("${extra%%:*}") done } # The download URL of a release asset of $VERSION. release_asset_url() { echo "${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/$1" } # Downloads SHA256SUMS.txt of the release into $TEMP_DIR, once. fetch_release_sums() { local sums_file="$TEMP_DIR/SHA256SUMS.txt" if [[ -f "$sums_file" ]]; then return 0 fi echo "Downloading checksums: $(release_asset_url SHA256SUMS.txt)" if ! curl "${CURL_OPTS[@]}" -fsSL "$(release_asset_url SHA256SUMS.txt)" -o "$sums_file"; then rm -f "$sums_file" return 1 fi } # Prints the SHA-256 that SHA256SUMS.txt of the release lists for an asset, or nothing. listed_checksum() { local sum local entry while read -r sum entry; do if [[ "${entry#\*}" == "$1" ]]; then echo "$sum" return 0 fi done < "$TEMP_DIR/SHA256SUMS.txt" } # --dry-run: checks that the release carries every file this installation would download, and changes nothing. run_dry_run() { local binary_name local asset local status local failed=0 echo "Dry run: nothing is installed, stopped or changed." if [[ "$INSTALL_MODE" == "source" ]]; then echo "A source install clones $REPO_URL at $VERSION and builds it." exit 0 fi binary_name=$(detect_binary_name) || exit 1 select_release_assets "$binary_name" if ! fetch_release_sums; then echo "Error: could not download $(release_asset_url SHA256SUMS.txt)" >&2 exit 1 fi for asset in ${RELEASE_ASSETS[@]+"${RELEASE_ASSETS[@]}"}; do if [[ -z "$(listed_checksum "$asset")" ]]; then echo "Error: SHA256SUMS.txt of $VERSION has no entry for $asset" >&2 failed=1 fi done echo "Release assets:" for asset in ${RELEASE_ASSETS[@]+"${RELEASE_ASSETS[@]}"}; do status=$(curl "${CURL_OPTS[@]}" -sSL -r 0-0 -o /dev/null -w '%{http_code}' "$(release_asset_url "$asset")" 2>/dev/null) || true status="${status:-000}" echo " $status $(release_asset_url "$asset")" case "$status" in 200|206) ;; *) failed=1 ;; esac done if [[ $failed -ne 0 ]]; then echo "Error: dry run failed: $VERSION lacks files this installation needs" >&2 exit 1 fi echo "Dry run passed: $VERSION carries every file this installation needs." exit 0 } echo "================================================" echo " MOXYTOOL Installation Script" echo "================================================" echo "" require_command curl require_command sed if [[ -n "$SPECIFIED_VERSION" ]]; then VERSION="$SPECIFIED_VERSION" echo "Installing specified version: $VERSION" else VERSION=$(get_latest_version) || exit 1 echo "Installing latest version: $VERSION" fi echo "Install mode: $INSTALL_MODE" echo "" TEMP_DIR=$(mktemp -d) SOURCE_DIR="$TEMP_DIR/source" BACKUP_DIR="" INSTALL_REPLACED=0 SERVICE_WAS_RUNNING=0 SERVICE_STOPPED=0 SYSTEMD_AVAILABLE=0 PRESERVE_PATH_EXISTS=0 cleanup_temp() { rm -rf "$TEMP_DIR" } trap cleanup_temp EXIT if [[ $DRY_RUN -eq 1 ]]; then run_dry_run fi if command -v systemctl >/dev/null 2>&1; then SYSTEMD_AVAILABLE=1 for service_name in ${SERVICE_NAMES[@]+"${SERVICE_NAMES[@]}"}; do if systemctl is-active --quiet "$service_name" 2>/dev/null; then SERVICE_WAS_RUNNING=1 break fi done fi if [[ -n "$PRIMARY_PRESERVE_PATH" && -e "$PRIMARY_PRESERVE_PATH" ]]; then PRESERVE_PATH_EXISTS=1 fi # Puts the moved-aside installation back, or removes an incomplete first installation. restore_previous_installation() { if [[ $INSTALL_REPLACED -ne 1 ]]; then return 0 fi if [[ -z "$BACKUP_DIR" ]]; then echo "Removing the incomplete installation at $INSTALL_DIR" rm -rf "$INSTALL_DIR" || true if [[ "$(readlink "$BIN_DIR/moxytool" 2>/dev/null || true)" == "$INSTALL_DIR/"* ]]; then rm -f "$BIN_DIR/moxytool" || true fi return 0 fi if [[ -d "$BACKUP_DIR" ]]; then echo "Restoring previous installation from $BACKUP_DIR..." rm -rf "$INSTALL_DIR" || true if ! mv "$BACKUP_DIR" "$INSTALL_DIR"; then echo "Error: could not restore $BACKUP_DIR to $INSTALL_DIR; restore it by hand" >&2 return 1 fi if [[ -f "$INSTALL_DIR/moxytool" ]]; then mkdir -p "$BIN_DIR" || true ln -sf "$INSTALL_DIR/moxytool" "$BIN_DIR/moxytool" || true fi fi } restart_previous_service_on_error() { if [[ $SERVICE_STOPPED -eq 1 && $SYSTEMD_AVAILABLE -eq 1 && -n "$SERVICE_RESTART_NAME" ]]; then echo "Restarting the previous service: $SERVICE_RESTART_NAME" if ! systemctl restart "$SERVICE_RESTART_NAME"; then echo "Error: the previous service $SERVICE_RESTART_NAME did not restart; check: systemctl status $SERVICE_RESTART_NAME" >&2 return 1 fi fi } # Every failure after the current installation was touched ends here: put it back, restart it, exit 1. abort_update() { trap - ERR echo "Error: $1" >&2 local restored=0 restore_previous_installation || restored=1 restart_previous_service_on_error || restored=1 if [[ $restored -eq 0 ]]; then echo "Error: MOXYTOOL $VERSION was not installed; the previous state is restored" >&2 else echo "Error: MOXYTOOL $VERSION was not installed, and the previous installation could not be fully restored" >&2 fi exit 1 } handle_install_error() { abort_update "the MOXYTOOL installation failed" } trap handle_install_error ERR stop_service_if_running() { if [[ $SYSTEMD_AVAILABLE -ne 1 ]]; then return fi for service_name in ${SERVICE_NAMES[@]+"${SERVICE_NAMES[@]}"}; do if systemctl is-active --quiet "$service_name" 2>/dev/null; then echo "Stopping service: $service_name" SERVICE_STOPPED=1 systemctl stop "$service_name" || abort_update "could not stop the service $service_name" fi done } remove_legacy_services() { if [[ $SYSTEMD_AVAILABLE -ne 1 ]]; then return fi local needs_reload=0 for service_name in ${LEGACY_SERVICE_NAMES[@]+"${LEGACY_SERVICE_NAMES[@]}"}; do if systemctl is-enabled --quiet "$service_name" 2>/dev/null \ || systemctl is-active --quiet "$service_name" 2>/dev/null \ || systemctl is-failed --quiet "$service_name" 2>/dev/null \ || [[ -f "/etc/systemd/system/${service_name}.service" ]] \ || [[ -f "/usr/lib/systemd/system/${service_name}.service" ]] \ || [[ -f "/lib/systemd/system/${service_name}.service" ]]; then echo "Removing legacy service: $service_name" if systemctl is-active --quiet "$service_name" 2>/dev/null; then systemctl stop "$service_name" fi systemctl disable "$service_name" 2>/dev/null || true systemctl reset-failed "$service_name" 2>/dev/null || true rm -f \ "/etc/systemd/system/${service_name}.service" \ "/usr/lib/systemd/system/${service_name}.service" \ "/lib/systemd/system/${service_name}.service" needs_reload=1 fi done if [[ $needs_reload -eq 1 ]]; then systemctl daemon-reload fi } move_previous_installation() { mkdir -p "$(dirname "$INSTALL_DIR")" || abort_update "could not create the parent directory of $INSTALL_DIR" if [[ -d "$INSTALL_DIR" ]]; then BACKUP_DIR="${INSTALL_DIR}.previous.$$" echo "Moving previous installation to $BACKUP_DIR" mv "$INSTALL_DIR" "$BACKUP_DIR" || abort_update "could not move $INSTALL_DIR aside" fi INSTALL_REPLACED=1 } sha256_of() { local digest if command -v sha256sum >/dev/null 2>&1; then digest=$(sha256sum "$1") elif command -v shasum >/dev/null 2>&1; then digest=$(shasum -a 256 "$1") else echo "Error: neither sha256sum nor shasum is available to verify the download" >&2 exit 1 fi echo "${digest%% *}" } # Refuses the download unless SHA256SUMS.txt of the release lists it with the same digest. verify_release_checksum() { local file="$1" local name="$2" local expected local actual if ! fetch_release_sums; then echo "Error: could not download $(release_asset_url SHA256SUMS.txt); refusing to install an unverified binary" >&2 exit 1 fi expected=$(listed_checksum "$name") if [[ -z "$expected" ]]; then echo "Error: SHA256SUMS.txt of $VERSION has no entry for $name; refusing to install it" >&2 exit 1 fi actual=$(sha256_of "$file") if [[ "$actual" != "$expected" ]]; then echo "Error: checksum mismatch for $name of $VERSION; refusing to install it" >&2 echo " expected: $expected" >&2 echo " actual: $actual" >&2 exit 1 fi echo "Checksum verified: $name" } # Copies the verified extra release assets into the installation directory, with their modes. install_extra_files() { local extra local name local rest for extra in ${EXTRA_FILES[@]+"${EXTRA_FILES[@]}"}; do name="${extra%%:*}" rest="${extra#*:}" install -m "${rest%%:*}" "$1/$name" "$INSTALL_DIR/$name" || return 1 done } # Copies the verified notice files into the installation directory. install_notice_files() { local notice_file for notice_file in ${NOTICE_FILES[@]+"${NOTICE_FILES[@]}"}; do install -m 0644 "$1/$notice_file" "$INSTALL_DIR/$notice_file" || return 1 done } install_release_binary() { local binary_name local download_url local temp_file binary_name=$(detect_binary_name) download_url=$(release_asset_url "$binary_name") temp_file="$TEMP_DIR/$binary_name" echo "Downloading MOXYTOOL binary: $download_url" if ! curl "${CURL_OPTS[@]}" -fSL "$download_url" -o "$temp_file"; then echo "Error: could not download $download_url; the current installation is untouched" >&2 exit 1 fi verify_release_checksum "$temp_file" "$binary_name" chmod 0755 "$temp_file" # The third-party notices are installed with the executable, under their release asset names. local notice_file local notice_dir="$TEMP_DIR/notices" select_notice_files "$binary_name" mkdir -p "$notice_dir" for notice_file in ${NOTICE_FILES[@]+"${NOTICE_FILES[@]}"}; do echo "Downloading notices: $(release_asset_url "$notice_file")" if ! curl "${CURL_OPTS[@]}" -fSL "$(release_asset_url "$notice_file")" -o "$notice_dir/$notice_file"; then echo "Error: could not download the notice file $notice_file of $VERSION; the current installation is untouched" >&2 exit 1 fi verify_release_checksum "$notice_dir/$notice_file" "$notice_file" done # The extra release assets installed beside the executable. local extra local extra_name local extra_dir="$TEMP_DIR/extras" mkdir -p "$extra_dir" for extra in ${EXTRA_FILES[@]+"${EXTRA_FILES[@]}"}; do extra_name="${extra%%:*}" echo "Downloading: $(release_asset_url "$extra_name")" if ! curl "${CURL_OPTS[@]}" -fSL "$(release_asset_url "$extra_name")" -o "$extra_dir/$extra_name"; then echo "Error: could not download $extra_name of $VERSION; the current installation is untouched" >&2 exit 1 fi verify_release_checksum "$extra_dir/$extra_name" "$extra_name" done # The new binary must run before the current installation is touched. echo "Validating downloaded binary..." if ! "$temp_file" --version >/dev/null; then echo "Error: the downloaded MOXYTOOL $VERSION binary failed '--version'; the current installation is untouched" >&2 exit 1 fi stop_service_if_running move_previous_installation echo "Installing binary to $INSTALL_DIR" mkdir -p "$INSTALL_DIR" \ && install -m 0755 "$temp_file" "$INSTALL_DIR/moxytool" \ && install_notice_files "$notice_dir" \ && install_extra_files "$extra_dir" \ && mkdir -p "$BIN_DIR" \ && ln -sf "$INSTALL_DIR/moxytool" "$BIN_DIR/moxytool" \ || abort_update "could not install the binary to $INSTALL_DIR" } remove_legacy_services if [[ "$INSTALL_MODE" == "source" ]]; then echo "Error: source install mode is not enabled for MOXYTOOL" exit 1 else install_release_binary fi echo "Symlink created: $BIN_DIR/moxytool" if ! "$BIN_DIR/moxytool" --version >/dev/null; then abort_update "the installed MOXYTOOL CLI failed '--version'" fi if [[ $SERVICE_WAS_RUNNING -eq 1 && $SYSTEMD_AVAILABLE -eq 1 ]]; then if [[ -n "$SERVICE_RESTART_NAME" ]]; then echo "Restarting service: $SERVICE_RESTART_NAME" systemctl restart "$SERVICE_RESTART_NAME" || abort_update "the service $SERVICE_RESTART_NAME did not restart with MOXYTOOL $VERSION" SERVICE_STOPPED=0 echo "Service restarted successfully." echo "" fi fi trap - ERR # The backup goes only once the new installation runs. if [[ -n "$BACKUP_DIR" && -d "$BACKUP_DIR" ]]; then rm -rf "$BACKUP_DIR" fi echo "================================================" echo " MOXYTOOL Installation Complete!" echo "================================================" echo "" echo "Installation details:" echo " Install directory: $INSTALL_DIR" echo " Symlink location: $BIN_DIR/moxytool" echo " Version: $VERSION" echo " Mode: $INSTALL_MODE" echo "" if [[ $PRESERVE_PATH_EXISTS -eq 1 ]]; then echo "Existing data/configuration preserved: $PRIMARY_PRESERVE_PATH" echo "" if [[ $SERVICE_WAS_RUNNING -eq 1 ]]; then echo "The service has been restarted with your current settings." else echo "Start the service with: " fi else echo "Get started:" echo " moxytool" echo " sudo moxytool vgpu-setup" fi echo ""