feat(compositor): port the compositor to macOS (Metal + VideoToolbox) #559
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # feat/ai-edition is a long-lived integration branch that PRs land on for | |
| # months at a time. Without it listed here, every one of those PRs merged with | |
| # no lint, no typecheck, no tests and no PR-title check. | |
| # | |
| # release/** is here for the same reason and it cost more: PRs #167, #168 and | |
| # #169 — 30k+ lines of deletion and refactor — merged into release/1.8.0 with | |
| # every one of those jobs skipped, because a release branch matched neither | |
| # pattern. A release branch is the LAST place to run a build unguarded. | |
| on: | |
| pull_request: | |
| branches: [main, feat/ai-edition, "release/**"] | |
| push: | |
| branches: [main, feat/ai-edition, "release/**"] | |
| jobs: | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup | |
| - run: npm run lint | |
| typecheck: | |
| name: Type Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup | |
| - run: npx tsc --noEmit | |
| typecheck-tests: | |
| name: Typecheck (tests) | |
| runs-on: ubuntu-latest | |
| # A RATCHET, not a gate. tsconfig.json includes only src + electron and | |
| # excludes **/*.test.ts, so no test file has ever been typechecked, and | |
| # vitest transpiles without checking — fixture types have drifted from the | |
| # schemas they claim to build for years. Failing on the whole backlog would | |
| # put a red X on every PR that nobody can fix, and a check everyone ignores | |
| # is worse than no check. So: fail only if the count GROWS. Lower BASELINE | |
| # whenever you fix some; delete this job's baseline logic at zero. | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup | |
| - name: Typecheck tests against a baseline | |
| shell: bash | |
| env: | |
| BASELINE: 71 | |
| run: | | |
| set -uo pipefail | |
| COUNT=$(npx tsc -p tsconfig.test.json --noEmit 2>&1 | grep -c 'error TS' || true) | |
| { | |
| echo "## Test-file typecheck" | |
| echo "" | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| Errors now | ${COUNT} |" | |
| echo "| Baseline | ${BASELINE} |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| if [ "${COUNT}" -gt "${BASELINE}" ]; then | |
| echo "::error::Test-file type errors went ${BASELINE} -> ${COUNT}. Fix the new ones, or raise BASELINE in ci.yml with a reason." | |
| npx tsc -p tsconfig.test.json --noEmit 2>&1 | grep 'error TS' || true | |
| exit 1 | |
| fi | |
| if [ "${COUNT}" -lt "${BASELINE}" ]; then | |
| echo "::notice::Test-file type errors down to ${COUNT} (baseline ${BASELINE}). Lower BASELINE in ci.yml to lock the win in." | |
| fi | |
| docs: | |
| name: Docs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # No ./.github/actions/setup: check-docs.mjs imports only node builtins, | |
| # so npm ci would be a minute of install for nothing. Node 22 is here for | |
| # import.meta.dirname (needs >= 20.11). | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - run: npm run docs:check | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup | |
| - run: npm run test | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup | |
| - run: npx vite build | |
| # PR #189 — port macOS du compositor (Metal + VideoToolbox). | |
| # | |
| # Compile check on a `macos-14` runner (Apple Silicon). Verifies that the | |
| # scaffold + engine-layer code added by #189 actually compiles for | |
| # `aarch64-apple-darwin` — bindgen + cc on macOS produce the ffmpeg FFI | |
| # bindings + the VideoToolbox context that the Rust crate statically | |
| # references, and the `metal`/`objc`/`block`/`core-foundation` crates need | |
| # their macOS system frameworks (Metal/CoreVideo/CoreMedia) linked. | |
| # | |
| # ffmpeg via Homebrew: this job installs `ffmpeg` from homebrew-core on the | |
| # runner to satisfy bindgen + the cargo:rustc-link-lib lines for avformat/ | |
| # avcodec/avutil/swscale/swresample. NB: homebrew's `ffmpeg` is GPL-3.0 (it | |
| # bundles x264/x265/svt-av1 etc.) — that is fine for a CI compile-check, but | |
| # the dev/runtime story is different: `scripts/fetch-ffmpeg.mjs` documents | |
| # that BtbN publishes no macOS build, so a pinned LGPL macOS dylib has to | |
| # land separately before we can ship. The vendored `electron/native/bin/ | |
| # darwin-*/` directory is the runtime pin and is empty today. | |
| # | |
| # Why this job runs at all today: every commit on #189 that touches | |
| # compositor_macos.rs / pipeline_macos.rs / mac_frames.rs / shaders.metal / | |
| # d3d_macos.rs / text_macos.rs is unverifiable from Windows (cross-compile | |
| # requires the macOS SDK + headers, which we don't have). This job is the | |
| # only signal that the macOS-side Rust still type-checks. | |
| # | |
| # `cargo test`, NOT just `cargo check` — and that difference is the whole point. | |
| # Three of the bugs that kept the first macOS run from rendering anything are | |
| # invisible to a type-check and are each pinned by a test now: | |
| # * `shaders.metal` is compiled AT RUNTIME by `new_library_with_source`, so a | |
| # whole file of invalid MSL (HLSL's global `cbuffer`/`Texture2D` have no MSL | |
| # equivalent) type-checks perfectly — `every_shader_entry_point_compiles` | |
| # catches it. macos-14 runners have a real Metal device, so it runs for real. | |
| # * `AVERROR(EAGAIN)` is -11 on Windows/Linux and -35 on macOS. A hardcoded -11 | |
| # compiles anywhere and silently decodes zero frames — | |
| # `averror_constants_match_the_ffmpeg_macros` confronts it with shim.c. | |
| # * pipeline-state creation rejects shader/attachment mismatches only at | |
| # `newRenderPipelineState` time — `the_compositor_builds_on_the_system_device`. | |
| rust-macos-compositor-check: | |
| name: Rust test (macOS compositor) | |
| runs-on: macos-14 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| run: | | |
| curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --target aarch64-apple-darwin | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| - name: Install ffmpeg (homebrew-core) and verify headers/libs | |
| run: | | |
| brew update | |
| brew install ffmpeg | |
| ls /opt/homebrew/opt/ffmpeg/include | head -5 | |
| ls /opt/homebrew/opt/ffmpeg/lib | head -5 | |
| /opt/homebrew/opt/ffmpeg/bin/ffmpeg -hide_banner -version | head -1 | |
| # MAC_FFMPEG_DIR only reaches build.rs because it now takes precedence over | |
| # FFMPEG_DIR on macOS. crates/.cargo/config.toml sets FFMPEG_DIR in a GLOBAL | |
| # [env] block (cargo has no [target.<cfg>.env] — the macOS section in that file | |
| # is inert and cargo warns "unused key"), so before that change this job pointed | |
| # bindgen at the win64 tree and could never have gone green. | |
| - name: cargo test (compositor, aarch64-apple-darwin) | |
| env: | |
| MAC_FFMPEG_DIR: /opt/homebrew/opt/ffmpeg | |
| run: | | |
| cd crates | |
| cargo test -p openscreen-compositor --lib --tests | |
| - name: cargo build (napi addon) | |
| env: | |
| MAC_FFMPEG_DIR: /opt/homebrew/opt/ffmpeg | |
| run: | | |
| cd crates | |
| cargo build -p compositor-view-napi --release | |
| # The Windows half of the compositor had NO type-check on any pull request. | |
| # `ci.yml`'s only Rust job is the macOS one above; `compositor_windows.rs` — the | |
| # 3654-line D3D11 engine that every Windows user actually runs — is compiled | |
| # exclusively by `build.yml`'s `build-windows` step (`npm run build:win`), and | |
| # build.yml triggers only on `push: tags: v*` or `workflow_dispatch`. A typo in | |
| # that file therefore surfaced when someone cut a release, not when they pushed it. | |
| # | |
| # That gap is what makes any cross-platform refactor of the compositor a blind | |
| # edit, so it gets closed before the refactor rather than after. | |
| # | |
| # `cargo check`, not `build`: the point is "does the Windows engine still compile", | |
| # and check is roughly half the wall-time of a full build on a windows runner. | |
| rust-windows-compositor-check: | |
| name: Rust check (Windows compositor) | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version-file: .nvmrc | |
| # bindgen needs libclang. `crates/.cargo/config.toml` pins LIBCLANG_PATH to | |
| # C:\Program Files\LLVM\bin, which is where the windows-latest image already | |
| # has LLVM — so the pin resolves with no extra install. | |
| - name: Check LLVM is where the cargo config expects it | |
| shell: bash | |
| run: ls "/c/Program Files/LLVM/bin/libclang.dll" | |
| # Vendors the pinned BtbN LGPL-shared build into | |
| # crates/thirdparty/ffmpeg-n8.1.2-win64-lgpl-shared — the exact directory | |
| # FFMPEG_DIR points at (crates/.cargo/config.toml). build.rs reads the headers | |
| # from there and links the import libs. | |
| - name: Vendor the pinned ffmpeg | |
| run: npm run fetch:ffmpeg | |
| - name: cargo check (compositor + napi addon) | |
| shell: bash | |
| run: | | |
| cd crates | |
| cargo check -p openscreen-compositor -p compositor-view-napi --all-targets | |
| semantic-pr: | |
| name: Validate PR title (semantic) | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: amannn/action-semantic-pull-request@v5 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| types: | | |
| feat | |
| fix | |
| chore | |
| refactor | |
| perf | |
| docs | |
| test | |
| build | |
| ci | |
| style | |
| revert | |
| requireScope: false |