Skip to content

feat(macos): add Intel (x86_64) build with separate DMG#8

Draft
jguice wants to merge 3 commits into
mainfrom
feat/macos-universal-binary
Draft

feat(macos): add Intel (x86_64) build with separate DMG#8
jguice wants to merge 3 commits into
mainfrom
feat/macos-universal-binary

Conversation

@jguice
Copy link
Copy Markdown
Owner

@jguice jguice commented Apr 26, 2026

Summary

  • Adds a separate macos-intel-build job that produces utter-VERSION-macos-x86_64.dmg for Intel Macs.
  • Silicon path is bit-for-bit unchanged — same macos-14 runner, same prebuilt ONNX Runtime download, same arm64 DMG. The new job runs in parallel and the release glob (dist/utter-*-macos-*.dmg) already picks up both.
  • README adds a brief ### macOS (Intel) section after Apple Silicon; existing silicon section + heading + anchor untouched.

Why this shape (vs. universal binary, vs. load-dynamic)

Pyke ort 2.0.0-rc.12 doesn't ship prebuilt ONNX Runtime for x86_64-apple-darwin. We considered:

  1. Universal binary (one DMG, both arches lipo'd) — would have inflated the silicon DMG ~3× because the universal binary carries both static-linked slices. Penalizes silicon users (the majority) for Intel support.
  2. ort load-dynamic + bundled universal2 dylib — symmetric across arches but inflates the silicon DMG from ~9.5 MB to ~70-80 MB.
  3. From-source ONNX Runtime + separate Intel DMGthis PR. Silicon DMG unchanged; Intel users download a separate ~10-15 MB DMG. Confines all build-system complexity to the Intel job.

How it works

  • New macos-intel-build job runs on macos-14-large (native Intel).
  • Clones microsoft/onnxruntime at v1.24.2 (matches what pykeio/ort's custom-static-link.yml validates against), runs their build.sh with --skip_tests to trim ~30% off cold-build time. Output is the multi-file static .a set ort-sys's static_link/mod.rs knows how to consume.
  • Caches the build dir keyed on ONNXRUNTIME_VERSION — heavy build only re-runs when we bump the pin.
  • cargo build --release --target x86_64-apple-darwin with ORT_LIB_PATH pointing at the build dir → ort-sys static-links the local libs instead of trying (and failing) to download a prebuilt.
  • Bundle / sign / notarize / DMG / staple — same machinery as silicon, just with the x86_64 binary and the -macos-x86_64.dmg name.

Local validation

End-to-end on an Intel Mac:

  • ✅ ONNX Runtime v1.24.2 builds cleanly from source (~30 min cold)
  • ✅ ort-sys's static-link probes find what onnxruntime's CMake produced — no patches needed
  • cargo build produces a 21 MB x86_64 Mach-O
  • make-bundle.sh + Apple Development cert → stable-signed .app
  • ✅ Launches, all 3 TCC prompts surface and grants persist
  • ✅ Model downloaded, PTT dictation transcribes successfully

Caveats / known unknowns

  • CI cross-runner equivalence not yet verified. Local POC was a native Intel Mac with brew-installed cmake; CI uses macos-14-large which is also native Intel but reportedly slow. First CI release run is the real test.
  • Cold build time on the slow Intel runner could be 60-90 min on first run; cached runs are instant. --skip_tests mitigates.
  • Future runner deprecation: macos-14-large will sunset alongside macos-14 per actions/runner-images#13518. Both macOS jobs will need to migrate to macos-15-intel / macos-15 together — kept aligned here intentionally.

Test plan

  • Trigger gh workflow run release.yml -f tag=v0.3.0 --ref feat/macos-universal-binary and confirm:
    • Silicon job (macos-build) succeeds with no behavior change
    • Intel job (macos-intel-build) succeeds: ONNX Runtime build → cargo build → bundle → notarize → DMG → notarize DMG
    • dist/ contains both utter-0.3.0-macos-arm64.dmg and utter-0.3.0-macos-x86_64.dmg
  • Install Intel DMG on an Intel Mac (macOS 13+) → confirm launch, permissions, dictation
  • Install arm64 DMG on Apple Silicon → confirm no regression

🤖 Generated with Claude Code

@jguice jguice marked this pull request as draft April 26, 2026 17:56
Pyke ort 2.0.0-rc.12 doesn't ship prebuilt ONNX Runtime binaries for
x86_64-apple-darwin (their prebuilts cover arm64-darwin, linux, and
windows only). To produce an Intel utter binary, we build ONNX Runtime
v1.24.2 from source on a native Intel runner (macos-14-large), then
point ort-sys at it via ORT_LIB_PATH so the multi-file static link
path takes over. The pin matches what pyke validates against in
their custom-static-link.yml.

Silicon path is bit-for-bit unchanged: same macos-14 runner, same
prebuilt-onnxruntime download, same DMG. The new Intel job runs in
parallel and produces a separate utter-VERSION-macos-x86_64.dmg.
The release job's existing dist/utter-*-macos-*.dmg glob picks up
both.

The from-source ONNX Runtime build is the slow step (~30 min cold).
Cached by ONNXRUNTIME_VERSION so it only re-runs when we bump the
pin. --skip_tests trims ~30% off the cold build time since we only
need the .a libs for static linking, not the test executables.

Validated end-to-end on a local Intel Mac: built, signed, launched,
permissions granted, model downloaded, PTT transcription works.
The CI cross-runner equivalence is on macos-14-large (slow but
native — see actions/runner-images#12545).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jguice jguice force-pushed the feat/macos-universal-binary branch from e095df9 to 9b871fa Compare April 26, 2026 19:07
@jguice jguice changed the title feat(macos): build universal binary (Intel + Apple Silicon) feat(macos): add Intel (x86_64) build with separate DMG Apr 26, 2026
jguice and others added 2 commits April 26, 2026 12:23
Adds two workflows that build and publish a per-branch GitHub
Pre-Release on every PR push, replacing the prior prerelease in
place so the Releases page never accumulates more than one
prerelease per branch.

prerelease.yml:
- Triggers on PR opened/synchronize/reopened + workflow_dispatch
  (so branches without a PR can still get a manual build).
- Concurrency group keyed on branch with cancel-in-progress, so
  burst pushes only build the latest commit.
- Computes a semver-compliant prerelease version of the form
  <cargo-version>-<sanitized-branch>.<short-sha>, e.g.
  0.3.0-feat-macos-universal-binary.abc1234. Branch name is
  lowercased and non-[a-z0-9.-] chars collapsed to dashes.
- Builds the same matrix as release.yml: Linux amd64+arm64
  (.deb/.rpm/.tar.gz), macOS arm64 DMG, macOS x86_64 DMG (with
  the from-source ONNX Runtime cache shared with release.yml's
  cache key).
- Publishes via softprops/action-gh-release with
  prerelease: true and make_latest: false, so it doesn't
  appear as "Latest" on the Releases page or in the
  /releases/latest API endpoint (which install-release.sh
  follows — Linux users won't accidentally pull a prerelease).
- Deletes the existing prerelease + tag (if any) before
  publishing the new one. Tag is constant per branch
  (prerelease-<sanitized-branch>), version inside the
  filename changes per build.

prerelease-cleanup.yml:
- On PR closed (merged or not), deletes the per-branch
  prerelease + tag. Same sanitization logic as prerelease.yml
  (must stay in lockstep).

Fork PRs from external contributors will fail at the codesign
step because GitHub doesn't expose secrets to fork PRs — by
design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macos-14-large is a Team/Enterprise-only larger runner — jobs targeting
it on a Pro account fail instantly with no runner assignment. Switch
both the release and prerelease workflows to macos-15-intel, which is
a standard 4-core Intel runner: free + unlimited on public repos, no
plan or billing setup needed.

The *-intel suffix denotes a standard Intel runner; the *-large suffix
denotes a 12-core larger runner. Easy to confuse — the runner-images
README lists both labels in the same row even though they're billed
differently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant