Skip to content

feat(desktop): build and ship a macOS Intel (x64) installer - #574

Merged
Weegy merged 4 commits into
mainfrom
feat/mac-intel-build
Jul 31, 2026
Merged

feat(desktop): build and ship a macOS Intel (x64) installer#574
Weegy merged 4 commits into
mainfrom
feat/mac-intel-build

Conversation

@Weegy

@Weegy Weegy commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Problem

macOS ships arm64 only. The stated reason in electron-builder.yml"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. 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.yml listing 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 MacUpdater selects the download by testing the file URL:

const isArm64 = (file) => file.url.pathname.includes("arm64") || file.info.url?.includes("arm64");

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-feed job merges both — and refuses to publish a feed covering only one architecture.

Changes

File Change
.github/workflows/desktop-apps.yml macos-26-intel matrix entry (--mac --x64)
.github/workflows/desktop-apps.yml pgvector step derives the architecture instead of hardcoding darwin-arm64, and asserts the staged vector.dylib really is that architecture
.github/workflows/desktop-apps.yml mac jobs hold back latest-mac.yml; new mac-update-feed job merges and uploads it
desktop/scripts/merge-mac-update-feed.mjs New. Dependency-free strict parser/merger — the job does not npm ci
desktop/scripts/merge-mac-update-feed.test.mjs New. 12 tests
desktop/electron-builder.yml arch no longer pinned; artifact names always carry it
desktop/README.md Documents the two-architecture setup and its consequences

No release asset is renamed. Existing arm64 filenames stay byte-identical (omadia-<v>-arm64.dmg, omadia-<v>-arm64-mac.zip); x64 gets the matching -x64 names. electron-builder omits the arch for x64 by default, which would have dropped a nondescript omadia-<v>.dmg next to the arm64 one — hence the explicit artifactName.

The architecture assertion on vector.dylib is deliberate: a mismatch there would not fail the build, it would fail at CREATE EXTENSION inside the shipped app, on a user's machine.

Verification

  • Merge tested against the real latest-mac.yml from v0.57.1 — round-trips byte-identically
  • 12 unit tests (npm test in desktop/) covering the merge plus every guard: single-architecture result, version mismatch, unknown top-level key, unparseable line, missing field, empty feed, deduplication
  • Upload-filter logic simulated: latest-mac.yml removed, all other artifacts retained
  • actionlint clean apart from the pre-existing SC2086
  • Replaced a ${{ matrix.os }} interpolation inside a run block with RUNNER_OS (fixes SC2193, 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 pgvector yields a PG17 x86_64 vector.dylib, and whether @embedded-postgres/darwin-x64 installs cleanly. Those are the two places where an Intel-specific surprise would show up.

Test plan

  • Merge verified against real release output + unit tests
  • Dispatch on this branch; macos-26-intel job goes green
  • x64 DMG signed + notarized, spctl --assess → accepted
  • Merged latest-mac.yml lists all four artifacts
  • Smoke-test the x64 app on an Intel Mac (or under Rosetta) — the embedded Postgres + pgvector path is the risk

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Weegy added 4 commits July 31, 2026 15:58
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.
@Weegy
Weegy merged commit fa0e48b into main Jul 31, 2026
12 checks passed
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