# 构建桌面客户端(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 不可写时退回 # \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,而是按 \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 # 宿主模式的 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 它会退回 \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 操作锁是按 \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: 前端依赖(严格按 lock 装) working-directory: desktop shell: pwsh # 这台 runner 上 pnpm 自己算出来的 store 路径很怪(\node_modules\.bin\ # store\v11),而且 --store-dir 的 env 写法它不认;这里直接给 CLI 参数: # 显式短路径的 store + copy 导入(不用硬链接),并把输出改成适合日志的形式。 # (shell 写死 pwsh:一是这台 runner 上 Windows 步骤本来就该用 pwsh, # 二是 actionlint 看到反斜杠路径会拿 shellcheck 的 bash 规则去挑刺。) run: pnpm install --frozen-lockfile --store-dir C:\Users\admin\runner\windows\cache\ps --package-import-method=copy --reporter=append-only # 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