Files
ESP32S3_vt/.github/workflows/build-client.yml
T
sloray f0538fa233
构建客户端 / 检查(类型 + 单元测试) (push) Successful in 1m9s
构建客户端 / macOS 通用(.app + .dmg) (push) Skipped
构建客户端 / Windows x64(NSIS 安装包) (push) Failing after 33s
构建客户端 / Linux x64(deb + AppImage) (push) Successful in 6m4s
ci: Windows job 加 USERPROFILE 重定向 —— pnpm 的操作锁按它算路径
run 9 的诊断2 已经把话说死了:

    LOCALAPPDATA = C:\Users\admin\runner\windows\cache\ci-localappdata   <- 重定向生效
    OK   ...\ci-localappdata\pnpm-store-operation-locks                  <- 目录能建
    Error: ERR_PNPM_STORE_DIR_OPEN_OPERATION_LOCK
      Failed to open the store operation lock at
      "C:\Users\admin\AppData\Local\pnpm-store-operation-locks\all-stores.lock"

也就是说 LOCALAPPDATA 指走了,pnpm 还是去开原来那个路径。本机复现出真正的规则:
只改 USERPROFILE(LOCALAPPDATA 不动)、store 换成一个全新的空目录时,
    <新 USERPROFILE>\AppData\Local\pnpm-store-operation-locks\all-stores.lock
    <新 USERPROFILE>\AppData\Local\pnpm-store-operation-locks\<hash>.lock
立刻出现 —— pnpm 是按 <USERPROFILE>\AppData\Local 算的,跟 LOCALAPPDATA 无关。

所以在那一步里再建一个 ci-profile,把 AppData\Local / AppData\Roaming 先建好,然后
把 USERPROFILE 写进 GITHUB_ENV。文件头第 5 条同步更新。
2026-10-15 23:06:35 +08:00

