diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce54741d..ac6cc0ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index ae98cb17..9ef51684 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -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' ) } diff --git a/electron/native/bin/darwin-arm64/openscreen-native-cursor-monitor b/electron/native/bin/darwin-arm64/openscreen-native-cursor-monitor new file mode 100755 index 00000000..9b3e16e1 Binary files /dev/null and b/electron/native/bin/darwin-arm64/openscreen-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper b/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper new file mode 100755 index 00000000..f91eeb1d Binary files /dev/null and b/electron/native/bin/darwin-arm64/openscreen-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-arm64/openscreen-system-cursors b/electron/native/bin/darwin-arm64/openscreen-system-cursors new file mode 100755 index 00000000..131caccd Binary files /dev/null and b/electron/native/bin/darwin-arm64/openscreen-system-cursors differ diff --git a/electron/native/bin/darwin-arm64/openscreen-window-list b/electron/native/bin/darwin-arm64/openscreen-window-list new file mode 100755 index 00000000..34ec60b7 Binary files /dev/null and b/electron/native/bin/darwin-arm64/openscreen-window-list differ diff --git a/package.json b/package.json index 12cc9239..59858e8b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/build-native-helpers.mjs b/scripts/build-native-helpers.mjs new file mode 100644 index 00000000..5dec545b --- /dev/null +++ b/scripts/build-native-helpers.mjs @@ -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}`); +}