Repository navigation
flake: add packages.outloud-cuda for whisper.cpp CUDA on x86_64-linux #172
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
| # PR / push CI: correctness, compliance, MSRV, headless, and a cheap build of | |
| # every shipped target so cross-platform breakage is caught per-PR instead of | |
| # at release time. | |
| # | |
| # Design decisions (full rationale in docs/build-and-release.md#ci): | |
| # - Scripts, not inline run-blocks, for anything longer than two lines: | |
| # contributors run scripts/ci-*.sh locally and get the identical result. | |
| # - Swatinem/rust-cache over manual actions/cache: it keys on the lockfile | |
| # AND toolchain AND target list, and prunes stale artifacts, which manual | |
| # cache keys always eventually get wrong. | |
| # - Every cargo invocation is --locked: CI must build what the lockfile | |
| # says, or the lockfile is fiction. | |
| name: ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| # A superseded push to the same PR cancels the older run; saves the finite | |
| # macOS/Windows runner minutes for the commit that matters. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| # Incremental compilation is a lose on CI: the cache restore already covers | |
| # warm starts, and incremental artifacts bloat the cache by gigabytes. | |
| CARGO_INCREMENTAL: "0" | |
| jobs: | |
| # Lint + test on the two OSes with OS-specific code paths. Linux covers the | |
| # pure crates; macOS covers ax-edit's real FFI. Windows joins this list the | |
| # day a Windows backend exists. | |
| check: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-24.04, macos-15] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # rustup reads rust-toolchain.toml automatically; no toolchain action | |
| # needed for the default case, which is one less pinned dependency. | |
| # The app icon, on macOS only (qlmanage/sips/iconutil are macOS tools). | |
| # | |
| # Nothing else in normal CI looks at it, so a broken logo.svg would | |
| # have surfaced at RELEASE time, which is the worst moment to learn | |
| # your app icon is blank. That is not hypothetical: a double hyphen in | |
| # the SVG's leading comment is illegal XML, and Quick Look answers by | |
| # rendering nothing, writing a PNG anyway, and exiting 0. The bundle | |
| # shipped an empty icon and every check downstream agreed with it. | |
| # | |
| # Renders to a temp path and asserts the raster is not blank; it does | |
| # not touch the repo. | |
| - name: App icon renders (not blank) | |
| if: runner.os == 'macOS' | |
| run: scripts/make-icon.sh "$RUNNER_TEMP/ci-icon.icns" | |
| - name: Install Linux system dependencies | |
| if: runner.os == 'Linux' | |
| run: scripts/ci-install-linux-deps.sh | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| # One shared cache per OS for check+test; the build matrix below | |
| # keys separately per target. | |
| shared-key: check | |
| - run: scripts/ci-check.sh | |
| shell: bash | |
| - name: Attribute failure | |
| if: failure() | |
| run: >- | |
| echo "::error title=check failed on ${{ matrix.os }}::scripts/ci-check.sh (fmt/clippy/test) failed on ${{ matrix.os }}. Search the step log above for the first 'error' line." | |
| # MSRV is validated with its own toolchain and WITHOUT --locked semantics | |
| # being violated: same lockfile, older compiler. If this fails, either fix | |
| # the code or bump the documented MSRV in a deliberate PR. | |
| # | |
| # MSRV is 1.86: the icu_* crates (transitive via idna/url) declare | |
| # rust-version 1.86 and refuse to build on 1.85. Full rationale in | |
| # docs/build-and-release.md#msrv-policy. Keep this version in sync with | |
| # that doc and the rust-toolchain.toml comment. | |
| msrv: | |
| runs-on: ubuntu-24.04 | |
| env: | |
| MSRV: "1.86.0" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Linux system dependencies | |
| run: scripts/ci-install-linux-deps.sh | |
| - name: Install MSRV toolchain | |
| run: rustup toolchain install "$MSRV" --profile minimal | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: msrv | |
| - name: Build with MSRV | |
| run: cargo +"$MSRV" build --workspace --locked | |
| - name: Attribute failure | |
| if: failure() | |
| run: >- | |
| echo "::error title=MSRV build failed::Workspace does not build with Rust $MSRV. Either fix the code or bump the documented MSRV (docs/build-and-release.md#msrv-policy) in a deliberate PR." | |
| # Licence + CVE gate. Runs on PRs (catches a bad dep at review time) and | |
| # is additionally scheduled nightly by audit-nightly below, because new | |
| # CVEs arrive without any commit happening. | |
| compliance: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Linux system dependencies | |
| run: scripts/ci-install-linux-deps.sh | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: compliance | |
| # cargo-install'd tools live in ~/.cargo/bin and are cached too. | |
| cache-all-crates: "true" | |
| - run: scripts/ci-compliance.sh | |
| shell: bash | |
| - name: Upload SBOM | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sbom | |
| path: dist/sbom/ | |
| # Cross-target build check: every triple we ship must at least COMPILE on | |
| # every PR. Cheap (check-only where possible, no packaging) and catches | |
| # cfg() breakage the day it is written, not at release tag time. | |
| build-matrix: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-15 | |
| target: aarch64-apple-darwin | |
| - os: macos-15 | |
| target: x86_64-apple-darwin | |
| - os: windows-2025 | |
| target: x86_64-pc-windows-msvc | |
| - os: windows-2025 | |
| # aarch64 Windows cross-compiles from the x86_64 runner; MSVC | |
| # ships the cross linker so no extra toolchain is required. | |
| target: aarch64-pc-windows-msvc | |
| - os: ubuntu-24.04 | |
| target: x86_64-unknown-linux-gnu | |
| - os: ubuntu-24.04 | |
| target: x86_64-unknown-linux-musl | |
| - os: ubuntu-24.04 | |
| target: aarch64-unknown-linux-gnu | |
| use-cross: true | |
| - os: ubuntu-24.04 | |
| target: aarch64-unknown-linux-musl | |
| use-cross: true | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| # Per-target cache key: artifacts for different triples never | |
| # share object files, so sharing a cache only causes eviction. | |
| key: ${{ matrix.target }} | |
| - name: Add target | |
| if: ${{ !matrix.use-cross }} | |
| run: rustup target add ${{ matrix.target }} | |
| - name: Install Linux system dependencies | |
| if: runner.os == 'Linux' && !matrix.use-cross | |
| run: scripts/ci-install-linux-deps.sh | |
| - name: Install musl tools | |
| if: contains(matrix.target, 'musl') && !matrix.use-cross | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| # Prebuilt binary instead of `cargo install cross`: same pinned version, | |
| # seconds instead of ~2 minutes of compiling cross from source per run. | |
| - name: Install cross | |
| if: matrix.use-cross | |
| uses: taiki-e/install-action@v2 | |
| with: | |
| tool: cross@0.2.5 | |
| - name: Build (${{ matrix.target }}) | |
| shell: bash | |
| run: | | |
| FLAGS=(--workspace --locked --target ${{ matrix.target }}) | |
| # musl targets build the shape we actually ship for them: the | |
| # static headless daemon (docs/build-and-release.md#matrix). The | |
| # default feature set pulls cpal -> alsa-sys, and there is no musl | |
| # ALSA on the runner or in the cross image, so a default-features | |
| # musl build can only ever fail in alsa-sys's pkg-config probe. | |
| # This mirrors scripts/build-headless.sh, not a weakening: every | |
| # crate still compiles for the triple, minus the audio backend | |
| # that a static server binary must not link anyway. | |
| case "${{ matrix.target }}" in | |
| *musl*) FLAGS+=(--no-default-features) ;; | |
| esac | |
| if [ "${{ matrix.use-cross }}" = "true" ]; then | |
| # alsa-sys uses the pkg-config crate, which refuses to run when | |
| # host != target unless explicitly allowed. The arm64 .pc files | |
| # are installed by the Cross.toml pre-build hook. | |
| export PKG_CONFIG_ALLOW_CROSS=1 | |
| export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig | |
| cross build "${FLAGS[@]}" | |
| else | |
| cargo build "${FLAGS[@]}" | |
| fi | |
| - name: Attribute failure | |
| if: failure() | |
| shell: bash | |
| run: >- | |
| echo "::error title=build failed for ${{ matrix.target }}::Workspace does not compile for ${{ matrix.target }} on ${{ matrix.os }} (cross: ${{ matrix.use-cross || 'false' }}). The first 'error' line in the Build step above names the crate at fault." | |
| # Windows runtime smoke test: the build-matrix job above proves the | |
| # Direct2D overlay renderer COMPILES AND LINKS on Windows, but link | |
| # success says nothing about whether D2D1CreateFactory, BindDC, or | |
| # UpdateLayeredWindow actually succeed at runtime — that is exactly the | |
| # class of bug this project has hit before (see docs/plans/ | |
| # windows-overlay.md's own "cannot verify without hardware" caveats). | |
| # This runs the real overlay-demo binary's Windows path | |
| # (crates/overlay/src/bin/overlay-demo.rs's `windemo::run()`) on a real | |
| # windows-2025 GitHub-hosted runner, which is CI infrastructure, not the | |
| # user's machine, so it does not touch anything this task was told to | |
| # stay away from. It cycles every OverlayState (including the Direct2D | |
| # skull paint path added by this change) for a few seconds and asserts a | |
| # clean exit: any HRESULT failure inside WinOverlay::new()/render() | |
| # (factory creation, DC render target creation, BindDC, geometry/brush | |
| # creation, UpdateLayeredWindow) surfaces as `anyhow::Result` propagating | |
| # up through `?` and returning a non-zero exit code, which this step | |
| # catches. What this still cannot prove: whether the pixels it pushed to | |
| # the layered window are visually correct (right colors, right alpha, | |
| # right position) — that needs a screenshot and a human, which is the | |
| # one gap this job does not close. | |
| windows-overlay-runtime-smoke: | |
| runs-on: windows-2025 | |
| # GHA-native timeout instead of the `timeout` coreutil: git-bash on the | |
| # Windows runner is not guaranteed to ship it, and a job-level timeout | |
| # is the same backstop without depending on one. | |
| timeout-minutes: 3 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Parse every PowerShell script. Nothing else in CI reads them, and | |
| # macOS has no pwsh, so a syntax error in a .ps1 was previously found | |
| # by a human running it on the Windows box: the slowest possible | |
| # feedback, on the machine least convenient to debug on. | |
| # | |
| # First step in the job, and before the toolchain cache, because it | |
| # needs neither and takes about a second. | |
| # | |
| # Parsing ONLY. These scripts press keys and type into windows, so a | |
| # CI runner must never execute them. `Parser::ParseFile` gives the | |
| # syntax check without running a single statement. | |
| - name: Parse the PowerShell verification scripts | |
| shell: pwsh | |
| run: | | |
| $bad = 0 | |
| Get-ChildItem -Path scripts -Filter *.ps1 -Recurse | ForEach-Object { | |
| $errs = $null | |
| [void][System.Management.Automation.Language.Parser]::ParseFile( | |
| $_.FullName, [ref]$null, [ref]$errs) | |
| if ($errs -and $errs.Count -gt 0) { | |
| $bad = 1 | |
| foreach ($e in $errs) { | |
| $line = $e.Extent.StartLineNumber | |
| Write-Output "::error file=$($_.FullName),line=$line::$($e.Message)" | |
| } | |
| } else { | |
| Write-Output " ok $($_.Name)" | |
| } | |
| } | |
| exit $bad | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: windows-overlay-smoke | |
| - name: Run overlay-demo (exercises WinOverlay::new/render for real) | |
| id: overlay_smoke | |
| shell: bash | |
| run: | | |
| # Exits 0 on its own after cycling 8 states x2 at 2.5s each | |
| # (~40s); the job-level timeout-minutes above is the backstop | |
| # against a real hang (e.g. a message-pump deadlock), not the | |
| # expected path. | |
| cargo run -p overlay --bin overlay-demo --features display | |
| # Only attribute a failure to the overlay if the overlay step is what | |
| # failed. This job now has two independent steps, and a blanket | |
| # `if: failure()` blamed the Direct2D renderer for a PowerShell syntax | |
| # error -- a confident, specific, wrong diagnosis, which is worse than | |
| # none because it sends the reader to the wrong file. | |
| - name: Attribute failure | |
| if: failure() && steps.overlay_smoke.outcome == 'failure' | |
| shell: bash | |
| run: >- | |
| echo "::error title=windows overlay runtime smoke failed::overlay-demo's Windows path panicked, returned an error, or hung on a real windows-2025 runner. This means the Direct2D renderer built cleanly but failed at runtime (factory/render-target/BindDC/UpdateLayeredWindow) -- check the step output above for the HRESULT or panic message." | |
| # Headless build: proves the daemon path stays display-free. Runs on a | |
| # plain ubuntu container with no X/Wayland present, which IS the deployment | |
| # environment being claimed. | |
| headless: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: headless | |
| - name: Install musl tools | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - name: Build static headless daemon | |
| run: | | |
| rustup target add x86_64-unknown-linux-musl | |
| scripts/build-headless.sh x86_64-unknown-linux-musl | |
| - name: Smoke test with no display server | |
| # Explicitly unset every display variable; if the binary needed a | |
| # display to even start, this is where it dies. | |
| run: | | |
| env -u DISPLAY -u WAYLAND_DISPLAY dist/headless/outloud-spiked-x86_64-unknown-linux-musl dry-run "change hello to goodbye" | |
| # The whisper backend, against a real model. Every other job builds the | |
| # workspace with default features, where whisper is compiled out entirely, | |
| # and the transcription test skips when no weights are present. So without | |
| # this job a broken whisper backend passes CI in two different ways at | |
| # once: not compiled, and not asserted. | |
| # | |
| # macOS because whisper-rs enables Metal there automatically; a Linux | |
| # runner would run the same test 20-30x slower on CPU for no extra signal. | |
| # Kept out of ci-check.sh because it is the expensive job: whisper.cpp is | |
| # compiled from source and the model is a 142MiB download, cached here | |
| # between runs and verified against the registry pin every time. | |
| whisper: | |
| runs-on: macos-15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: whisper | |
| # Keyed by the pinned hash, not by a date: a re-pin must miss the cache | |
| # and fetch the new file, and an unchanged pin must never re-download. | |
| - name: Cache the ggml model | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.outloud/models/whisper-base.en | |
| key: whisper-base-en-a03779c86df3323075f5e796cb2ce5029f00ec8869eee3fdfb897afe36c6d002 | |
| - run: scripts/ci-whisper.sh | |
| shell: bash | |
| - name: Attribute failure | |
| if: failure() | |
| run: >- | |
| echo "::error title=whisper backend::scripts/ci-whisper.sh failed. Either the backend broke, or the pinned model hash no longer matches what the upstream URL serves (which is the check working, not a flake)." | |
| # Performance tripwire. Every other job answers "is it correct"; this one | |
| # answers "is it still fast", which no correctness test can see: a change | |
| # that makes the overlay ten times slower produces exactly the same text | |
| # and passes every assertion in the workspace. | |
| # | |
| # It measures only PURE paths (the rolling-window animation, hypothesis | |
| # ingest, command parsing) precisely so it can run here. The richer gate in | |
| # crates/ax-edit/benches/gate.rs measures the real accessibility path and | |
| # correctly skips without a focused text field, which means on a CI runner | |
| # it protects nothing. This one cannot skip. | |
| # | |
| # Budgets carry ~10x headroom over measured cost, so a shared runner being | |
| # slow does not fail the build. See the bench's own comment for numbers. | |
| perf: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: perf | |
| - name: Perf gate | |
| run: cargo bench -p overlay --bench perf_gate | |
| - name: Attribute failure | |
| if: failure() | |
| run: | | |
| echo "::error title=perf gate::A hot path got materially slower. This is not a correctness failure: the app still produces the right text, only later. See crates/overlay/benches/perf_gate.rs for the budgets and the reasoning." | |
| # Reproducibility tripwire: double-build on Linux and require identical | |
| # hashes. Only x86_64-linux (the cheapest runner) because determinism bugs | |
| # are almost never target-specific; the flags are shared by all targets. | |
| repro: | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # build-repro.sh derives SOURCE_DATE_EPOCH from git history. | |
| fetch-depth: 1 | |
| - name: Install Linux system dependencies | |
| run: scripts/ci-install-linux-deps.sh | |
| # NOTE: no rust-cache here on purpose. A cache-restored target dir | |
| # defeats the point of proving a clean double-build is deterministic. | |
| - name: Double build and compare hashes | |
| run: REPRO_VERIFY=1 scripts/build-repro.sh | |
| - name: Attribute failure | |
| if: failure() | |
| run: >- | |
| echo "::error title=repro job failed::Either the workspace failed to compile on x86_64 Linux (see the first 'error' line above) or the double build produced different hashes; the script prints both hashes and suggests diffoscope." | |
| # The installer is the first thing a stranger runs and, until now, the only | |
| # public interface with no check at all. The previous one lived solely on a | |
| # feature branch, so the published one-liner would 404 the moment that | |
| # branch was deleted, and nothing in CI could have noticed. | |
| install: | |
| runs-on: macos-15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install from the live release into a temp directory | |
| run: scripts/test-install.sh | |
| - name: Attribute failure | |
| if: failure() | |
| run: >- | |
| echo "::error title=install job failed::scripts/install.sh could not install the published release, or the app it installed was not the outloud daemon with its speech helper. Run scripts/test-install.sh locally; it installs into a temp dir and never touches /Applications." |