Skip to content

fix(macos): guard the DMG architecture, name installers Intel / Apple-Silicon, and stop the startup Keychain prompt - #225

Merged
EtienneLescot merged 4 commits into
release/v1.8.0from
fix/macos-dmg-arch-guard
Aug 1, 2026
Merged

fix(macos): guard the DMG architecture, name installers Intel / Apple-Silicon, and stop the startup Keychain prompt#225
EtienneLescot merged 4 commits into
release/v1.8.0from
fix/macos-dmg-arch-guard

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Four fixes found while investigating why the packaged macOS build was unusably slow on an Apple Silicon Mac. The first two are the direct cause and its guard; the last two are things the investigation surfaced.

1. The arm64 job could package the x64 bundle

npx electron-builder --mac --${{ matrix.arch }} --dir does not restrict the architecture — the arch list in electron-builder.json5's mac.target names both x64 and arm64, and the config wins over the CLI flag. Every job therefore produces both bundles (release/<ver>/mac/ for x64, release/<ver>/mac-arm64/), and the step that picked one did:

find "release/${VERSION}" -maxdepth 4 -name "*.app" | head -n1

head -n1 takes whichever directory comes first in readdir order, and electron-builder walks the target list in declaration order, so mac/ (x64) is created first and wins. The arm64 job could hand the x64 bundle to hdiutil and publish it as Openscreen-Mac-arm64-<ver>.dmg.

v1.8.0-rc.5 happens to be correct — I verified its published arm64 DMG is genuinely arm64 (lipo -archsarm64, helpers in darwin-arm64, Mach-O thin (arm64)). So this is a latent defect, not an incident.

Fixed by selecting the arch directory explicitly, and guarded by a lipo -archs assertion that fails the job when the bundle does not match matrix.arch. The guard is the part that matters: selection can drift again, an assertion cannot pass while wrong.

2. The installers are named after the instruction set, not the machine

x64 reads to most people as "the normal 64-bit one" and arm64 as the exotic variant, which is backwards on every Mac sold since 2020. This is not cosmetic: an Intel bundle on Apple Silicon runs the compositor, the encoder and whisper.cpp under Rosetta 2, which is slow enough to make the app unusable — this PR exists because that happened to a maintainer, who then had to be told which of two similarly-named files to pick.

Openscreen-macOS-Apple-Silicon-<ver>.dmg
Openscreen-macOS-Intel-<ver>.dmg

3. Every launch prompted for Keychain access

LlmConfigStore's constructor does two sync readFileSync plus a safeStorage decrypt, and on macOS safeStorage is backed by a Keychain item. Building it inside registerIpcHandlers meant every launch reached the Keychain — including for the majority of users who never open the AI layer and have no credentials stored at all.

The store is now built on first use and memoised. Making the handlers-side binding lazy was not enough on its own: registerNativeBridgeHandlers resolved it eagerly with llmConfig: context.getAiEditionLlmConfig() while wiring the service, so the laziness is carried through to AiEditionService, whose option is now a factory rather than an instance. All thirteen of its uses already sat behind a method the renderer must call first, so nothing else moved. The single-instance guarantee the old comment argued for is preserved by the memoisation.

This does not fix the prompt repeating, only its appearing at startup. The item's ACL binds to a code signature, and an unsigned or ad-hoc-signed build has no stable identity for it to trust. Signing is the other half of that, and is out of scope here.

4. The roadmap described a Windows-only engine

ROADMAP.md still called the compositor "Direct3D 11", listed MP4 export on macOS and Linux as unstarted, and framed "porting it off Windows" as the biggest open item — while rc.5 was already publishing DMGs and Linux packages built on the Metal and WGSL backends. #18 shipped with them, as an automatically-selected CPU backend rather than the encoder flag the entry anticipated.

Marked shipped, and replaced with the two gaps that are actually open: Linux export is software-encoded, and every performance number on record comes from one passive-iGPU laptop.

Also in this area — rendering-performance.md's headline table reads as a before/after and is not one. Its WebCodecs in Chromium | 79 | removed — was the previous export path row is the web pipeline after the Canvas2D rebuild, an intermediate state of the 1.8.0 cycle that no release ever contained. What users upgrade from is v1.7.0, the webcodecs-legacy arm at ~8 fps. src/lib/exporter/frameRenderer.ts settles it: no shadowCache at all in v1.7.0, carried throughout by v1.8.0-rc.4, so the ~2× rebuild landed inside the 1.8.0 window. The real delta is an order of magnitude, not 1.6×. This was not hypothetical — the rc.4 release notes were drafted with the 1.6× figure straight off this table before the tree was checked.

