Prebundle mac native helpers and avoid runtime Swift compile in packaged app

This commit is contained in:
webadderall
2026-03-13 19:31:06 +11:00
parent 2e915a7d09
commit 41805b8e7a
8 changed files with 105 additions and 7 deletions
+6
View File
@@ -44,6 +44,9 @@ jobs:
- name: Install app dependencies
run: npx electron-builder install-app-deps
- name: Build native macOS helpers
run: npm run build:native-helpers
- name: Build macOS x64
run: |
npx tsc
@@ -77,6 +80,9 @@ jobs:
- name: Install app dependencies
run: npx electron-builder install-app-deps
- name: Build native macOS helpers
run: npm run build:native-helpers
- name: Build macOS arm64
run: |
npx tsc
+37 -5
View File
@@ -3,6 +3,7 @@ import { execFile, spawn, spawnSync } from 'node:child_process'
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import fs from 'node:fs/promises'
import { constants as fsConstants } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { promisify } from 'node:util'
@@ -202,6 +203,14 @@ function getNativeCaptureHelperSourcePath() {
return resolveUnpackedAppPath('electron', 'native', 'ScreenCaptureKitRecorder.swift')
}
function getNativeArchTag() {
return process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
}
function getPrebundledNativeHelperPath(binaryName: string) {
return resolveUnpackedAppPath('electron', 'native', 'bin', getNativeArchTag(), binaryName)
}
function getNativeCaptureHelperBinaryPath() {
return path.join(app.getPath('userData'), 'native-tools', 'openscreen-screencapturekit-helper')
}
@@ -247,7 +256,26 @@ type NativeMacWindowSource = {
let cachedNativeMacWindowSources: NativeMacWindowSource[] | null = null
let cachedNativeMacWindowSourcesAtMs = 0
async function ensureSwiftHelperBinary(sourcePath: string, binaryPath: string, label: string) {
async function ensureSwiftHelperBinary(
sourcePath: string,
binaryPath: string,
label: string,
prebundledBinaryName?: string,
) {
if (prebundledBinaryName) {
const prebundledPath = getPrebundledNativeHelperPath(prebundledBinaryName)
try {
await fs.access(prebundledPath, fsConstants.X_OK)
return prebundledPath
} catch {
if (app.isPackaged) {
throw new Error(
`${label} is missing from this app build (${prebundledPath}). Reinstall or update the app.`
)
}
}
}
const helperDir = path.dirname(binaryPath)
await fs.mkdir(helperDir, { recursive: true })
@@ -284,7 +312,8 @@ async function ensureNativeCaptureHelperBinary() {
return ensureSwiftHelperBinary(
getNativeCaptureHelperSourcePath(),
getNativeCaptureHelperBinaryPath(),
'native ScreenCaptureKit helper'
'native ScreenCaptureKit helper',
'openscreen-screencapturekit-helper'
)
}
@@ -292,7 +321,8 @@ async function ensureNativeWindowListBinary() {
return ensureSwiftHelperBinary(
getNativeWindowListSourcePath(),
getNativeWindowListBinaryPath(),
'native ScreenCaptureKit window list helper'
'native ScreenCaptureKit window list helper',
'openscreen-window-list'
)
}
@@ -348,7 +378,8 @@ async function getSystemCursorAssets() {
const binaryPath = await ensureSwiftHelperBinary(
sourcePath,
getSystemCursorHelperBinaryPath(),
'system cursor helper'
'system cursor helper',
'openscreen-system-cursors'
)
const { stdout } = await execFileAsync(binaryPath, [], { timeout: 15000, maxBuffer: 20 * 1024 * 1024 })
@@ -748,7 +779,8 @@ async function ensureNativeCursorMonitorBinary() {
return ensureSwiftHelperBinary(
getNativeCursorMonitorSourcePath(),
getNativeCursorMonitorBinaryPath(),
'native cursor monitor helper'
'native cursor monitor helper',
'openscreen-native-cursor-monitor'
)
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -2
View File
@@ -17,13 +17,14 @@
"scripts": {
"dev": "vite",
"postinstall": "npm run rebuild:native",
"build": "tsc && vite build && electron-builder",
"build": "npm run build:native-helpers && tsc && vite build && electron-builder",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"preview": "vite preview",
"rebuild:native": "electron-rebuild --force --only uiohook-napi",
"build:mac": "tsc && vite build && electron-builder --mac",
"build:native-helpers": "node scripts/build-native-helpers.mjs",
"build:mac": "npm run build:native-helpers && tsc && vite build && electron-builder --mac",
"build:win": "tsc && vite build && electron-builder --win",
"build:linux": "tsc && vite build && electron-builder --linux",
"test": "vitest --run",
+59
View File
@@ -0,0 +1,59 @@
import { spawnSync } from 'node:child_process';
import { chmod, mkdir } from 'node:fs/promises';
import path from 'node:path';
const projectRoot = process.cwd();
const nativeRoot = path.join(projectRoot, 'electron', 'native');
if (process.platform !== 'darwin') {
console.log('[build-native-helpers] Skipping: host platform is not macOS.');
process.exit(0);
}
const archTag = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
const outputDir = path.join(nativeRoot, 'bin', archTag);
const helpers = [
{
source: 'ScreenCaptureKitRecorder.swift',
output: 'openscreen-screencapturekit-helper',
},
{
source: 'ScreenCaptureKitWindowList.swift',
output: 'openscreen-window-list',
},
{
source: 'SystemCursorAssets.swift',
output: 'openscreen-system-cursors',
},
{
source: 'NativeCursorMonitor.swift',
output: 'openscreen-native-cursor-monitor',
},
];
const swiftcCheck = spawnSync('swiftc', ['--version'], { encoding: 'utf8' });
if (swiftcCheck.status !== 0) {
const details = [swiftcCheck.stderr, swiftcCheck.stdout].filter(Boolean).join('\n').trim();
throw new Error(details || 'swiftc is unavailable; install Xcode Command Line Tools.');
}
await mkdir(outputDir, { recursive: true });
for (const helper of helpers) {
const sourcePath = path.join(nativeRoot, helper.source);
const outputPath = path.join(outputDir, helper.output);
const result = spawnSync('swiftc', ['-O', sourcePath, '-o', outputPath], {
encoding: 'utf8',
timeout: 120000,
});
if (result.status !== 0) {
const details = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
throw new Error(details || `Failed to compile ${helper.source}`);
}
await chmod(outputPath, 0o755);
console.log(`[build-native-helpers] Built ${helper.output} -> ${outputPath}`);
}