Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/actions/cache-macos-mlx/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Cache macOS MLX native build
description: Preserve vendored MLX CMake output before rust-cache removes in-tree path dependencies.
inputs:
profile:
description: Cargo output profile directory (debug or release)
required: true
runs:
using: composite
steps:
- name: Fingerprint MLX native toolchain
id: native
shell: bash
working-directory: openless-all/app
env:
OPENLESS_MLX_PROFILE: ${{ inputs.profile }}
run: |
set -euo pipefail
case "$OPENLESS_MLX_PROFILE" in
debug|release) ;;
*) echo "::error::Unsupported MLX cache profile"; exit 1 ;;
esac
fingerprint=$({
rustc -vV
xcrun clang --version
# Downloadable Metal toolchains can change independently of Clang.
# Their InstalledDir may contain a per-boot mount identifier.
xcrun --sdk macosx metal --version | sed '/^InstalledDir:/d'
xcrun --sdk macosx --show-sdk-version
cmake --version
git -C src-tauri/vendor/qwen3-asr-rs rev-parse HEAD
git -C src-tauri/vendor/qwen3-asr-rs/mlx-c rev-parse HEAD
for name in CARGO_PROFILE_DEV_DEBUG CARGO_PROFILE_TEST_DEBUG CARGO_PROFILE_RELEASE_CODEGEN_UNITS CARGO_PROFILE_RELEASE_STRIP RUSTFLAGS CARGO_ENCODED_RUSTFLAGS CC CXX CFLAGS CXXFLAGS SDKROOT DEVELOPER_DIR MACOSX_DEPLOYMENT_TARGET CMAKE_GENERATOR CMAKE_TOOLCHAIN_FILE; do
printf '%s=%s\n' "$name" "${!name-}"
done
} | shasum -a 256 | awk '{print $1}')
echo "fingerprint=$fingerprint" >> "$GITHUB_OUTPUT"

# Call this action AFTER rust-cache: post actions run in reverse order,
# so MLX is saved before rust-cache prunes src-tauri/vendor path packages.
# Cache only CMake output, not Cargo fingerprints: Cargo still reruns the
# build script and CMake validates its native inputs on every clean checkout.
- uses: actions/cache@v4
with:
path: openless-all/app/src-tauri/target/${{ inputs.profile }}/build/qwen3-asr-rs-*/out
key: macos-mlx-v1-${{ runner.arch }}-${{ inputs.profile }}-${{ steps.native.outputs.fingerprint }}-${{ hashFiles('openless-all/app/src-tauri/Cargo.toml', 'openless-all/app/src-tauri/Cargo.lock', 'openless-all/app/src-tauri/tauri*.json', '.cargo/config.toml', '.github/actions/cache-macos-mlx/action.yml') }}
85 changes: 76 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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++ 依赖和完整
Expand All @@ -226,15 +228,36 @@ 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: Cache macOS MLX native build
if: runner.os == 'macOS' && runner.arch == 'ARM64'
uses: ./.github/actions/cache-macos-mlx
with:
profile: debug

- name: Prepare Windows Sherpa static libraries
if: runner.os == 'Windows'
shell: pwsh
Expand Down Expand Up @@ -284,11 +307,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
Expand All @@ -304,9 +330,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
Expand Down Expand Up @@ -338,3 +366,42 @@ 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
- name: Cache macOS MLX native build
if: runner.arch == 'ARM64'
uses: ./.github/actions/cache-macos-mlx
with:
profile: debug
- 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
50 changes: 37 additions & 13 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}
Expand Down Expand Up @@ -79,11 +73,31 @@ 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: Cache macOS MLX native build
if: matrix.updater-arch == 'aarch64'
uses: ./.github/actions/cache-macos-mlx
with:
profile: release

- name: Prepare Windows Sherpa static libraries
if: matrix.platform == 'windows-latest'
working-directory: 'openless-all/app'
Expand Down Expand Up @@ -192,6 +206,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 不固定)。
Expand Down Expand Up @@ -427,6 +449,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
Expand All @@ -436,6 +459,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
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
55 changes: 55 additions & 0 deletions docs/macos-build-performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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 的产物互相挤占。

MLX 的 CMake 输出另用 [cache-macos-mlx](../.github/actions/cache-macos-mlx/action.yml) 保存。实际使用的 `rust-cache` [源码](https://github.com/Swatinem/rust-cache/blob/6323deb102c322ba6fcbdcafc7e3dddab59af2b6/src/workspace.ts) 排除 workspace 目录内的 path 依赖,导致 `src-tauri/vendor/qwen3-asr-rs` 的原生输出在 post 阶段被清理;首轮验证中,命中 Cargo 缓存仍重建 MLX 约 7 分 17 秒。独立缓存步骤放在 `rust-cache` 后,利用 post 的逆序执行先保存原生输出。缓存按架构、profile、Rust/Clang/Metal/CMake/SDK、子模块提交、编译环境和 manifest/lock/config 隔离,无跨 key 的模糊回退。Metal 版本输出去掉每次启动可能变化的挂载目录,保留实际版本与目标信息。只保存 CMake `out`,不保存 Cargo freshness 指纹,下一轮仍执行 build script 与 CMake 输入校验。

`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 性能已经验证。
Loading
Loading