Related issue

Refs #18 — the software-encoder fallback it asks for is marked shipped here.

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

The DMG filenames change. publish-release uploads via find artifacts -type f, so nothing there is affected. update-homebrew-cask.yml's "Find macOS DMG assets" step already matched apple[-_. ]?silicon and intel, but its "Wait for release DMG assets" step compared exact filenames — after the rename it would have polled for its full 12-minute timeout and warned, on a release whose assets were present the whole time. Both steps now match on the same patterns and cannot disagree. Old -Mac-arm64- / -Mac-x64- names still match, so past releases keep working.

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

n/a — CI, main-process wiring and docs.

Testing

npx tsc --noEmit clean. vitest --run: 1466 passed, 6 failed — all six in electron/recording/webm-seek-index.test.ts, which this branch does not touch and which fails identically on origin/release/v1.8.0 (verified by checking out the base and re-running that file alone). biome check clean on the four changed TypeScript files.

Laziness is covered by a new test, aiEditionService.lazyLlmConfig.test.ts, because the failure mode is invisible: reintroducing the eager form breaks no behaviour, passes every other test, and shows up only as a Keychain prompt on a machine the author may not have. It asserts the factory is not called at construction, not called by non-LLM work, and called exactly once on first LLM use.

No macOS runner available locally, so the workflow logic was exercised directly rather than by running it:

  • YAML parsesruby -ryaml -e 'YAML.load_file(...)' on both workflows.
  • Shell syntax — the four modified run: blocks extracted and passed through bash -n.
  • Naming — the case produces Openscreen-macOS-Apple-Silicon-1.8.0.dmg and Openscreen-macOS-Intel-1.8.0.dmg.
  • Homebrew matching — both new names are matched by the cask workflow's existing patterns, and so are the old -Mac-arm64- / -Mac-x64- names.
  • The arch guard — run against real bundles: an arm64 bundle passes an arm64 job and is refused by an x64 job; a universal x86_64 arm64 bundle passes an arm64 job.
  • Docsnpm run docs:check → OK (22 files); the new anchors and the new ROADMAP.md link resolve.

The first macOS build after merge is the real test of the guard — and if it fails a job, it will have done its work.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aab90679-a77a-4985-ba9c-15883a807bcc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot force-pushed the fix/macos-dmg-arch-guard branch 2 times, most recently from 92279b6 to 3fdfe43 Compare August 1, 2026 13:52
…achine

`npx electron-builder --mac --${matrix.arch} --dir` does not restrict the
architecture: the `arch` list in electron-builder.json5's `mac.target` names
both x64 and arm64, and the config wins over the CLI flag. Every job therefore
produces both bundles — x64 in release/<ver>/mac/, arm64 in
release/<ver>/mac-arm64/ — and the step that picked one did

    find "release/${VERSION}" -maxdepth 4 -name "*.app" | head -n1

which takes whichever directory comes first in readdir order. electron-builder
walks the target list in the order it is declared, so `mac/` (x64) is created
first and wins the race. The arm64 job could hand the x64 bundle to `hdiutil`
and publish it as `Openscreen-Mac-arm64-<ver>.dmg`. Nothing downstream ever
compared the name against the contents, so it would have shipped silently.

v1.8.0-rc.5 happens to be correct — its arm64 DMG really is arm64 — so this is
a latent defect, not an incident. Fixed by selecting the arch directory
explicitly, and guarded by a `lipo -archs` assertion that fails the job when
the bundle does not match `matrix.arch`. The guard is the part that matters:
selection can drift again, an assertion cannot pass while wrong.

The rename is the same defect seen from the user's side. `x64` reads to most
people as "the normal 64-bit one" and `arm64` as the exotic variant, which is
backwards on every Mac sold since 2020 — and choosing wrong is not a cosmetic
mistake: an Intel bundle on Apple Silicon runs the compositor, the encoder and
whisper.cpp under Rosetta 2, which is slow enough to make the app unusable.
The DMGs are now named `Openscreen-macOS-Apple-Silicon-<ver>.dmg` and
`Openscreen-macOS-Intel-<ver>.dmg`, matching what About This Mac shows.

