feat(desktop): build and ship a macOS Intel (x64) installer - #574
Merged
Conversation
macOS shipped arm64 only. The stated reason — "GitHub's Intel (macos-13)
runners queue for hours" — is obsolete: macos-13 was retired in December 2025
and `macos-26-intel` is now a standard runner, so Intel builds natively rather
than needing cross-compilation. (GitHub drops x86_64 entirely in August 2027.)
Each architecture is built on a runner of its own architecture. That is forced,
not preferred: the omadia runtime ships as unpacked extraResources (native node
modules + the embedded Postgres engine), which electron-builder copies verbatim
and cannot arch-split, so an x64 bundle produced on an arm64 host would contain
arm64 binaries and crash on Intel.
Auto-update is the non-obvious part. Each run emits its own latest-mac.yml
listing only its own artifacts, and both upload to the same release with
--clobber, so whichever job finished last would become the only updatable
architecture. electron-updater's MacUpdater picks a download by testing the
file URL for "arm64":
const isArm64 = (file) => file.url.pathname.includes("arm64") || ...
so a single-architecture feed leaves Intel users with no matching file, or
pushes every Apple Silicon user onto the Rosetta build. The mac jobs now hold
that file back and a new mac-update-feed job merges both, refusing to publish a
feed that covers only one architecture.
Also:
- stage-runtime.mjs already follows process.arch and needed no change, but the
pgvector CI step hardcoded darwin-arm64. It now derives the architecture and
asserts the staged vector.dylib really is that architecture — a mismatch
would otherwise surface only at CREATE EXTENSION, inside the shipped app, on
a user's machine.
- electron-builder.yml no longer pins arch; artifact names always carry it.
Existing arm64 filenames are unchanged, so no release asset is renamed.
- Replaced a `${{ matrix.os }}` interpolation in a run block with RUNNER_OS
(actionlint SC2193, and avoids expression injection into a shell script).
Verified: merge tested against the real v0.57.1 latest-mac.yml (round-trips
byte-identically) plus 12 unit tests covering the merge and every guard —
single-architecture result, version mismatch, unknown key, unparseable line,
missing field, empty feed. actionlint clean apart from the pre-existing SC2086.
Not yet exercised on CI: the Intel runner itself. First dispatch is the
acceptance test.
The first Intel run was cancelled at 31 minutes, mid-'Package installers', by the 30-minute cap — and took the mac-update-feed job down with it, since that job correctly refuses to publish a feed covering only one architecture. Measured on that run: linux 5 min, windows 9 min, macOS arm64 21 min, macOS Intel >30. The Intel runner is materially slower, and notarization wait time is Apple's to decide. 60 keeps the runaway protection with real headroom. Everything Intel-specific worked: pgvector provisioning (x86_64 dylib from brew into the darwin-x64 engine) and the Apple signing setup both succeeded.
The Intel job failed in 'Package installers' with:
security set-key-partition-list ... : SecKeychainUnlock:
The user name or passphrase you entered is not correct.
Cause is in app-builder-lib itself. With CSC_LINK set, electron-builder
creates a temp keychain using a random password (macCodeSign.js createKeychain)
and then, in importCerts, runs:
security set-key-partition-list -S apple-tool:,apple: -s -k <password> <keychain>
where <password> is the *P12 passphrase*, not the keychain password it just
generated. That only succeeds while the keychain still happens to be unlocked
from the create step moments earlier — it is luck, and the Intel runner did not
get it. arm64 has been passing on that same luck.
macPackager.js only takes that path when CSC_LINK is present; otherwise it uses
CSC_KEYCHAIN + CSC_NAME. We already create a keychain, import the Developer ID
and set its partition list with the CORRECT password, so point electron-builder
at that one and drop CSC_LINK for macOS. One keychain instead of two, and the
buggy path is never entered.
Also hard-fails when the identity cannot be read back after import, rather than
silently continuing toward an unsigned build. The Windows legacy path still uses
CSC_LINK and is untouched.
My previous commit passed the full identity string, which electron-builder
rejects outright:
⨯ Please remove prefix "Developer ID Application:" from the specified name
— appropriate certificate will be chosen automatically
That broke the arm64 job too, which had been passing. electron-builder prepends
the certificate type itself, so CSC_NAME takes only the common name. afterPack
calls codesign directly and still receives the full identity via
MAC_SIGN_IDENTITY.
The keychain change itself was sound: Intel now reaches the same signing stage
as arm64 instead of dying in set-key-partition-list.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
macOS ships arm64 only. The stated reason in
electron-builder.yml— "GitHub's Intel (macos-13) runners queue for hours" — is obsolete:macos-13was retired in December 2025, andmacos-26-intelis now a standard runner. Intel therefore builds natively, and none of the cross-compilation the old comment proposed is needed.(Worth calendaring: GitHub drops x86_64 entirely in August 2027.)
Why one job per architecture
Not a preference — a constraint. The omadia runtime ships as unpacked
extraResources(native node modules plus the embedded Postgres engine), which electron-builder copies verbatim and cannot arch-split. An x64 bundle produced on an arm64 host would contain arm64 binaries and crash on Intel. A universal binary is off the table while the runtime ships this way.The non-obvious part: auto-update
Each mac run emits its own
latest-mac.ymllisting only its own artifacts, and both upload to the same release with--clobber— so whichever job finished last would silently become the only updatable architecture.That matters because
MacUpdaterselects the download by testing the file URL:A feed listing only arm64 leaves Intel users with no matching file; a feed listing only x64 pushes every Apple Silicon user onto Rosetta. So the mac jobs now hold that file back, and a new
mac-update-feedjob merges both — and refuses to publish a feed covering only one architecture.Changes
.github/workflows/desktop-apps.ymlmacos-26-intelmatrix entry (--mac --x64).github/workflows/desktop-apps.ymldarwin-arm64, and asserts the stagedvector.dylibreally is that architecture.github/workflows/desktop-apps.ymllatest-mac.yml; newmac-update-feedjob merges and uploads itdesktop/scripts/merge-mac-update-feed.mjsnpm cidesktop/scripts/merge-mac-update-feed.test.mjsdesktop/electron-builder.ymldesktop/README.mdNo release asset is renamed. Existing arm64 filenames stay byte-identical (
omadia-<v>-arm64.dmg,omadia-<v>-arm64-mac.zip); x64 gets the matching-x64names. electron-builder omits the arch for x64 by default, which would have dropped a nondescriptomadia-<v>.dmgnext to the arm64 one — hence the explicitartifactName.The architecture assertion on
vector.dylibis deliberate: a mismatch there would not fail the build, it would fail atCREATE EXTENSIONinside the shipped app, on a user's machine.Verification
latest-mac.ymlfrom v0.57.1 — round-trips byte-identicallynpm testindesktop/) covering the merge plus every guard: single-architecture result, version mismatch, unknown top-level key, unparseable line, missing field, empty feed, deduplicationlatest-mac.ymlremoved, all other artifacts retainedactionlintclean apart from the pre-existingSC2086${{ matrix.os }}interpolation inside arunblock withRUNNER_OS(fixesSC2193, and avoids interpolating an expression into a shell script)Not yet validated
The Intel runner itself has never been exercised. A dispatch on this branch is the acceptance test — specifically whether
brew install pgvectoryields a PG17 x86_64vector.dylib, and whether@embedded-postgres/darwin-x64installs cleanly. Those are the two places where an Intel-specific surprise would show up.Test plan
macos-26-inteljob goes greenspctl --assess→ acceptedlatest-mac.ymllists all four artifactsNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.