#!/bin/sh # g-term 一键安装脚本(macOS / Linux) # # 用法: # curl -fsSL https://download.gorun.fun/g-term/install.sh | sh # curl -fsSL https://download.gorun.fun/g-term/install.sh | sh -s -- 1.2.3 # 安装指定版本 # # 为什么这样分发:浏览器下载的 app 会被 macOS 打上隔离标记(quarantine), # 未经 Apple 公证的应用双击即被 Gatekeeper 拦截(“Apple 无法验证…”); # curl 下载的文件不带隔离标记,经本脚本解压安装的 app 可直接启动。 # # 发布目录约定与应用内自更新(internal/updater)完全一致: # {base}/{平台目录}/{版本}/g-term-{平台后缀}.zip # 未指定版本时取目录列表里 semver 最新、且含产物文件的版本。 # # 环境变量: # GTERM_DOWNLOAD_BASE 下载基址(默认 https://download.gorun.fun/g-term) # GTERM_VERSION 指定版本,同位置参数 # GTERM_INSTALL_DIR macOS 的 .app 安装目录(默认 /Applications,不可写则 # 回落 ~/Applications) # GTERM_NO_LAUNCH=1 安装完成后不自动启动 # # 发布:CI(.gitlab-ci.yml 的 publish 阶段)随 zip 一起 scp 到下载服务器根目录。 set -u BASE="${GTERM_DOWNLOAD_BASE:-https://download.gorun.fun/g-term}" BASE="${BASE%/}" WANT="${1:-${GTERM_VERSION:-}}" say() { printf '==> %s\n' "$*"; } die() { printf 'install.sh: 错误: %s\n' "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } have curl || have wget || die "需要 curl 或 wget" # fetch_page URL —— nginx autoindex 目录页内容;404/失败输出空 fetch_page() { if have curl; then curl -fsSL --retry 2 "$1" 2>/dev/null || true else wget -q -T 15 -O - "$1" 2>/dev/null || true fi } # fetch URL DEST —— 下载文件(终端里显示进度) fetch() { if have curl; then curl -fSL --retry 3 -o "$2" "$1" else wget --tries=3 -O "$2" "$1" fi } # hrefs —— stdin 目录页里的条目名(去引号/尾斜杠,过滤 ../ 与 ?排序链接) hrefs() { grep -o 'href="[^"]*"' | sed -e 's/^href="//' -e 's/"$//' -e 's:/$::' } # ver_key V —— semver 风格排序键。数字段补零到 6 位后拼接(等长字符串比较 # 即数值比较);预发布加 "1" 前缀、正式版加 "2" 前缀(正式版 > 预发布); # 预发布标识符同样补零,数字标识符按数值比(rc.10 > rc.2)。 ver_key() { v="$1" pre="" case "$v" in *-*) pre="${v#*-}"; v="${v%%-*}" ;; esac key="" rest="$v" while :; do f="${rest%%.*}" n=$(printf '%s' "$f" | tr -cd '0-9'); [ -n "$n" ] || n=0 key="$key.$(printf '%06d' "$n")" case "$rest" in *.*) rest="${rest#*.}" ;; *) break ;; esac done if [ -z "$pre" ]; then printf '2%s' "$key" return fi rest="$pre" while :; do f="${rest%%.*}" case "$f" in ''|*[!0-9]*) p="$f" ;; *) p=$(printf '%06d' "$f") ;; esac key="$key.$p" case "$rest" in *.*) rest="${rest#*.}" ;; *) break ;; esac done printf '1%s' "$key" } # list_versions PDIR —— 平台目录下的版本目录,按 semver 降序输出 list_versions() { fetch_page "$BASE/$1/" | hrefs \ | grep -E '^[0-9]+([.-][0-9A-Za-z]+)*$' \ | while read -r v; do echo "$(ver_key "$v") $v"; done \ | sort -r | while read -r _k v; do echo "$v"; done } # pick_artifact PDIR VER ZIP —— 版本目录里本平台的产物文件名;精确名优先, # 其次任意 g-term-*.zip(同 updater.matchArchive 的兜底)。无产物输出空。 pick_artifact() { page=$(fetch_page "$BASE/$1/$2/") [ -n "$page" ] || return 1 files=$(printf '%s' "$page" | hrefs) if printf '%s\n' "$files" | grep -qxF "$3"; then echo "$3" return 0 fi printf '%s\n' "$files" | grep -E '^g-term-.*\.zip$' | head -n 1 } # --- 平台识别(目录名/zip 名与 updater.PlatformDir 一致)--- OS=$(uname -s) ARCH=$(uname -m) case "$OS" in Darwin) case "$ARCH" in arm64) PDIR=macos-arm64; ZIP=g-term-macos-arm64.zip ;; *) # Rosetta 终端里 uname -m 报 i386/x86_64;Apple Silicon 以 sysctl 为准 if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = "1" ]; then PDIR=macos-arm64; ZIP=g-term-macos-arm64.zip else PDIR=macos; ZIP=g-term-macos-amd64.zip fi ;; esac ;; Linux) case "$ARCH" in x86_64) PDIR=linux; ZIP=g-term-linux-amd64.zip ;; aarch64|arm64) PDIR=linux-arm64; ZIP=g-term-linux-arm64.zip ;; *) die "不支持的 Linux 架构: $ARCH" ;; esac ;; *) die "不支持的系统: $OS。请到 $BASE 手动下载对应安装包。" ;; esac # --- 版本解析 --- if [ -n "$WANT" ]; then VER="$WANT" else say "探测最新版本($BASE/$PDIR/)" VER="" for v in $(list_versions "$PDIR"); do if [ -n "$(pick_artifact "$PDIR" "$v" "$ZIP")" ]; then VER="$v"; break; fi done [ -n "$VER" ] || die "服务器上没有 $PDIR 平台的发布产物" fi FILE=$(pick_artifact "$PDIR" "$VER" "$ZIP") [ -n "$FILE" ] || die "版本 $VER 没有 $PDIR 平台的产物" URL="$BASE/$PDIR/$VER/$FILE" # --- 下载 + 安装 --- TMP=$(mktemp -d "${TMPDIR:-/tmp}/gterm-install.XXXXXX") trap 'rm -rf "$TMP"' EXIT say "下载 $URL" fetch "$URL" "$TMP/g-term.zip" || die "下载失败: $URL" if [ "$OS" = Darwin ]; then # ditto 对 mac zip 的权限保留最稳,缺失时兜底 unzip (cd "$TMP" && { have ditto && ditto -x -k g-term.zip . || unzip -q g-term.zip; }) [ -d "$TMP/g-term.app" ] || die "安装包里没有 g-term.app" DEST="${GTERM_INSTALL_DIR:-/Applications}" if [ ! -d "$DEST" ] || [ ! -w "$DEST" ]; then DEST="$HOME/Applications" mkdir -p "$DEST" || die "无法创建安装目录 $DEST" fi say "安装 v$VER 到 $DEST/g-term.app" rm -rf "$DEST/g-term.app" mv "$TMP/g-term.app" "$DEST/g-term.app" # 双保险:清掉可能携带的隔离属性,确保双击/启动不被 Gatekeeper 拦截 xattr -dr com.apple.quarantine "$DEST/g-term.app" 2>/dev/null || true say "安装完成" if [ "${GTERM_NO_LAUNCH:-}" != "1" ]; then open "$DEST/g-term.app" 2>/dev/null || say "启动: open \"$DEST/g-term.app\"" else say "启动: open \"$DEST/g-term.app\"" fi else (cd "$TMP" && mkdir x \ && { have unzip && unzip -q g-term.zip -d x \ || python3 -c "import zipfile; zipfile.ZipFile('g-term.zip').extractall('x')"; }) BIN=$(find "$TMP/x" -maxdepth 1 -type f -name 'g-term-*' | head -n 1) [ -n "$BIN" ] || die "安装包里没有可执行文件" DEST_DIR="$HOME/.local/bin" mkdir -p "$DEST_DIR" || die "无法创建 $DEST_DIR" install -m 755 "$BIN" "$DEST_DIR/g-term" say "已安装 v$VER 到 $DEST_DIR/g-term" case ":$PATH:" in *":$DEST_DIR:"*) ;; *) say "提示: 将 $DEST_DIR 加入 PATH 后即可运行 g-term" ;; esac fi