update-homebrew-cask.yml needed one change to follow. Its "Find macOS DMG
assets" step already matched `apple[-_. ]?silicon` and `intel`, but the
"Wait for release DMG assets" step ahead of it compared exact filenames, so
after the rename it would have polled for its full 12-minute timeout and
warned, on a release whose assets were present the whole time. Both steps now
match on the same patterns and cannot disagree; old `-Mac-arm64-` /
`-Mac-x64-` names still match, so past releases keep working.
The headline table reads as a before/after and is not one. Its
`WebCodecs in Chromium | 79 | removed — was the previous export path` row is
the web pipeline *after* the Canvas2D compositor rebuild — an intermediate
state of the 1.8.0 cycle that no release ever contained. "The previous export
path" invites the reader to compare 126 against it and conclude ~1.6x.

What users actually upgrade from is v1.7.0, which is the `webcodecs-legacy`
arm: ~8 fps at M1, 9.8 fps under Gate G0. The tree settles it —
`src/lib/exporter/frameRenderer.ts` has no `shadowCache` in v1.7.0 and carries
it throughout by v1.8.0-rc.4, so the ~2x rebuild landed inside the 1.8.0
window, not before it. 1.8.0 therefore compounds two changes against what a
user had, and the real delta is an order of magnitude rather than 1.6x.

This was not hypothetical: the v1.8.0-rc.4 release notes were drafted with the
1.6x figure straight off this table before the tree was checked. The note also
says not to quote a precise multiple across these runs, because the same arm on
this machine has measured 44.0, 36.8, 32.3, 31.8, 22.2 and 11.9 fps across
sessions — the magnitude survives that drift, an exact ratio does not.
@EtienneLescot
EtienneLescot force-pushed the fix/macos-dmg-arch-guard branch from 3fdfe43 to 3bcfa2b Compare August 1, 2026 13:59
`LlmConfigStore`'s constructor does two sync readFileSync plus a `safeStorage`
decrypt. On macOS `safeStorage` is backed by a Keychain item, so building the
store during `registerIpcHandlers` meant every single launch reached the
Keychain — including for the majority of users who never open the AI layer and
have no credentials stored at all.

The store is now built on first use and memoised. Making the handlers-side
binding lazy was not enough on its own: `registerNativeBridgeHandlers` resolved
it eagerly with `llmConfig: context.getAiEditionLlmConfig()` while wiring the
service, so the laziness had to be carried through to `AiEditionService`, whose
option is now a factory rather than an instance. Every one of its thirteen uses
already sat behind a method the renderer has to call first, so nothing else
moved. The service memoises too — `llmGetSnapshot` alone reads the store once
per provider definition, which would otherwise have called the factory nine
times per snapshot.

The single-instance guarantee the old comment argued for is preserved: the
memoisation is what keeps a second store from racing the first, and the
DocumentService rationale next to it (a per-instance save queue whose duplicate
destroyed two real project files) is untouched and still eager.

This does not fix the prompt repeating, only its appearing at startup. The item's
ACL binds to a code signature, and an unsigned or ad-hoc-signed build has no
stable identity for it to trust, so the prompt returns on every launch that
reaches the store. Signing is the other half, and is not this function's
business.

Guarded by a test, because the failure mode is invisible: reintroducing the
eager form breaks no behaviour, passes every other test, and shows up only as a
Keychain prompt on a machine the author may not have.
It still called the compositor "Direct3D 11", listed MP4 export on macOS and
Linux as unstarted, and framed "porting it off Windows" as the biggest open
item — while v1.8.0-rc.5 was already publishing DMGs and Linux packages built
on the Metal and WGSL backends. #18 (software encoder fallback) shipped with
them too, as an automatically-selected CPU backend rather than the encoder flag
the entry anticipated.

Marked all three shipped, and replaced them with the two gaps that are actually
open: Linux export is software-encoded (the capture helper has a hardware H.264
encoder, the export pipeline does not), and every performance number on record
still comes from one passive-iGPU laptop, so nothing is measured on the hardware
most users have.
@EtienneLescot EtienneLescot changed the title ci(macos): guard the DMG architecture, and name the installers Intel / Apple-Silicon fix(macos): guard the DMG architecture, name installers Intel / Apple-Silicon, and stop the startup Keychain prompt Aug 1, 2026
@EtienneLescot
EtienneLescot merged commit 3f4ac46 into release/v1.8.0 Aug 1, 2026
13 checks passed
@EtienneLescot
EtienneLescot deleted the fix/macos-dmg-arch-guard branch August 1, 2026 14:13
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