perf(macos): 加速 Rust CI 与桌面打包
This commit is contained in:
1 parent
d9113e0b03
commit
b268dcf978
7 files changed
+317
-38
No files matched your search
@@ -12,6 +12,12 @@ on:
|
||||
pull_request:
|
||||
branches: [main, beta]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: Platforms to check (macos runs only macOS gates)
|
||||
type: choice
|
||||
options: [all, macos]
|
||||
default: all
|
||||
|
||||
# 同一 PR 快速重复推送时取消旧运行;workflow_dispatch 用 run_id 隔离。
|
||||
concurrency:
|
||||
@@ -20,6 +26,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
android-check:
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.platform != 'macos'
|
||||
name: Android cargo check
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -166,6 +173,7 @@ jobs:
|
||||
--no-daemon
|
||||
|
||||
linux-core-contract:
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.platform != 'macos'
|
||||
name: Linux core tests
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
@@ -194,13 +202,7 @@ jobs:
|
||||
# 一个平台挂掉不阻塞其他平台拿到验证结果。
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
label: macOS
|
||||
preflight: false
|
||||
- os: windows-latest
|
||||
label: Windows
|
||||
preflight: true
|
||||
include: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos' && '[{"os":"macos-latest","label":"macOS","preflight":false}]' || '[{"os":"macos-latest","label":"macOS","preflight":false},{"os":"windows-latest","label":"Windows","preflight":true}]') }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
# 新增 shared Core 后,macOS 首次编译同时构建 MLX C++ 依赖和完整
|
||||
@@ -226,15 +228,30 @@ jobs:
|
||||
cache: npm
|
||||
cache-dependency-path: openless-all/app/package-lock.json
|
||||
|
||||
# 现有测试继续用 stable;额外安装声明的 MSRV,供下方兼容性门禁显式调用。
|
||||
# macOS MSRV 在独立 job 并行检查;Windows 保留原有检查顺序。
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Configure macOS check profile
|
||||
if: runner.os == 'macOS'
|
||||
run: echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: swatinem/rust-cache@v2
|
||||
if: runner.os == 'Windows'
|
||||
with:
|
||||
workspaces: 'openless-all/app/src-tauri -> target'
|
||||
|
||||
- name: Cache macOS stable dependencies
|
||||
if: runner.os == 'macOS'
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
key: macos-stable-v1
|
||||
workspaces: |
|
||||
openless-all/app -> target
|
||||
openless-all/app/src-tauri -> target
|
||||
|
||||
- name: Prepare Windows Sherpa static libraries
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
@@ -284,11 +301,14 @@ jobs:
|
||||
run: cargo test --locked -p openless-core hardening_actually_narrows_the_writable_roots -- --ignored --nocapture --test-threads=1
|
||||
|
||||
- name: Check Tauri backend (cargo check)
|
||||
if: runner.os == 'Windows'
|
||||
run: cargo check --locked --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
# test 编译并运行 lib/bin,覆盖原 cargo check 的生产 binary 路径。
|
||||
# 避免先 metadata-only check、再为同一依赖图做一次 codegen。
|
||||
- name: Run Rust backend unit tests
|
||||
if: runner.os != 'Windows'
|
||||
run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib
|
||||
run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --bins --timings
|
||||
|
||||
- name: Compile Rust backend unit tests (Windows)
|
||||
# Windows runner 能链接 lib test binary,但干净镜像缺少可选 native runtime
|
||||
@@ -304,9 +324,11 @@ jobs:
|
||||
run: cargo test --locked --manifest-path src-tauri/backend-tests/Cargo.toml
|
||||
|
||||
- name: Check Tauri backend with Rust 1.88 MSRV
|
||||
if: runner.os == 'Windows'
|
||||
run: cargo +1.88.0 check --locked --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
- name: Compile backend tests with Rust 1.88 MSRV
|
||||
if: runner.os == 'Windows'
|
||||
run: cargo +1.88.0 test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --no-run
|
||||
|
||||
- name: Verify version sync across all 5 files
|
||||
@@ -338,3 +360,37 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "[ok] 全部 5 处版本号一致:$PKG"
|
||||
|
||||
macos-msrv:
|
||||
name: macOS Rust 1.88 MSRV
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
CARGO_BUILD_JOBS: 2
|
||||
CARGO_PROFILE_DEV_DEBUG: 0
|
||||
CARGO_PROFILE_TEST_DEBUG: 0
|
||||
CMAKE_BUILD_PARALLEL_LEVEL: 2
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openless-all/app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: openless-all/app/package-lock.json
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
key: macos-msrv-v1
|
||||
workspaces: |
|
||||
openless-all/app/src-tauri -> target
|
||||
openless-all/app/src-tauri/backend-tests -> target
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: Check Tauri backend with Rust 1.88 MSRV
|
||||
run: cargo +1.88.0 check --locked --manifest-path src-tauri/Cargo.toml --timings
|
||||
- name: Compile backend tests with Rust 1.88 MSRV
|
||||
run: cargo +1.88.0 test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --no-run --timings
|
||||
@@ -21,6 +21,12 @@ on:
|
||||
tags:
|
||||
- 'v*-tauri'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: Desktop platforms to build (manual builds do not publish a release)
|
||||
type: choice
|
||||
options: [all, macos]
|
||||
default: all
|
||||
|
||||
# 同一 tag 重复推送只跑最新一次;workflow_dispatch 用 run_id 隔离避免互相取消。
|
||||
concurrency:
|
||||
@@ -34,19 +40,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
rust-target: aarch64-apple-darwin
|
||||
updater-target: darwin
|
||||
updater-arch: aarch64
|
||||
- platform: macos-15-intel
|
||||
rust-target: x86_64-apple-darwin
|
||||
updater-target: darwin
|
||||
updater-arch: x86_64
|
||||
- platform: windows-latest
|
||||
rust-target: x86_64-pc-windows-msvc
|
||||
updater-target: windows
|
||||
updater-arch: x86_64
|
||||
include: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos' && '[{"platform":"macos-latest","rust-target":"aarch64-apple-darwin","updater-target":"darwin","updater-arch":"aarch64"},{"platform":"macos-15-intel","rust-target":"x86_64-apple-darwin","updater-target":"darwin","updater-arch":"x86_64"}]' || '[{"platform":"macos-latest","rust-target":"aarch64-apple-darwin","updater-target":"darwin","updater-arch":"aarch64"},{"platform":"macos-15-intel","rust-target":"x86_64-apple-darwin","updater-target":"darwin","updater-arch":"x86_64"},{"platform":"windows-latest","rust-target":"x86_64-pc-windows-msvc","updater-target":"windows","updater-arch":"x86_64"}]') }}
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
@@ -79,11 +73,25 @@ jobs:
|
||||
with:
|
||||
targets: ${{ matrix.rust-target }}
|
||||
|
||||
# 在恢复缓存前设置实际编译环境,让 cache key 包含 macOS profile。
|
||||
- name: Configure macOS build environment
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
working-directory: openless-all/app
|
||||
run: bash scripts/macos-build-env.sh
|
||||
|
||||
- name: Cache Cargo
|
||||
if: matrix.platform == 'windows-latest'
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: 'openless-all/app/src-tauri -> target'
|
||||
|
||||
- name: Cache macOS release dependencies
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
key: macos-release-v1
|
||||
workspaces: 'openless-all/app/src-tauri -> target'
|
||||
|
||||
- name: Prepare Windows Sherpa static libraries
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: 'openless-all/app'
|
||||
@@ -192,6 +200,14 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
run: bash scripts/build-mac.sh
|
||||
|
||||
- name: Upload macOS Cargo timings
|
||||
if: always() && startsWith(matrix.platform, 'macos')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-cargo-timings-${{ matrix.updater-arch }}
|
||||
path: openless-all/app/src-tauri/target/cargo-timings/*.html
|
||||
if-no-files-found: ignore
|
||||
|
||||
# ── Windows:先 build OpenLessIme.dll(x64+x86),再跑 tauri bundle。
|
||||
# openless-ime.wxs 用 $(env.OPENLESS_IME_DLL_X64) / _X86 拿绝对路径,
|
||||
# 跨 candle/light cwd 都能 resolve(Tauri wix bundler cwd 不固定)。
|
||||
@@ -427,6 +443,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openless-macos-${{ matrix.updater-arch }}
|
||||
compression-level: 0
|
||||
path: |
|
||||
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
if-no-files-found: error
|
||||
@@ -436,6 +453,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: openless-macos-${{ matrix.updater-arch }}-updater
|
||||
compression-level: 0
|
||||
path: |
|
||||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz
|
||||
openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
## 平台与运营
|
||||
|
||||
- [macOS CI 与打包耗时](macos-build-performance.md):基线日志、Rust 编译优化、缓存边界与仅 macOS 验证入口。
|
||||
- [Android APK / 悬浮窗计划](android-mobile-apk-overlay-plan.md)(实施中)
|
||||
- [火山引擎 ASR 配置](volcengine-setup.md)
|
||||
- [讯飞(iflytek)ASR 配置](xfyun-asr.md)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# macOS CI 与打包耗时
|
||||
|
||||
状态:实现说明;更新:2026-09-25。范围仅包含 macOS 检查和桌面打包。Windows、Android、Linux 的编译参数与发布行为由原有工作流维护。
|
||||
|
||||
## 调研依据
|
||||
|
||||
对照 `beta` 的 `d9113e0b`、[CI 35984434219](https://github.com/Open-Less/openless/actions/runs/35984434219) 与 [桌面构建 35989822601](https://github.com/Open-Less/openless/actions/runs/35989822601) 的完整日志:
|
||||
|
||||
| 基线步骤 | 时间 | 日志证据 |
|
||||
| --- | --- | --- |
|
||||
| macOS 检查 job | 24 分 59 秒 | stable 检查、测试、MSRV 串行执行 |
|
||||
| stable `cargo check` | 7 分 54 秒 | 含 MLX,本轮 dev profile 带 debug info |
|
||||
| stable Tauri 库测试编译 | 7 分 27 秒 | 再次编译 qwen3、Core、Host,test profile 不带 debug info |
|
||||
| Rust 1.88 Host 检查 | 5 分 03 秒 | 第三次编译 qwen3 与 Host |
|
||||
| Rust 1.88 独立 backend-tests 编译 | 58.51 秒 | 独立 target 未配置缓存 |
|
||||
| Apple Silicon release Rust 编译 | 14 分 16 秒 | 依赖缓存命中约 731 MB,仍重新编译 qwen3、Core、Host |
|
||||
| Intel release Rust 编译 | 13 分 57 秒 | 依赖缓存命中约 700 MB,仍重新编译 Core、Host |
|
||||
|
||||
ARM/Intel 的整个打包步骤分别约 15 分 17 秒、15 分 08 秒。主要等待发生在 Rust,而不是 npm 安装、DMG 或 artifact 上传。时间是该次运行的观测值,不是不同 runner、缓存和提交之间的性能保证。
|
||||
|
||||
源码中的相关因素:
|
||||
|
||||
- `ci.yml` 的 macOS job 先 metadata-only check,再生成测试机器码;dev/test 的 debug 配置也不同。MSRV 与 stable 共用 job 和缓存,Core workspace、独立 backend-tests 的 target 没有全部纳入缓存。
|
||||
- Tauri release profile 使用 `opt-level=3`、thin LTO 和 `codegen-units=1`。最后一个设置限制单个大 crate 的 LLVM 并行能力;命中第三方依赖缓存仍无法避免 Core/Host 的代码生成成本。
|
||||
- `build-mac.sh` 曾将所有 `qwen3-asr-rs-*` 目录当成重复输出。Cargo 实际分别保存 build-script 可执行文件和 `OUT_DIR`;只留一个目录会破坏下一轮缓存。
|
||||
- `rust-cache` 默认只保留依赖产物,不应把命中缓存等同于整个应用无须重编译。参见 [rust-cache 缓存行为](https://github.com/Swatinem/rust-cache#cache-details)。没有添加 sccache:该工具不能缓存调用系统 linker 的 bin/cdylib/proc-macro,参见 [官方限制](https://github.com/mozilla/sccache/blob/main/docs/Rust.md)。
|
||||
|
||||
## 当前流程
|
||||
|
||||
`ci.yml` 的 macOS stable job 用 `cargo test --lib --bins` 编译并运行库及二进制目标,覆盖库的生产构建与测试构建。MSRV 单独并行执行,继续检查完整 Tauri Host 并编译独立 backend-tests。两个 job 均保留 `--locked`、MLX 子模块与两路编译并发限制;stable 保留全部前端/合同测试和 Codex sandbox 实测。
|
||||
|
||||
macOS 检查统一关闭 dev/test debug info,分别缓存 Core、Host、backend-tests 的实际 target 目录。stable、MSRV、release 缓存分开,避免不同工具链和 profile 的产物互相挤占。
|
||||
|
||||
`scripts/macos-build-env.sh` 为 macOS 打包默认设置 `CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16`,保留 `opt-level=3`、thin LTO 和 unwind;环境变量可以显式覆盖为其他值。参数取舍依据 [Cargo profiles](https://doc.rust-lang.org/cargo/reference/profiles.html#codegen-units):更多 codegen units 允许更快的并行代码生成,可能影响最终体积或优化效果。共享 `Cargo.toml` 不修改。CI 在恢复缓存前加载同一环境,本地 `build-mac.sh` 也加载它。
|
||||
|
||||
MLX 清理只比较含非空 metallib 的输出目录,保留 build-script 可执行文件。构建前删除本次架构的旧 app、DMG 和 updater,保留 Cargo 编译缓存;Tauri 非零退出直接失败。因此热构建复用旧时间戳二进制时仍能正确打包,失败时也不会接受旧安装包。现有用途声明、签名、公证和 MLX 包内容校验继续执行。
|
||||
|
||||
## 仅 macOS 的验证入口
|
||||
|
||||
从待验证分支手动触发已有工作流:
|
||||
|
||||
```sh
|
||||
gh workflow run ci.yml --repo Open-Less/openless --ref <branch> -f platform=macos
|
||||
gh workflow run release-tauri.yml --repo Open-Less/openless --ref <branch> -f platform=macos
|
||||
```
|
||||
|
||||
前者只运行 macOS stable/MSRV,后者并行生成 Apple Silicon 与 Intel 的桌面包。使用分支 ref,不创建 tag 或 GitHub Release。省略 input 的既有手动运行以及 tag 发布仍使用原有全部平台矩阵。
|
||||
|
||||
桌面工作流上传两个架构的 Cargo HTML timings;失败时若已生成计时文件也会上传。DMG/updater 本身已压缩,artifact 使用 `compression-level: 0`。
|
||||
|
||||
本地在 `openless-all/app` 运行 `npm test`、`cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --bins` 与 `INSTALL=0 bash scripts/build-mac.sh`。`macos-build-cache.test.mjs` 实际执行隔离的 shell 构建流程,验证缓存目录保留、重复热打包、旧产物清理以及失败传播。
|
||||
|
||||
性能验收应记录一次新缓存运行及相同提交的再次运行,分别报告 Rust 编译、完整 job 和产物大小。不同机器的本地构建时间不与 GitHub runner 直接比较;构建通过不等于真实设备 ASR 性能已经验证。
|
||||
@@ -27,25 +27,28 @@ fi
|
||||
echo "▶ 检查 Apple Silicon MLX 构建依赖"
|
||||
npm run check:macos-metal-toolchain
|
||||
|
||||
# Homebrew rustc 在 macOS 上对 `strip=symbols` 生成的 proc-macro dylib
|
||||
# 可能报 "mis-aligned LINKEDIT string pool"。仅官方 macOS 发布脚本降级
|
||||
# 为 debuginfo;Cargo.toml 的全局 profile 仍让 Linux/Windows/Android 使用 symbols。
|
||||
export CARGO_PROFILE_RELEASE_STRIP=debuginfo
|
||||
export RUSTC_WRAPPER="$PWD/scripts/rustc-macos-proc-macro-wrapper.sh"
|
||||
source scripts/macos-build-env.sh
|
||||
echo "▶ Cargo release codegen units: ${CARGO_PROFILE_RELEASE_CODEGEN_UNITS} (macOS only)"
|
||||
echo "▶ Cargo release strip: ${CARGO_PROFILE_RELEASE_STRIP} (macOS only)"
|
||||
echo "▶ Rust proc-macro host wrapper: ${RUSTC_WRAPPER}"
|
||||
|
||||
# 只保留最新一份 qwen3-asr-rs 构建目录:本地混跑 cargo check/test/build 会按不同
|
||||
# feature 上下文生成多份,各自带一份 metallib,会让暂存脚本拒绝猜测。
|
||||
KEEP_QWEN_DIR="$(ls -dt src-tauri/target/release/build/qwen3-asr-rs-* 2>/dev/null | head -1 || true)"
|
||||
# Cargo 为 build-script 可执行文件和 OUT_DIR 创建不同目录。只在多个
|
||||
# metallib 输出之间清理,保留所有 build-script 缓存,避免每次重新编译。
|
||||
KEEP_QWEN_DIR=""
|
||||
for d in src-tauri/target/release/build/qwen3-asr-rs-*; do
|
||||
[ -s "$d/out/lib/mlx.metallib" ] || continue
|
||||
if [ -z "$KEEP_QWEN_DIR" ] || [ "$d/out/lib/mlx.metallib" -nt "$KEEP_QWEN_DIR/out/lib/mlx.metallib" ]; then
|
||||
KEEP_QWEN_DIR="$d"
|
||||
fi
|
||||
done
|
||||
if [ -n "$KEEP_QWEN_DIR" ]; then
|
||||
for d in src-tauri/target/release/build/qwen3-asr-rs-*; do
|
||||
[ -s "$d/out/lib/mlx.metallib" ] || continue
|
||||
[ "$d" = "$KEEP_QWEN_DIR" ] || rm -rf "$d"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "▶ tauri build"
|
||||
BUILD_START_TS="$(date +%s)"
|
||||
TAURI_BUILD_ARGS=(build --ci)
|
||||
case "$(uname -m)" in
|
||||
arm64)
|
||||
@@ -63,16 +66,17 @@ esac
|
||||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ -n "${TAURI_SIGNING_PRIVATE_KEY_PATH:-}" ]; then
|
||||
TAURI_BUILD_ARGS+=(--config '{"bundle":{"createUpdaterArtifacts":true}}')
|
||||
fi
|
||||
# bundle_dmg(AppleScript)在 Xcode beta 上可能退出非零;.app 与 DMG 是否真实
|
||||
# 产出交给下方的新鲜度/存在性校验判定,不在这一步盲 abort。
|
||||
npm run tauri -- "${TAURI_BUILD_ARGS[@]}" || echo "⚠ tauri build 退出码非零,继续校验产物"
|
||||
|
||||
APP_VERSION="$(node -p "require('./package.json').version")"
|
||||
DMG_PATH="$DMG_DIR/OpenLess_${APP_VERSION}_${MAC_BUNDLE_ARCH}.dmg"
|
||||
|
||||
# bundle 必须是本次构建产出的(与构建开始时间比;打包后原始二进制还会被签名
|
||||
# 触碰,不能拿它当基准)。
|
||||
if [ ! -d "$APP" ] || [ "$(stat -f %m "$APP/Contents/MacOS/openless")" -lt "$BUILD_START_TS" ]; then
|
||||
# 清掉交付产物,保留 Cargo 缓存。热构建可复用旧时间戳的二进制,不能用
|
||||
# 编译文件的 mtime 判断 bundle 新鲜度;Tauri 失败时也不能接受上轮安装包。
|
||||
rm -rf "$APP"
|
||||
rm -f "$DMG_PATH" "${APP}.tar.gz" "${APP}.tar.gz.sig"
|
||||
TAURI_BUILD_ARGS+=(-- --locked --timings)
|
||||
npm run tauri -- "${TAURI_BUILD_ARGS[@]}"
|
||||
|
||||
if [ ! -f "$APP/Contents/MacOS/openless" ]; then
|
||||
echo "✗ $APP 缺失或不是本次构建的产物(打包未完成),中止"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
console.log('macOS build cache tests skipped on other platforms');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const scripts = dirname(fileURLToPath(import.meta.url));
|
||||
const root = mkdtempSync(join(tmpdir(), 'openless-macos-build-'));
|
||||
const app = join(root, 'src-tauri/target/release/bundle/macos/OpenLess.app');
|
||||
const dmg = join(root, 'src-tauri/target/release/bundle/dmg/OpenLess_1.2.3_x64.dmg');
|
||||
const builds = join(root, 'src-tauri/target/release/build');
|
||||
|
||||
function put(path, content) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, content);
|
||||
}
|
||||
|
||||
function executable(name, content) {
|
||||
const path = join(root, 'bin', name);
|
||||
put(path, `#!/usr/bin/env bash\nset -euo pipefail\n${content}\n`);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
function run(mode = 'success') {
|
||||
const env = { ...process.env };
|
||||
for (const name of Object.keys(env)) {
|
||||
if (/^(APPLE_|TAURI_SIGNING_|CARGO_PROFILE_RELEASE_|GITHUB_ENV$)/.test(name)) delete env[name];
|
||||
}
|
||||
return spawnSync('bash', ['scripts/build-mac.sh'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
env: { ...env, INSTALL: '0', BUILD_FIXTURE_MODE: mode, PATH: `${root}/bin:${env.PATH}` },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(join(root, 'scripts'), { recursive: true });
|
||||
for (const name of [
|
||||
'build-mac.sh',
|
||||
'macos-build-env.sh',
|
||||
'check-macos-speech-usage-description.sh',
|
||||
]) {
|
||||
copyFileSync(join(scripts, name), join(root, 'scripts', name));
|
||||
}
|
||||
put(join(root, 'package.json'), '{"version":"1.2.3"}');
|
||||
put(
|
||||
join(root, 'fixture.plist'),
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plist version="1.0"><dict>
|
||||
<key>NSMicrophoneUsageDescription</key><string>Microphone</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key><string>Speech</string>
|
||||
</dict></plist>`,
|
||||
);
|
||||
|
||||
executable('uname', 'echo x86_64');
|
||||
executable('codesign', 'echo com.apple.security.device.audio-input');
|
||||
executable('xattr', 'exit 1');
|
||||
executable(
|
||||
'npm',
|
||||
`
|
||||
if [ "$*" = "run check:macos-metal-toolchain" ]; then exit 0; fi
|
||||
[[ "$*" == *"-- --locked --timings"* ]]
|
||||
[[ "$CARGO_PROFILE_RELEASE_CODEGEN_UNITS" == 16 ]]
|
||||
[[ "$CARGO_PROFILE_RELEASE_STRIP" == debuginfo ]]
|
||||
app=src-tauri/target/release/bundle/macos/OpenLess.app
|
||||
dmg=src-tauri/target/release/bundle/dmg/OpenLess_1.2.3_x64.dmg
|
||||
# Old bundles must be gone before compilation starts, while Cargo stays warm.
|
||||
[[ ! -e "$app" && ! -e "$dmg" && ! -e "$app.tar.gz" && ! -e "$app.tar.gz.sig" ]]
|
||||
[[ -f src-tauri/target/release/build/qwen3-asr-rs-helper/build-script-build ]]
|
||||
[[ -f src-tauri/target/release/build/qwen3-asr-rs-new/out/lib/mlx.metallib ]]
|
||||
[[ ! -e src-tauri/target/release/build/qwen3-asr-rs-old ]]
|
||||
if [ "$BUILD_FIXTURE_MODE" = missing ]; then exit 0; fi
|
||||
mkdir -p "$app/Contents/MacOS" "$(dirname "$dmg")"
|
||||
cp fixture.plist "$app/Contents/Info.plist"
|
||||
echo binary > "$app/Contents/MacOS/openless"
|
||||
# Cargo may reuse a binary compiled before this packaging invocation.
|
||||
touch -t 200001010000 "$app/Contents/MacOS/openless"
|
||||
echo dmg > "$dmg"
|
||||
if [ "$BUILD_FIXTURE_MODE" = failure ]; then exit 23; fi
|
||||
`,
|
||||
);
|
||||
|
||||
put(join(builds, 'qwen3-asr-rs-helper/build-script-build'), 'cached executable');
|
||||
put(join(builds, 'qwen3-asr-rs-new/out/lib/mlx.metallib'), 'new shader');
|
||||
put(join(builds, 'qwen3-asr-rs-old/out/lib/mlx.metallib'), 'old shader');
|
||||
utimesSync(join(builds, 'qwen3-asr-rs-old/out/lib/mlx.metallib'), 1, 1);
|
||||
put(join(app, 'Contents/MacOS/openless'), 'stale binary');
|
||||
put(dmg, 'stale dmg');
|
||||
put(`${app}.tar.gz`, 'stale updater');
|
||||
put(`${app}.tar.gz.sig`, 'stale signature');
|
||||
|
||||
for (let repeat = 0; repeat < 2; repeat += 1) {
|
||||
const result = run();
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
assert.equal(readFileSync(dmg, 'utf8').trim(), 'dmg');
|
||||
assert.equal(
|
||||
readFileSync(join(builds, 'qwen3-asr-rs-helper/build-script-build'), 'utf8'),
|
||||
'cached executable',
|
||||
);
|
||||
}
|
||||
const failure = run('failure');
|
||||
assert.equal(failure.status, 23, failure.stdout + failure.stderr);
|
||||
const missing = run('missing');
|
||||
assert.notEqual(missing.status, 0, missing.stdout + missing.stderr);
|
||||
assert.equal(existsSync(app), false);
|
||||
assert.equal(existsSync(dmg), false);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
console.log('macOS warm build and packaging failure tests passed');
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Source locally; execute before rust-cache in GitHub Actions.
|
||||
set -euo pipefail
|
||||
|
||||
MACOS_BUILD_APP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
# Keep opt-level=3 and thin LTO, while allowing LLVM to compile large crates
|
||||
# in parallel. The shared Cargo profile continues to govern other platforms.
|
||||
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS="${CARGO_PROFILE_RELEASE_CODEGEN_UNITS:-16}"
|
||||
# Avoid malformed proc-macro dylibs with the macOS strip tool.
|
||||
export CARGO_PROFILE_RELEASE_STRIP=debuginfo
|
||||
export RUSTC_WRAPPER="$MACOS_BUILD_APP_ROOT/scripts/rustc-macos-proc-macro-wrapper.sh"
|
||||
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
{
|
||||
echo "CARGO_PROFILE_RELEASE_CODEGEN_UNITS=$CARGO_PROFILE_RELEASE_CODEGEN_UNITS"
|
||||
echo "CARGO_PROFILE_RELEASE_STRIP=$CARGO_PROFILE_RELEASE_STRIP"
|
||||
echo "RUSTC_WRAPPER=$RUSTC_WRAPPER"
|
||||
} >> "$GITHUB_ENV"
|
||||
fi
|
||||
Reference in new issue
Block a user