568 lines
31 KiB
YAML
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.
# 构建桌面客户端(desktop/,Tauri v2 + React)。
#
# 产物都挂在这次运行下面(Actions 页面直接下载),保留 14 天:
# Linux x64 deb + AppImage cam-desktop_<版本>_amd64.deb / .AppImage ← 默认每次编
# Windows x64 NSIS 安装包 cam-desktop_<版本>_x64-setup.exe ← 默认每次编
# macOS 通用 .app + .dmg arm64 与 x86_64 合成一个包 ← 还没有机器,手动勾选才编
#
# ── 触发 ─────────────────────────────────────────────────────────────────────
# * push 到 main/master —— 只认 desktop/、protocol/ 与本文件的改动,改固件不会
# 白占 runner
# * 打 v* tag、开 PR
# * 手动 Run workflow:逐个平台勾选,不进 paths 过滤("就想编一次"走它)
# 平台开关一律看勾选框(理由见下面第 4 条)。
#
# ── 跑在 Gitea 上要先对上的两件事 ────────────────────────────────────────────
# 1. runner 标签是注册 runner 时自己起的名字,猜不到,所以三个 job 都写成
# "仓库变量优先、常见标签兜底":
# runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
# 标签对不上时 job 是**排队**而不是报错 —— 排队会一直占着这次运行不结束,
# 所以上线新平台前先看一眼 runner 页面上那几行标签。
#
# 2026-10-15 实测在线的(括号里是它们自己声明的标签):
# * simba-runner-01 (ubuntu-latest / ubuntu-24.04 / ubuntu-22.04)
# * windows-runner-01 (windows-latest / windows-2025 / windows-2022)
# * macOS:还没有
# 所以 Windows 默认就跑;macOS 的 job 写成"手动勾选才跑"。
#
# 为什么不能拿 Linux runner 交叉编出来:
# * Windows:Rust 侧用 mingw 交叉编还能出个裸 exe,但 tauri 的 NSIS 打包器
# 本身只能在 Windows 上跑,出不了安装包;
# * macOS:要 Apple SDK 与 codesign/lipo,只能在 macOS 上编。
# Windows runner 上要预装好 MSVC Build Tools(link.exe)、WebView2 运行时与
# rustup(dtolnay/rust-toolchain 靠它装工具链);macOS 上要 Xcode Command Line
# Tools + rustup。Linux 的 job 通常跑在容器里,没装 rustup 也没关系 —— 见兜底那步。
# 2. 这个文件放在 .github/workflows/ 下就够:Gitea 的 [actions] WORKFLOW_DIRS
# 默认值就是 [".gitea/workflows", ".github/workflows"](源码里的默认值,且
# .yml 与 .yaml 都认),所以两个目录都是原生支持的,不用挪。
# 3. 两处"GitHub 能跑、Gitea 上不行"的地方,都是 2026-10-15 第一次真实运行的
# 日志里钉出来的:
# * 传产物用 upload-artifact@v3,**不能用 v4**。v4 里的 @actions/artifact 会
# 拒绝非 github.com 的服务器,日志原文:
# GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
# and download-artifact@v4+ are not currently supported on GHES
# Gitea 实现的是 v3 那套接口(ACTIONS_RUNTIME_URL),所以 v3 能传上去。
# * 一个缓存都没开。这台实例的缓存服务在 job 容器里连不上,日志原文:
# Failed to restore: getCacheEntry failed: connect ETIMEDOUT 172.18.0.2:37315
# setup-node 的 cache 每次要干等 4 分 39 秒才放弃(两个 job 各一次)。
# 等缓存能从容器访问到了再加回来,写法就是:
# setup-node 的 with 里加:cache: pnpm / cache-dependency-path: desktop/pnpm-lock.yaml
# 各 job 里加:
# - uses: actions/cache@v4
# with:
# path: |
# ~/.cargo/registry/index
# ~/.cargo/registry/cache
# ~/.cargo/git/db
# key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
# restore-keys: ${{ runner.os }}-cargo-registry-
# 4. 平台开关只用 github.event.inputs,**不用仓库变量**。2026-10-15 那次 Windows# runner 明明在线、job 也确实派给了它(它自己的日志里写着
# "Runner labels: windows-latest, windows-2025, windows-2022"),产物却一直是
# skipped —— 卡在 job 级 if 的 `vars.ENABLE_WINDOWS == 'true'` 上:Gitea 的
# runner 在 job 级 if 里拿不到(或拿不对)vars 上下文,而这个条件写反的代价是
# "静悄悄不编",最难发现。同一份文件里 `github.event.inputs.linux != 'false'`
# 这个形式在 runner 日志里能看到正常求值(evaluating expression '...' evaluated
# to 'true'),所以平台开关统一成这种:
# * Linux / Windows:`github.event.inputs.<平台> != 'false'` → 默认跑,
# 手动触发时取消勾选就不跑
# * macOS:`github.event.inputs.macos == 'true'` → 默认不跑,等有机器了改成
# 上一种形式即可
# runs-on 里的 vars(RUNNER_*)不受影响,标签不匹配时用那个覆盖。
# 5. Windows runner 的 job 跑在**低完整性**上(实测 Mandatory Label\Low Mandatory
# Level, S-1-16-4096),而 Windows 不允许低完整性往中完整性写:用户目录
# (~/.rustup、~/.cargo、%TEMP%、%LOCALAPPDATA%)统统 Access denied (os error 5)
# —— ACL 是干净的(SYSTEM / Administrators / 本人 全 F,没有任何 Deny),拦它的是
# 完整性级别,所以"放宽权限"一点用都没有。表现是一串看着毫不相干的错误:
# * Git Bash 的 mktemp 写 /tmp 被拒(它的 /tmp 跟着 TEMP 走;TEMP 不可写时退回
# <Git 安装目录>\tmp,那个目录普通权限进不去);
# * rustup: could not create temp file C:\Users\admin\.rustup\tmp\xxx_file
# 处置:Windows job 把能重定向的都指到 runner 自己的工作目录
# (C:\Users\admin\runner\windows\cache,job 内写探测实测可写)——
# RUSTUP_HOME / CARGO_HOME / npm_config_cache / npm_config_store_dir /
# RUNNER_TOOL_CACHE / LOCALAPPDATA / APPDATA,加上 TEMP/TMP/TMPDIR、
# pnpm/action-setup 的 dest,以及 **USERPROFILE**。
# 最后这个是被 pnpm 逼出来的:它的 store 操作锁不在 store-dir 下面,也不看
# LOCALAPPDATA,而是按 <USERPROFILE>\AppData\Local\pnpm-store-operation-locks
# 算 —— 只把 LOCALAPPDATA 指走它照样去开原来的路径(报错原文见 run 8 的日志),
# 把 USERPROFILE 指走它才跟着走(本机实测:探针 profile 下立刻出现
# all-stores.lock)。
# 想从根上解决:让 gitea-runner 以**中完整性**启动(在普通终端里跑
# run-runner.ps1,而不是从被沙箱化的低完整性 shell 里拉起来),那样这些重定向
# 就都可以撤掉。这台机器上 runner 由计划任务 GiteaRunner-Windows(admin /
# Limited)拉起,但进程链上的完整性已经落到了 Low。
#
# ── 三个平台都显式写 --target,这不是啰嗦 ────────────────────────────────────
# desktop/.cargo/config.toml 里钉着 `target = "aarch64-apple-darwin"`(本机开发用;
# 它存在的理由是 desktop/ 嵌在固件目录树里,不然会继承根目录那份
# xtensa-esp32s3-espidf 的交叉编译配置)。CI 上不覆盖它,Linux 与 Windows 的 runner
# 会去编 macOS 的目标,然后在依赖深处以 libc 找不到 MAP_FAILED 这类形式失败。
# 所以每个 job 都把真正的目标交两遍:CARGO_BUILD_TARGET 给裸 cargo(测试)用,
# --target 给 tauri build 用(打包路径也跟着它走)。
#
# ── 还有一条:仓库根目录的 rust-toolchain.toml 钉的是 `esp` ─────────────────
# 那是给 ESP32 固件的定制工具链,CI 上没有、也不打算装(espup 要下几百 MB)。
# rustup 是按**当前目录**往上找 rust-toolchain.toml 的:
# * desktop/** 有自己的 rust-toolchain.toml(stable),所以那里的 cargo 直接可用;
# * protocol/ 没有,会一路找到根目录那个 esp —— 所以那条测试写成 `cargo +stable`,
# 显式越过它。
# 任何在仓库根目录直接跑 cargo 的步骤都会去要 esp 工具链,别加。
#
# ── 没做的事 ────────────────────────────────────────────────────────────────
# 不签名、不公证:Windows 的 exe 与 macOS 的 app 在别人机器上会被 SmartScreen /
# Gatekeeper 拦一次(右键打开即可)。要做的话给 macOS 那几个 job 加
# APPLE_CERTIFICATE / APPLE_SIGNING_IDENTITY / APPLE_ID / APPLE_PASSWORD /
# APPLE_TEAM_ID(tauri 认这些环境变量,构建步骤不用改)。也没有自动发 Release:
# 要发的话在最后加一个下载全部 artifact 再创建 release 的 job。
name: 构建客户端
on:
push:
branches: [main, master]
tags: ["v*"]
# GitHub 语义下 tag 推送不受 paths 过滤影响;如果你们的 Gitea 版本把 tag 也
# 一起过滤掉了(打 tag 不触发),把这三行 paths 删掉即可。
paths:
- "desktop/**"
- "protocol/**"
- ".github/workflows/build-client.yml"
pull_request:
paths:
- "desktop/**"
- "protocol/**"
- ".github/workflows/build-client.yml"
workflow_dispatch:
inputs:
# Linux 与 Windows 的 runner 都在线,默认勾上;macOS 还没有机器,默认不勾
# (勾了会一直排队)。开关为什么不用仓库变量见文件头第 4 条。
linux:
description: "构建 Linux(deb + AppImage)"
type: boolean
default: true
windows:
description: "构建 Windows(NSIS 安装包)"
type: boolean
default: true
macos:
description: "构建 macOS(通用 dmg)——还没有 macOS runner,勾了会排队"
type: boolean
default: false
# 同一分支连推两次时把上一次没跑完的取消掉:自建 runner 数量有限,让过期的构建
# 占着机器没有意义。老版本 runner 忽略这个键,不影响构建。
concurrency:
group: build-client-${{ github.ref }}
cancel-in-progress: true
env:
# 与本机开发对齐:node 24 / pnpm 12(pnpm-lock.yaml 就是 pnpm 12.8.1 写的,
# lockfileVersion 9.0)。改这两个数之前先确认 lock 文件还认。
NODE_VERSION: "24"
PNPM_VERSION: "12"
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"
jobs:
# ───────────────────────────────────────────────────────────────────────────
# 守门:类型检查、前端构建、两个不依赖窗口的 crate 的单元测试。
# 下面每个构建 job 都 needs 它 —— 编到一半才发现 tsc 报错,是最浪费 runner 的
# 一种失败。src-tauri 的 30 个测试放在 Linux 那个 job 里跑(它本来就要装
# webkit 依赖,而且那几条录制测试要真的起 ffmpeg)。
# ───────────────────────────────────────────────────────────────────────────
checks:
name: 检查(类型 + 单元测试)
runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
# runner 镜像里多半没有 rustup,而 dtolnay/rust-toolchain 靠它装工具链。
# 这里只装一个最小的 rustup 本身,工具链交给下面那个 action —— 这样自建
# 镜像不用为了 CI 专门烤一个带 Rust 的镜像。
# 国内网络慢的话,在 runner 的环境变量里给 RUSTUP_DIST_SERVER 指一个镜像
# (如 https://mirrors.ustc.edu.cn/rust-static)会快很多。
- name: 确保有 rustup
run: |
if command -v rustup >/dev/null 2>&1; then
rustup --version
exit 0
fi
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
else
echo "装 rustup 需要 curl 或 wget,这个镜像里两个都没有" >&2
exit 1
fi
if [ -n "$GITHUB_PATH" ]; then
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
# pnpm 装在 setup-node 之前:这一步只靠 runner 镜像里自带的 node(实测
# /opt/hostedtoolcache 里有 24.x),而下面的 pnpm install 要用它。
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
- name: 类型检查(tsc --noEmit)
working-directory: desktop
run: pnpm run check
- name: 前端构建(vite)
working-directory: desktop
run: pnpm build
- name: 装 Rust(stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
# protocol 没有自己的 rust-toolchain.toml,会被根目录那个 `esp` 接住,
# 所以这里必须 +stable 显式指定。
- name: protocol 单元测试(15)
working-directory: protocol
run: cargo +stable test --locked --target x86_64-unknown-linux-gnu
# core 在 desktop/ 下面,desktop/rust-toolchain.toml 已经是 stable,不用 +stable。
- name: cam-core 单元测试(80)
working-directory: desktop/core
run: cargo test --locked --target x86_64-unknown-linux-gnu
# ───────────────────────────────────────────────────────────────────────────
# Linux x64:deb + AppImage
# ───────────────────────────────────────────────────────────────────────────
linux:
name: Linux x64(deb + AppImage)
needs: checks
if: ${{ github.event.inputs.linux != 'false' }}
runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
timeout-minutes: 120
env:
TARGET: x86_64-unknown-linux-gnu
CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- name: 确保有 rustup
run: |
if command -v rustup >/dev/null 2>&1; then
rustup --version
exit 0
fi
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
else
echo "装 rustup 需要 curl 或 wget,这个镜像里两个都没有" >&2
exit 1
fi
if [ -n "$GITHUB_PATH" ]; then
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
- name: 装系统依赖
run: |
# 容器里跑的 runner 通常是 root,没有 sudo;托管 runner 上有。
SUDO=""
[ "$(id -u)" -eq 0 ] || SUDO=sudo
$SUDO apt-get update
$SUDO apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libxdo-dev \
libssl-dev \
xdg-utils \
build-essential curl wget file \
ffmpeg
# AppImage 打包要运行 linuxdeploy(它本身是个 AppImage);24.04 起包名带 t64。
$SUDO apt-get install -y libfuse2 || $SUDO apt-get install -y libfuse2t64 || true
- name: 装 Rust(stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
# 上面 apt 装了 ffmpeg,所以 recording.rs 里那几条"真的起 ffmpeg 并读回帧数"
# 的测试不会走 has_ffmpeg() 的跳过分支,是真的在验。
# 如果这台 runner 上它们长期不稳定,删掉这一步即可 —— 它们不在守门 job 里,
# 不影响另外两个平台的产物。
- name: Tauri 后端单元测试(30)
working-directory: desktop/src-tauri
run: cargo test --locked --target ${{ env.TARGET }}
# beforeBuildCommand(pnpm build)由 tauri 自己跑,这里不用再 build 一次。
- name: 打包(tauri build)
working-directory: desktop
run: pnpm tauri build --target ${{ env.TARGET }} --bundles deb,appimage
- name: 上传产物
uses: actions/upload-artifact@v3
with:
name: cam-desktop-linux-x64
# 一个都没找到就报错:产物路径写错却"构建成功"是最难发现的一种失败。
if-no-files-found: error
retention-days: 14
path: |
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/deb/*.deb
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/appimage/*.AppImage
# ───────────────────────────────────────────────────────────────────────────
# Windows x64:NSIS 安装包
# ───────────────────────────────────────────────────────────────────────────
windows:
name: Windows x64(NSIS 安装包)
needs: checks
# 默认就跑:windows-runner-01 在线(自报标签 windows-latest)。
# 开关用 inputs 而不是仓库变量的原因见文件头第 4 条 —— 这个坑正是它踩出来的。
if: ${{ github.event.inputs.windows != 'false' }}
# 这台机器上要预装:MSVC Build Tools(tauri 的 Rust 侧要 link.exe)、WebView2
# 运行时(装了包的应用要它才能起窗口;打包本身不需要)与 rustup。
runs-on: ${{ vars.RUNNER_WINDOWS || 'windows-latest' }}
timeout-minutes: 120
env:
TARGET: x86_64-pc-windows-msvc
CARGO_BUILD_TARGET: x86_64-pc-windows-msvc
# 见文件头第 5 条:这台 runner 的 job 跑在**低完整性**(Low Mandatory Level,
# S-1-16-4096)上,而 Windows 不允许低完整性往中完整性写 —— 用户目录全中招:
# ~/.rustup、~/.cargo、%TEMP%、%LOCALAPPDATA% 下新建任何东西都是
# "Access denied (os error 5)",ACL 再宽松也没用(没有 Deny ACE,是完整性
# 级别拦的)。所以 CI 用到的每个"家目录"都挪到 runner 自己的工作目录下,
# 那几处在 job 内的写探测里实测可写。换一台 Windows runner 时改这几行。
RUSTUP_HOME: C:\Users\admin\runner\windows\cache\ci-rustup
CARGO_HOME: C:\Users\admin\runner\windows\cache\ci-cargo
# LOCALAPPDATA/APPDATA 也得改:pnpm 12 的 store 锁与 cache/state 都挂在
# %LOCALAPPDATA% 下(报错原文:pnpm-store-operation-locks\all-stores.lock:
# 拒绝访问),tauri 打包时下载的 NSIS 也放这儿。本机实测:把 LOCALAPPDATA
# 指到可写目录后,pnpm 会在那里建 pnpm-cache / pnpm-state,install 正常。
LOCALAPPDATA: C:\Users\admin\runner\windows\cache\ci-localappdata
APPDATA: C:\Users\admin\runner\windows\cache\ci-appdata
npm_config_cache: C:\Users\admin\runner\windows\cache\ci-npm
npm_config_store_dir: C:\Users\admin\runner\windows\cache\ci-pnpm-store
RUNNER_TOOL_CACHE: C:\Users\admin\runner\windows\cache\ci-toolcache
steps:
- uses: actions/checkout@v4
# 临时诊断:rustup 写 C:\Users\admin\.rustup\tmp 被拒(os error 5),但同一个
# 账户在交互式会话里写得进去 —— 先让 job 自己说清楚"我是谁、哪种完整性级别、
# 哪些目录写得进去"。查完这一段就删。
- name: 诊断(临时,查完删)
shell: pwsh
run: |
"whoami = $(whoami)"
"integrity = $((whoami /groups | Select-String -Pattern 'Mandatory Label') -join ' ')"
"USERPROFILE = $env:USERPROFILE"
"HOME = $env:HOME"
"TEMP / TMP = [$env:TEMP] / [$env:TMP]"
"TMPDIR = [$env:TMPDIR]"
"RUSTUP_HOME = [$env:RUSTUP_HOME] CARGO_HOME = [$env:CARGO_HOME]"
"TOOL_CACHE = [$env:RUNNER_TOOL_CACHE]"
"npm_config_* = store=[$env:npm_config_store_dir] cache=[$env:npm_config_cache]"
"user-level TEMP = $([Environment]::GetEnvironmentVariable('TEMP','User'))"
$targets = @(
"$env:USERPROFILE\.rustup\tmp",
"$env:USERPROFILE\.cargo",
"$env:USERPROFILE\.cargo\registry",
'C:\Users\admin\runner\windows',
'C:\Users\admin\runner\windows\cache',
(Join-Path (Split-Path -Parent $env:GITHUB_WORKSPACE) 'ci-probe')
)
foreach ($p in $targets) {
$verdict = 'OK'
try {
New-Item -ItemType Directory -Force -Path $p -ErrorAction Stop | Out-Null
$f = Join-Path $p 'probe.txt'
Set-Content -Path $f -Value 'x' -ErrorAction Stop
Remove-Item $f -Force -ErrorAction SilentlyContinue
} catch { $verdict = "FAIL: $($_.Exception.Message)" }
"{0,-46} {1}" -f $verdict, $p
}
# 宿主模式的 Windows runner 上,PATH 里的 `bash` 是 C:\Windows\System32\bash.exe
# —— WSL 的启动器,而 WSL 在 runner 账户下起不来,报 E_ACCESSDENIED(日志里是
# 一串 GBK 被当 UTF-8 解出来的乱码 + https://github.com/microsoft/WSL/issues)。
# 于是任何 `shell: bash` 的步骤、以及用 bash 写的 composite action
# (dtolnay/rust-toolchain 就是),都会在 0 秒内失败。
# Git for Windows 自带 bash,把它的 bin 前置到 PATH —— GITHUB_PATH 的语义就是
# **前置**插入,所以它会盖过 System32 那个 —— 后面这些步骤才跑得起来。
#
# 换掉 bash 之后还有第二个坑:这个 job 的环境里 TEMP/TMP 是空的,而 Git Bash 的
# /tmp 是从 TEMP 推出来的;没有 TEMP 它会退回 <Git 安装目录>\tmp
# (C:\Program Files\Git\tmp),那个目录普通权限写不进去,于是 dtolnay 里那句
# log=$(mktemp) 直接挂掉:
# mktemp: failed to create file via template '/tmp/tmp.XXXXXXXXXX': Permission denied
# 所以顺手给 job 指定一个自己建的可写目录(workspace 的同级 ci-tmp)。
# 两个坑都是 2026-10-15 真跑 Windows job 时踩出来的,本地按这样改完后
# mktemp、rustup toolchain install、rustup target add 全部正常。
- name: 让 bash 指向 Git for Windows,并给 job 一个可写的临时目录
shell: pwsh
run: |
if (-not $env:GITHUB_PATH) { throw '这个 runner 没提供 GITHUB_PATH,没法给后续步骤改 PATH' }
if (-not $env:GITHUB_ENV) { throw '这个 runner 没提供 GITHUB_ENV,没法给后续步骤加变量' }
if (-not $env:GITHUB_WORKSPACE) { throw '这个 runner 没提供 GITHUB_WORKSPACE' }
$gitBash = 'C:\Program Files\Git\bin'
if (-not (Test-Path (Join-Path $gitBash 'bash.exe'))) {
throw "找不到 $gitBash\bash.exe:这台机器上没装 Git for Windows,bash 会落到 WSL 上"
}
Add-Content -Path $env:GITHUB_PATH -Value $gitBash -Encoding ascii
$tmpRoot = Join-Path (Split-Path -Parent $env:GITHUB_WORKSPACE) 'ci-tmp'
New-Item -ItemType Directory -Force -Path $tmpRoot | Out-Null
Add-Content -Path $env:GITHUB_ENV -Value "TEMP=$tmpRoot" -Encoding ascii
Add-Content -Path $env:GITHUB_ENV -Value "TMP=$tmpRoot" -Encoding ascii
Add-Content -Path $env:GITHUB_ENV -Value "TMPDIR=$($tmpRoot -replace '\\', '/')" -Encoding ascii
# 第三个坑:pnpm 的 store 操作锁是按 <USERPROFILE>\AppData\Local\
# pnpm-store-operation-locks 算的 —— 实测只把 LOCALAPPDATA 指走它不跟,
# 只把 USERPROFILE 指走它就跟着走(换完 USERPROFILE 后探针目录里立刻出现
# all-stores.lock)。所以 USERPROFILE 也得指到能写的地方,并且先把
# AppData\Local、AppData\Roaming 建好。
$profileRoot = Join-Path (Split-Path -Parent $env:GITHUB_WORKSPACE) 'ci-profile'
New-Item -ItemType Directory -Force -Path (Join-Path $profileRoot 'AppData\Local'), (Join-Path $profileRoot 'AppData\Roaming') | Out-Null
Add-Content -Path $env:GITHUB_ENV -Value "USERPROFILE=$profileRoot" -Encoding ascii
"bash = $(Join-Path $gitBash 'bash.exe')"
"TEMP/TMP/TMPDIR = $tmpRoot"
"USERPROFILE = $profileRoot"
- name: 装 Rust(stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
# 默认 dest 是 ~/setup-pnpm,而 ~ 是中完整性的,低完整性写不进去。
dest: C:\Users\admin\runner\windows\cache\ci-pnpm
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 诊断2(临时,查完删)——pnpm 眼里的目录
shell: pwsh
run: |
"LOCALAPPDATA = $env:LOCALAPPDATA"
"APPDATA = $env:APPDATA"
"pnpm 在 = $((Get-Command pnpm -ErrorAction SilentlyContinue).Source)"
try { "pnpm --version = $(pnpm --version)" } catch { "pnpm 调不动: $($_.Exception.Message)" }
pnpm config list 2>&1 | Select-Object -First 25
"改过的环境变量:RUSTUP_HOME=$env:RUSTUP_HOME | CARGO_HOME=$env:CARGO_HOME | RUNNER_TOOL_CACHE=$env:RUNNER_TOOL_CACHE"
"目标锁目录能不能建:"
foreach ($d in @("$env:LOCALAPPDATA\pnpm-store-operation-locks", 'C:\Users\admin\runner\windows\cache\ci-localappdata\pnpm-store-operation-locks')) {
try { New-Item -ItemType Directory -Force -Path $d -ErrorAction Stop | Out-Null; " OK $d" } catch { " FAIL $d -> $($_.Exception.Message)" }
}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
# nsis 是纯安装包,不需要 WiX;tauri 会自己把 NSIS 下到用户缓存里(这个缓存
# 也跟着 LOCALAPPDATA 走,见上面 job env),所以这台 runner 要能访问 GitHub
# (离线环境改成 --bundles msi 也一样要下 WiX)。
- name: 打包(tauri build)
working-directory: desktop
run: pnpm tauri build --target ${{ env.TARGET }} --bundles nsis
- name: 上传产物
uses: actions/upload-artifact@v3
with:
name: cam-desktop-windows-x64
if-no-files-found: error
retention-days: 14
path: |
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/nsis/*.exe
# ───────────────────────────────────────────────────────────────────────────
# macOS 通用包:.app + .dmg
# ───────────────────────────────────────────────────────────────────────────
macos:
name: macOS 通用(.app + .dmg)
needs: checks
# 还没有 macOS runner,所以只有"手动触发 + 勾上"才跑;等机器注册好,把这一行
# 换成 `github.event.inputs.macos != 'false'` 就变成默认跑(同 Windows)。
if: ${{ github.event.inputs.macos == 'true' }}
# 那台机器上要有 Xcode Command Line Tools(链接要它)与 rustup。
runs-on: ${{ vars.RUNNER_MACOS || 'macos-latest' }}
timeout-minutes: 150
env:
# 注意:这里**不能**设 CARGO_BUILD_TARGET —— universal-apple-darwin 是 tauri
# 自己的伪目标(编两遍再 lipo 合成),裸 cargo 不认它。这个 job 里也没有裸
# cargo 步骤,所有目标都由 tauri build 的 --target 交代。
TARGET: universal-apple-darwin
steps:
- uses: actions/checkout@v4
# 通用包要两个架构的 std 都在。
- name: 装 Rust(stable + 两个 mac 目标)
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
# 只想要当前架构、想省一半编译时间的话,把 --target 换成 aarch64-apple-darwin
# (Apple Silicon 的 runner)并去掉上面 targets 里的 x86_64 即可。
- name: 打包(tauri build)
working-directory: desktop
run: pnpm tauri build --target ${{ env.TARGET }} --bundles app,dmg
# 第二行是兜底:不同 tauri-cli 版本给通用包放的目录略有差异,宁可多写一行,
# 也不要上传时静默少东西。(注意 path 是块标量,里面写 # 是路径不是注释。)
- name: 上传产物
uses: actions/upload-artifact@v3
with:
name: cam-desktop-macos-universal
if-no-files-found: error
retention-days: 14
path: |
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/dmg/*.dmg
desktop/src-tauri/target/*/release/bundle/dmg/*.dmg