Move notification status helper to Contents/MacOS

On macOS 26, UNUserNotificationCenter aborts when executables are run
from Contents/Resources because bundleProxyForCurrentProcess returns
nil.

Moving the orca-notification-status helper to Contents/MacOS next to
the main Electron executable ensures proper bundle resolution and
avoids immediate crashes.
This commit is contained in:
Jinjing
2026-07-09 03:01:03 -07:00
parent 7e85db4130
commit 26e7a6c9d9
3 changed files with 32 additions and 16 deletions
+9 -4
View File
@@ -159,7 +159,7 @@ module.exports = {
if (context.electronPlatformName === 'darwin') {
await signMacComputerUseHelper(join(resourcesDir, 'Orca Computer Use.app'), context.packager)
await signMacNotificationStatusHelper(
join(resourcesDir, 'orca-notification-status'),
join(resourcesDir, '..', 'MacOS', 'orca-notification-status'),
context.packager
)
}
@@ -251,11 +251,16 @@ module.exports = {
from: 'native/computer-use-macos/.build/release/Orca Computer Use.app',
to: 'Orca Computer Use.app'
},
featureWallResources
],
// Why: the notification-status helper must execute from Contents/MacOS —
// on macOS 26 UNUserNotificationCenter aborts (bundleProxyForCurrentProcess
// is nil) for executables launched out of Contents/Resources (#7929).
extraFiles: [
{
from: 'native/notification-status-macos/.build/release/orca-notification-status',
to: 'orca-notification-status'
},
featureWallResources
to: 'MacOS/orca-notification-status'
}
],
target: [
{
@@ -63,6 +63,22 @@ describe('electron-builder config', () => {
)
})
// Why: on macOS 26 UNUserNotificationCenter aborts for executables launched
// from Contents/Resources, so the helper must ship in Contents/MacOS (#7929).
it('ships the mac notification-status helper in Contents/MacOS, not Resources', () => {
expect(electronBuilderConfig.mac.extraFiles).toEqual(
expect.arrayContaining([
expect.objectContaining({
from: 'native/notification-status-macos/.build/release/orca-notification-status',
to: 'MacOS/orca-notification-status'
})
])
)
expect(electronBuilderConfig.mac.extraResources).not.toEqual(
expect.arrayContaining([expect.objectContaining({ to: 'orca-notification-status' })])
)
})
it('unpacks the compiled CommonJS boundary with CLI runtime files', () => {
expect(electronBuilderConfig.asarUnpack).toEqual(
expect.arrayContaining(['out/package.json', 'out/cli/**', 'out/shared/**'])
@@ -12,10 +12,11 @@ let cachedHelperPath: string | null | undefined
/**
* Resolves the bundled notification-status helper binary.
*
* Why: the helper must live inside the app bundle — NSBundle resolves the
* process's bundle by walking up from the executable path, and macOS keys
* notification records to that identity. Both dev copies and packaged builds
* place it next to the Electron executable in Contents/MacOS.
* Why: the helper must live in Contents/MacOS next to the Electron executable
* — NSBundle resolves the process's bundle by walking up from the executable
* path, macOS keys notification records to that identity, and macOS 26 aborts
* UNUserNotificationCenter for executables run from Contents/Resources
* (#7929). Dev copies and packaged builds (extraFiles) both place it there.
*/
function resolveHelperPath(): string | null {
if (cachedHelperPath !== undefined) {
@@ -25,14 +26,8 @@ function resolveHelperPath(): string | null {
cachedHelperPath = null
return cachedHelperPath
}
// Dev copies place the helper next to the Electron executable; packaged
// builds ship it via extraResources. Both are inside the .app, which is
// what NSBundle resolution requires.
const candidates = [
join(dirname(process.execPath), HELPER_EXECUTABLE),
...(process.resourcesPath ? [join(process.resourcesPath, HELPER_EXECUTABLE)] : [])
]
cachedHelperPath = candidates.find((candidate) => existsSync(candidate)) ?? null
const candidate = join(dirname(process.execPath), HELPER_EXECUTABLE)
cachedHelperPath = existsSync(candidate) ? candidate : null
return cachedHelperPath
}