From 3d4173774f52c133f56ea98c8517397ceb389283 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 3 Nov 2026 04:05:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(ci=E8=BF=9C=E7=AB=AF?= =?UTF-8?q?=E5=8C=96=C2=B7=E9=98=B6=E6=AE=B5=E5=9B=9B=E6=94=B9=E7=89=88):?= =?UTF-8?q?=20mac=5Fsetup=E6=8C=89=E4=B8=93=E7=94=A8=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E4=BC=9A=E8=AF=9D=E5=BD=A2=E6=80=81=E9=87=8D?= =?UTF-8?q?=E5=86=99=E2=80=94=E2=80=94LaunchDaemon=E6=94=B9LaunchAgent(?= =?UTF-8?q?=E6=8C=82ci-runner=E4=BC=9A=E8=AF=9D)+=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=99=BB=E5=BD=95(/etc/kcpassword=E7=BC=96=E7=A0=81+autoLoginU?= =?UTF-8?q?ser,=E5=AF=86=E7=A0=81root600)+=E4=BF=9D=E7=95=99=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E9=9A=94=E7=A6=BB=E8=AE=BE=E8=AE=A1(=E9=9A=8F?= =?UTF-8?q?=E6=9C=BA=E5=AF=86=E7=A0=81=E4=B8=8D=E5=8F=AF=E7=99=BB=E5=BD=95?= =?UTF-8?q?/=E6=97=A0admin/=E5=8F=AF=E5=86=99=E9=9D=A2=E9=99=90=E5=AE=9A);?= =?UTF-8?q?=20=E6=B8=B2=E6=9F=93=E9=97=B8=E7=AA=97=E5=8F=A3=E6=9D=A5?= =?UTF-8?q?=E6=BA=90=3Dci-runner=E5=9B=BE=E5=BD=A2=E4=BC=9A=E8=AF=9D(?= =?UTF-8?q?=E5=BF=AB=E9=80=9F=E7=94=A8=E6=88=B7=E5=88=87=E6=8D=A2=E4=B8=8E?= =?UTF-8?q?=E6=97=A5=E5=B8=B8=E4=BD=BF=E7=94=A8=E5=B9=B6=E5=AD=98);=20?= =?UTF-8?q?=E4=BE=9D=E6=8D=AE=3D=E7=94=A8=E6=88=B7=E8=A3=81=E4=B8=93?= =?UTF-8?q?=E7=94=A8=E8=B4=A6=E6=88=B7=E6=96=B9=E6=A1=88+=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E7=A1=AC=E9=9C=80=E6=B1=82,WindowServer=E5=8F=AA?= =?UTF-8?q?=E5=9C=A8=E5=B7=B2=E7=99=BB=E5=BD=95=E4=BC=9A=E8=AF=9D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=9A=84=E6=9C=BA=E5=88=B6=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/ci/mac_setup.sh | 159 +++++++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 64 deletions(-) diff --git a/tools/ci/mac_setup.sh b/tools/ci/mac_setup.sh index dc42fece..5805473e 100644 --- a/tools/ci/mac_setup.sh +++ b/tools/ci/mac_setup.sh @@ -1,77 +1,103 @@ #!/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): +# mac_setup.sh — CI runner 自举件(Mac 本机 host 模式+专用账户图形会话;2026-11-03 用户裁)。 +# 一条命令把一台 macOS 变成 gplan 的远端 CI runner:专用账户+自动登录图形会话+Godot+gdstyle+ +# Pillow+act_runner 用户级常驻(LaunchAgent)。 +# 形态要点(渲染闸的硬需求):渲染闸要真窗口 = 必须有图形会话(WindowServer 只存在于已登录会话)。 +# 方案 = ci-runner 账户开机自动登录(owns 控制台图形会话)→ 该会话内 LaunchAgent 拉起 runner → +# 测试作业全部以 ci-runner 身份运行(无 admin、可写面限定);日常使用经「快速用户切换」到本人账户, +# 两会话并存互不干扰(后台会话保留约 1-2GB 内存;后台会话渲染行为在端到端验收第一轮实证)。 +# 用法(sudo 必须——建账户/写 /etc/kcpassword/装 /usr/local/bin 都要 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(系统级)常驻:开机自启、不依赖任何人登录、崩溃自动拉起。 -# 幂等:重复执行只补缺件,不重复建账户/不重复下载(本地已有同版本产物即跳过)。 +# ① macOS(Intel/Apple Silicon 皆可,Godot 走 universal 包)② Xcode Command Line Tools 已装 +# (python3/git 在位;缺则先 xcode-select --install)③ 调用账户的 ~/gitea-runner/ 下已有 +# act_runner(darwin 包,gitea.com gitea/runner releases)与已注册的 .runner(一次性注册动作)。 +# 幂等:重复执行只补缺件;账户密码首次生成后存 /var/db/gplan-ci-runner.pw(root 600),重跑复用。 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" +GODOT_URL="https://github.com/godotengine/godot/releases/download/${GODOT_VER}/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}" +PW_FILE="/var/db/gplan-ci-runner.pw" log() { echo "[mac_setup] $*"; } die() { echo "[mac_setup] ✗ $*" >&2; exit 1; } [ "$(uname -s)" = "Darwin" ] || die "仅支持 macOS" -[ "$(id -u)" -eq 0 ] || die "请用 sudo 运行(建账户/写 LaunchDaemon 需要 root)" +[ "$(id -u)" -eq 0 ] || die "请用 sudo 运行(建账户/写 /etc/kcpassword/装 /usr/local/bin 需要 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 注册时一致)" +[ -x "${RUNNER_SRC}/act_runner" ] || die "缺 ${RUNNER_SRC}/act_runner——先从 gitea.com/gitea/runner releases 下载 darwin 包" [ -f "${RUNNER_SRC}/.runner" ] || die "缺 ${RUNNER_SRC}/.runner——act_runner 尚未注册(register 子命令,见 Gitea 文档)" -# ── 1. 专用账户(sysadminctl 一条建全:随机未告知密码=无法交互登录,纯服务账户)── +GODOT_ZIP="" +while [ $# -gt 0 ]; do + case "$1" in + --godot-zip) GODOT_ZIP="${2:-}"; shift 2 ;; + *) die "未知参数: $1" ;; + esac +done + +# ── 1. 专用账户+密码落盘(自动登录需要密码编码进 /etc/kcpassword,root 600)── if dscl . -read "${CI_HOME}" NFSHomeDirectory >/dev/null 2>&1; then - log "账户 ${CI_USER} 已存在,跳过创建" + log "账户 ${CI_USER} 已存在" + [ -f "$PW_FILE" ] || die "账户已存在但 $PW_FILE 缺失(无法配置自动登录)——删除账户重跑,或手工补密码文件" + CI_PW="$(cat "$PW_FILE")" else - _randpw="$(uuidgen | tr -d '-')" - /usr/sbin/sysadminctl -addUser "$CI_USER" -password "$_randpw" -home "$CI_HOME" \ - || die "账户创建失败(sysadminctl)——检查磁盘/SecureToken 设置" - log "账户 ${CI_USER} 已创建(随机密码未落盘=无法交互登录,纯服务账户)" + CI_PW="$(uuidgen | tr -d '-')" + /usr/sbin/sysadminctl -addUser "$CI_USER" -password "$CI_PW" -home "$CI_HOME" \ + || die "账户创建失败(sysadminctl)" + printf '%s' "$CI_PW" > "$PW_FILE" + chmod 600 "$PW_FILE" + log "账户 ${CI_USER} 已创建(密码仅存 $PW_FILE,root 600,不对外告知)" 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 +# ── 2. 自动登录(ci-runner 拥有控制台图形会话——渲染闸的窗口来源)── +_KCP="/etc/kcpassword" +_need_kcp=1 +[ -f "$_KCP" ] && _need_kcp=0 +if defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null | grep -q "$CI_USER"; then + log "自动登录已指向 ${CI_USER},跳过" + _need_kcp=0 +fi +if [ "$_need_kcp" = 1 ]; then + python3 - "$CI_PW" <<'PYEOF' +import sys +pwd = sys.argv[1].encode() +key = bytes([0x7D, 0x89, 0x52, 0x23, 0xD2, 0xBC, 0xDD, 0xEA, 0xA3, 0xB9, 0x1F]) +enc = bytes(b ^ key[i % len(key)] for i, b in enumerate(pwd)) +enc += b"\x00" * ((12 - len(enc) % 12) % 12 + 1) +open("/etc/kcpassword", "wb").write(enc) +PYEOF + chmod 600 /etc/kcpassword + defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "$CI_USER" + log "自动登录已配置:开机直接进入 ${CI_USER} 图形会话(日常使用=快速用户切换到本人账户)" +fi + +# ── 3. 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" + 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" + if [ -n "$GODOT_ZIP" ] && [ -s "$GODOT_ZIP" ]; then + ZIP="$GODOT_ZIP"; log "使用本地 zip: $ZIP" + elif [ -s "$SRC_ZIP" ]; then + ZIP="$SRC_ZIP"; log "复用已下载 zip: $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 "下载 Godot(${m:+经 $m})" + if curl -fL --retry 2 -m 1800 -o "$ZIP" "${m}${GODOT_URL}"; then _ok=1; break; fi log "该源失败,换下一源" done - [ "$_ok" = 1 ] || die "全部下载源失败——请手动下载 ${GODOT_URL_BASE} 后重跑本脚本加 --godot-zip " + [ "$_ok" = 1 ] || die "全部下载源失败——手动下载 ${GODOT_URL} 后重跑并加 --godot-zip <路径>" fi TMPD="$(mktemp -d)" unzip -q -o "$ZIP" -d "$TMPD" @@ -81,7 +107,7 @@ else log "Godot 已装:$(/usr/local/bin/godot --version 2>/dev/null | head -1)" fi -# ── 3. gdstyle v0.4.0 darwin → /usr/local/bin/gdstyle(S13 静态扫描闸件,版本钉死)── +# ── 4. gdstyle v0.4.0 darwin → /usr/local/bin/gdstyle(S13 静态扫描闸件,版本钉死)── if /usr/local/bin/gdstyle --version >/dev/null 2>&1; then log "gdstyle 已在位,跳过" else @@ -91,20 +117,20 @@ else 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 再重跑" + [ "$_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 压缩包内未找到二进制" + [ -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 用户)── +# ── 5. Pillow(渲染闸依赖,装给 ci-runner 用户)── if sudo -u "$CI_USER" python3 -c "import PIL" >/dev/null 2>&1; then - log "Pillow 已在位(ci-runner 用户),跳过" + log "Pillow 已在位(ci-runner),跳过" else log "为 ci-runner 安装 Pillow(阿里源)" sudo -u "$CI_USER" python3 -m pip install --user pillow \ @@ -112,12 +138,11 @@ else || die "Pillow 安装失败" fi -# ── 5. runner 程序与注册文件就位(复制既有注册身份,免重新注册)── -mkdir -p "${CI_HOME}/gitea-runner" +# ── 6. runner 程序与注册文件就位(复制既有注册身份,免重新注册)── +mkdir -p "${CI_HOME}/gitea-runner" "${CI_HOME}/Library/LaunchAgents" 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] @@ -127,18 +152,16 @@ 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" +chown -R "${CI_USER}:staff" "${CI_HOME}/gitea-runner" "${CI_HOME}/Library/LaunchAgents" -# ── 6. LaunchDaemon 常驻(系统级:开机自启、不依赖登录、崩溃自动拉起)── -PLIST="/Library/LaunchDaemons/com.gitea.act-runner.plist" +# ── 7. LaunchAgent(用户级常驻,挂在 ci-runner 图形会话内——渲染闸窗口的来源)── +PLIST="${CI_HOME}/Library/LaunchAgents/com.gitea.act-runner.plist" cat > "$PLIST" < Labelcom.gitea.act-runner - UserName${CI_USER} - GroupNamestaff ProgramArguments ${CI_HOME}/gitea-runner/act_runner @@ -152,24 +175,32 @@ cat > "$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})" +chown "${CI_USER}:staff" "$PLIST" +_uid="$(id -u "$CI_USER")" +if launchctl asuser "$_uid" launchctl bootstrap "gui/$_uid" "$PLIST" 2>/dev/null; then + log "LaunchAgent 已在 ${CI_USER} 会话内启动" + _started=1 +else + _started=0 + log "当前无 ${CI_USER} 图形会话——LaunchAgent 已就位,重启后随自动登录自动拉起" +fi -# ── 7. 退役 Docker 版 runner(如存在)── +# ── 8. 退役 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 服务在跑" +# ── 9. 验证与收尾提示 ── +if [ "$_started" = 1 ]; then + sleep 5 + grep -q "declare successfully" "${CI_HOME}/gitea-runner/daemon.log" 2>/dev/null \ + && log "runner 已向 Gitea 声明成功" || log "runner 已启动,声明确认见 ${CI_HOME}/gitea-runner/daemon.log" else - die "runner 服务未起来——查 ${CI_HOME}/gitea-runner/daemon.log" + log "⚠ 重启 Mac 后自动登录生效、runner 随会话拉起——重启后验证第①条" fi log "全部完成。验证清单:" -log " ① Gitea 网页 → 仓库设置 → Actions → Runners 应显示 mac-ci 在线(Idle)" +log " ① Gitea 网页 → 仓库设置 → Actions → Runners 应显示 mac-ci 在线(Idle,标签 ci-mac)" log " ② sudo -u ${CI_USER} /usr/local/bin/godot --headless --version" log " ③ sudo -u ${CI_USER} python3 -c 'import PIL'" -log " ④ 任意 push 触发一次跑批端到端验证" +log " ④ 日常使用:登录窗/快速用户切换到本人账户(ci-runner 会话在后台保持)" +log " ⑤ 端到端:下一次合回 master 的推送自动触发跑批"