Files
gplan-2/tools/ci/mac_setup.sh
T

176 lines
8.6 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# mac_setup.sh — CI runner 自举件(Mac 本机 host 模式;2026-11-03 用户裁,取代 Docker 镜像路线)。
# 一条命令把一台 macOS 变成 gplan 的远端 CI runner:专用低权限账户+Godot+gdstyle+Pillow+act_runner 常驻。
# 用法(sudo 必须——建账户与写 /Library/LaunchDaemons 都要 root):
# sudo bash tools/ci/mac_setup.sh [--godot-zip <本地已下载的 macOS 版 zip 路径>]
# 环境前提:
# ① macOS(Intel/Apple Silicon 皆可,Godot 走 universal 包)② Xcode Command Line Tools 已装(python3/git 在位:
# 缺则先 xcode-select --install)③ 调用账户的 ~/gitea-runner/act_runner 已就位(gitea.com gitea/runner
# releases 的 darwin 包,版本与注册时一致)且该目录含已注册的 .runner(注册动作一次性,见 Gitea 文档)。
# 安全设计(用户裁「收紧 runner 权限」):
# - 所有作业以专用账户 ci-runner 运行:无 admin、无 sudo、可写面仅其家目录与仓库工作区;
# 与日常账户隔离——测试代码摸不到日常环境(威胁模型=测试写坏环境,非恶意第三方件)。
# - runner 以 LaunchDaemon(系统级)常驻:开机自启、不依赖任何人登录、崩溃自动拉起。
# 幂等:重复执行只补缺件,不重复建账户/不重复下载(本地已有同版本产物即跳过)。
set -euo pipefail
GODOT_ZIP=""
while [ $# -gt 0 ]; do
case "$1" in
--godot-zip) GODOT_ZIP="${2:-}"; shift 2 ;;
*) die "未知参数: $1(用法见头注)" ;;
esac
done
GODOT_VER="4.7.2-stable"
GODOT_URL_BASE="https://github.com/godotengine/godot/releases/download/${GODOT_VER}-stable/Godot_${GODOT_VER}-stable_macos.universal.zip"
GDSTYLE_VER="0.4.0"
GDSTYLE_URL="https://github.com/atelico/gdstyle/releases/download/v${GDSTYLE_VER}/gdstyle-x86_64-apple-darwin.tar.gz"
MIRRORS=("" "https://gh-proxy.net/" "https://gh-proxy.com/") # 直连优先,镜像回落
CI_USER="ci-runner"
CI_HOME="/Users/${CI_USER}"
log() { echo "[mac_setup] $*"; }
die() { echo "[mac_setup] ✗ $*" >&2; exit 1; }
[ "$(uname -s)" = "Darwin" ] || die "仅支持 macOS"
[ "$(id -u)" -eq 0 ] || die "请用 sudo 运行(建账户/写 LaunchDaemon 需要 root)"
for t in git curl python3 unzip dscl; do command -v "$t" >/dev/null 2>&1 || die "缺 $t(先 xcode-select --install)"; done
INVOKER="${SUDO_USER:-$(whoami)}"
RUNNER_SRC="/Users/${INVOKER}/gitea-runner"
[ -x "${RUNNER_SRC}/act_runner" ] || die "缺 ${RUNNER_SRC}/act_runner——先从 gitea.com/gitea/runner releases 下载 darwin 包(版本须与 .runner 注册时一致)"
[ -f "${RUNNER_SRC}/.runner" ] || die "缺 ${RUNNER_SRC}/.runner——act_runner 尚未注册(register 子命令,见 Gitea 文档)"
# ── 1. 专用账户(sysadminctl 一条建全:随机未告知密码=无法交互登录,纯服务账户)──
if dscl . -read "${CI_HOME}" NFSHomeDirectory >/dev/null 2>&1; then
log "账户 ${CI_USER} 已存在,跳过创建"
else
_randpw="$(uuidgen | tr -d '-')"
/usr/sbin/sysadminctl -addUser "$CI_USER" -password "$_randpw" -home "$CI_HOME" \
|| die "账户创建失败(sysadminctl)——检查磁盘/SecureToken 设置"
log "账户 ${CI_USER} 已创建(随机密码未落盘=无法交互登录,纯服务账户)"
fi
# ── 2. Godot macOS 版 → /usr/local/bin/godot ──
if /usr/local/bin/godot --version >/dev/null 2>&1 && /usr/local/bin/godot --version 2>/dev/null | grep -q "${GODOT_VER}"; then
log "Godot ${GODOT_VER} 已在位,跳过"
else
ZIP="$GODOT_ZIP"
SRC_ZIP="/Users/${INVOKER}/godot-linux/Godot_${GODOT_VER}-stable_macos.universal.zip"
if [ -n "$ZIP" ] && [ -s "$ZIP" ]; then
log "使用本地 zip: $ZIP"
elif [ -z "$ZIP" ] && [ -s "$SRC_ZIP" ]; then
log "复用已下载 zip: $SRC_ZIP"
ZIP="$SRC_ZIP"
else
ZIP="$(mktemp "${TMPDIR:-/tmp}/godot_mac_XXXXXX.zip")"
_ok=0
for m in "${MIRRORS[@]}"; do
url="${m}${GODOT_URL_BASE}"
log "下载 Godot(${m:+经 $m}):${GODOT_URL_BASE}"
if curl -fL --retry 2 -m 1800 -o "$ZIP" "$url"; then _ok=1; break; fi
log "该源失败,换下一源"
done
[ "$_ok" = 1 ] || die "全部下载源失败——请手动下载 ${GODOT_URL_BASE} 后重跑本脚本加 --godot-zip <zip路径>"
fi
TMPD="$(mktemp -d)"
unzip -q -o "$ZIP" -d "$TMPD"
install -m 0755 "${TMPD}/Godot.app/Contents/MacOS/Godot" /usr/local/bin/godot
xattr -dr com.apple.quarantine /usr/local/bin/godot 2>/dev/null || true
rm -rf "$TMPD"
log "Godot 已装:$(/usr/local/bin/godot --version 2>/dev/null | head -1)"
fi
# ── 3. gdstyle v0.4.0 darwin → /usr/local/bin/gdstyle(S13 静态扫描闸件,版本钉死)──
if /usr/local/bin/gdstyle --version >/dev/null 2>&1; then
log "gdstyle 已在位,跳过"
else
TGZ="$(mktemp "${TMPDIR:-/tmp}/gdstyle_XXXXXX.tar.gz")"
_ok=0
for m in "${MIRRORS[@]}"; do
log "下载 gdstyle v${GDSTYLE_VER} darwin(${m:+经 $m})"
if curl -fL --retry 2 -m 600 -o "$TGZ" "${m}${GDSTYLE_URL}"; then _ok=1; break; fi
done
[ "$_ok" = 1 ] || die "gdstyle 下载失败——手动下载 ${GDSTYLE_URL} 解压后放 /usr/local/bin/gdstyle 再重跑"
TMPD="$(mktemp -d)"
tar -xzf "$TGZ" -C "$TMPD"
BIN="$(find "$TMPD" -type f -name 'gdstyle*' | head -1)"
[ -n "$BIN" ] || die "gdstyle 压缩包内未找到二进制"
install -m 0755 "$BIN" /usr/local/bin/gdstyle
xattr -dr com.apple.quarantine /usr/local/bin/gdstyle 2>/dev/null || true
rm -rf "$TMPD"
log "gdstyle 已装:/usr/local/bin/gdstyle"
fi
# ── 4. Pillow(渲染闸 shot_checker/golden_compare 依赖,装给 ci-runner 用户)──
if sudo -u "$CI_USER" python3 -c "import PIL" >/dev/null 2>&1; then
log "Pillow 已在位(ci-runner 用户),跳过"
else
log "为 ci-runner 安装 Pillow(阿里源)"
sudo -u "$CI_USER" python3 -m pip install --user pillow \
-i https://mirrors.aliyun.com/pypi/simple/ --quiet \
|| die "Pillow 安装失败"
fi
# ── 5. runner 程序与注册文件就位(复制既有注册身份,免重新注册)──
mkdir -p "${CI_HOME}/gitea-runner"
cp -f "${RUNNER_SRC}/act_runner" "${CI_HOME}/gitea-runner/act_runner"
cp -f "${RUNNER_SRC}/.runner" "${CI_HOME}/gitea-runner/.runner"
cp -f "${RUNNER_SRC}/config.yaml" "${CI_HOME}/gitea-runner/config.yaml" 2>/dev/null || true
# 标签切 host 模式:作业直接跑在本机(.runner 的 labels 是声明面,daemon 启动时重新 declare)
python3 - "${CI_HOME}/gitea-runner/.runner" <<'PYEOF'
import json, sys
p = sys.argv[1]
d = json.load(open(p))
d["labels"] = ["ci-mac:host"]
json.dump(d, open(p, "w"))
print("labels ->", d["labels"])
PYEOF
sed -i '' 's/^ level: debug/ level: info/' "${CI_HOME}/gitea-runner/config.yaml" 2>/dev/null || true
chown -R "${CI_USER}:staff" "${CI_HOME}/gitea-runner"
# ── 6. LaunchDaemon 常驻(系统级:开机自启、不依赖登录、崩溃自动拉起)──
PLIST="/Library/LaunchDaemons/com.gitea.act-runner.plist"
cat > "$PLIST" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.gitea.act-runner</string>
<key>UserName</key><string>${CI_USER}</string>
<key>GroupName</key><string>staff</string>
<key>ProgramArguments</key>
<array>
<string>${CI_HOME}/gitea-runner/act_runner</string>
<string>daemon</string>
</array>
<key>WorkingDirectory</key><string>${CI_HOME}/gitea-runner</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>${CI_HOME}/gitea-runner/daemon.log</string>
<key>StandardErrorPath</key><string>${CI_HOME}/gitea-runner/daemon.log</string>
</dict>
</plist>
EOF
launchctl bootout system/com.gitea.act-runner 2>/dev/null || true
launchctl bootstrap system "$PLIST" || launchctl load -w "$PLIST"
log "LaunchDaemon 已加载(com.gitea.act-runner,用户 ${CI_USER})"
# ── 7. 退役 Docker 版 runner(如存在)──
if command -v docker >/dev/null 2>&1; then
docker rm -f gitea-runner >/dev/null 2>&1 && log "Docker 版 runner 容器已退役" || log "无 Docker 版 runner 容器(或 docker 未运行)"
fi
# ── 8. 验证 ──
sleep 5
if launchctl print "system/com.gitea.act-runner" 2>/dev/null | grep -q "state"; then
log "runner 服务在跑"
else
die "runner 服务未起来——查 ${CI_HOME}/gitea-runner/daemon.log"
fi
log "全部完成。验证清单:"
log " ① Gitea 网页 → 仓库设置 → Actions → Runners 应显示 mac-ci 在线(Idle)"
log " ② sudo -u ${CI_USER} /usr/local/bin/godot --headless --version"
log " ③ sudo -u ${CI_USER} python3 -c 'import PIL'"
log " ④ 任意 push 触发一次跑批端到端验证"