Skip to content

fix: ship the native addon inside the macOS .app - #607

Open
Ernxst wants to merge 1 commit into
mainfrom
fix/601-addon-in-app
Open

Ernxst wants to merge 1 commit into
mainfrom
fix/601-addon-in-app

Conversation

@Ernxst

@Ernxst Ernxst commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Harness: Claude Code
Agent: none
Model: anthropic/claude-opus-5

Task statements
  1. Compare this fork with a public benchmark of one Mac app built in 21 frameworks, and bring startup closer to plain GPUI. (Working-directory and workflow instructions omitted.)

  2. Settle where shipped apps keep the native addon so macOS stops rescanning it. (Working-directory and workflow instructions omitted.)

Closes #601.

The chat example's .app now ships the native addon in Contents/Frameworks instead of embedding it in the executable. An embedded addon is written to $TMPDIR/.bun-*.node and scanned by macOS on the first launch and again after every $TMPDIR purge (342 ms scan, 620–665 ms launches in #601). The bare bun build --compile --production binary still embeds it.

GPUIX Chat.app/Contents
├── MacOS/chat                                   bun build --compile --external '*.node'
├── Frameworks/gpuix-native.darwin-arm64.node    ad-hoc signed
├── Resources/AppIcon.icns
└── Info.plist

The executable is compiled from a generated entry that points napi-rs's loader at the bundled file, then loads the app. The README's "Wrap it in an app" section shows the same six lines for app authors:

import path from 'node:path'

const addon = path.join(path.dirname(process.execPath), '..', 'Frameworks', 'gpuix-native.darwin-arm64.node')
process.env.NAPI_RS_NATIVE_LIBRARY_PATH ??= addon

await import('./chat.tsx')

NAPI_RS_NATIVE_LIBRARY_PATH is the first path the generated packages/native/index.js loader checks. --external '*.node' stops Bun from embedding the addon at bundle time; without it the addon is embedded and extracted whatever the variable says. The app is resealed with codesign --force --deep --sign - after the addon is in place.

main This PR
.app executable 91 MB (addon embedded) 62 MB
.bun-*.node written to $TMPDIR on launch yes no

The app opens through open -W -n and renders. Notarisation is not tested: there is no Developer ID certificate available. The README's cargo-packager variant copies and signs the addon after cargo packager runs; that sequence was not run end to end because cargo-packager is not installed here.

Risk: apps that follow the old README keep working, still embedding the addon. Reverting the commit restores the embedded layout.

🤖 Generated with Claude Code

bun build --compile embeds @gpuix/native's .node in the executable, so
every .app launch after $TMPDIR is purged re-extracts it and re-pays a
Gatekeeper scan. compile-chat.ts now compiles the .app's executable with
--external '*.node' and a small entry shim that points
NAPI_RS_NATIVE_LIBRARY_PATH — checked first by napi-rs's generated
loader — at the addon shipped in Contents/Frameworks, ad-hoc signing
both. Bare `bun build --compile --production` binaries are unaffected
and keep embedding the addon.

Co-Authored-By: Claude Opus 5 (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.

Ship the native addon inside the .app so macOS stops rescanning it

1 participant