Ship serve-sim camera dylib as data and materialize it at runtime (#7168)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-07-02 18:24:23 -07:00 committed by GitHub
parent c06507ee30
commit f44cb3059b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 446 additions and 0 deletions

View File

@ -7,6 +7,7 @@ const {
prunePackagedRuntimeNodeModules,
verifyPackagedMainRuntimeDeps
} = require('./packaged-runtime-node-modules.cjs')
const { compressMacServeSimCameraDylibs } = require('./serve-sim-camera-packaging.cjs')
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1'
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
@ -131,6 +132,7 @@ module.exports = {
verifyPackagedMainRuntimeDeps(resourcesDir)
chmodUnixCliLaunchers(resourcesDir, context.electronPlatformName)
chmodMacServeSimHelpers(resourcesDir, context.electronPlatformName)
compressMacServeSimCameraDylibs(resourcesDir, context.electronPlatformName)
for (const filename of readdirSync(resourcesDir)) {
if (!filename.startsWith('agent-browser-')) {
continue

View File

@ -0,0 +1,104 @@
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { gunzipSync } from 'node:zlib'
import { afterEach, describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const { compressMacServeSimCameraDylibs } = require('../serve-sim-camera-packaging.cjs')
const DYLIB_CONTENT = Buffer.from('fake-ios-simulator-dylib')
async function createServeSimPackage(packageDir, { withSources = true } = {}) {
await mkdir(join(packageDir, 'dist', 'simcam'), { recursive: true })
await writeFile(join(packageDir, 'dist', 'simcam', 'libSimCameraInjector.dylib'), DYLIB_CONTENT)
await writeFile(join(packageDir, 'dist', 'simcam', 'serve-sim-camera-helper'), 'helper')
if (withSources) {
await mkdir(join(packageDir, 'Sources', 'SimCameraInjector'), { recursive: true })
await writeFile(join(packageDir, 'Sources', 'SimCameraInjector', 'build.sh'), 'echo build')
}
}
describe('compressMacServeSimCameraDylibs', () => {
const cleanupPaths = []
afterEach(async () => {
for (const path of cleanupPaths.splice(0)) {
await rm(path, { recursive: true, force: true })
}
})
async function createResourcesDir() {
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-simcam-packaging-'))
cleanupPaths.push(resourcesDir)
return resourcesDir
}
it('replaces raw dylibs with gzip payloads in both packaged copies', async () => {
const resourcesDir = await createResourcesDir()
await createServeSimPackage(join(resourcesDir, 'serve-sim'))
await createServeSimPackage(join(resourcesDir, 'node_modules', 'serve-sim'))
compressMacServeSimCameraDylibs(resourcesDir, 'darwin')
for (const packageDir of ['serve-sim', join('node_modules', 'serve-sim')]) {
const dylibPath = join(
resourcesDir,
packageDir,
'dist',
'simcam',
'libSimCameraInjector.dylib'
)
await expect(stat(dylibPath)).rejects.toThrow()
expect(gunzipSync(await readFile(`${dylibPath}.gz`))).toEqual(DYLIB_CONTENT)
const helperPath = join(resourcesDir, packageDir, 'dist', 'simcam', 'serve-sim-camera-helper')
expect(await readFile(helperPath, 'utf8')).toBe('helper')
}
})
it('removes Sources so the build-from-source fallback cannot write into the sealed bundle', async () => {
const resourcesDir = await createResourcesDir()
await createServeSimPackage(join(resourcesDir, 'serve-sim'))
compressMacServeSimCameraDylibs(resourcesDir, 'darwin')
await expect(stat(join(resourcesDir, 'serve-sim', 'Sources'))).rejects.toThrow()
})
it('throws when a packaged copy is missing its dylib', async () => {
const resourcesDir = await createResourcesDir()
await createServeSimPackage(join(resourcesDir, 'serve-sim'))
await rm(join(resourcesDir, 'serve-sim', 'dist', 'simcam', 'libSimCameraInjector.dylib'))
expect(() => compressMacServeSimCameraDylibs(resourcesDir, 'darwin')).toThrow(
/camera dylib missing/
)
})
it('throws when no packaged serve-sim copy exists on darwin', async () => {
const resourcesDir = await createResourcesDir()
expect(() => compressMacServeSimCameraDylibs(resourcesDir, 'darwin')).toThrow(
/No packaged serve-sim copies/
)
})
it('leaves non-darwin packaging untouched', async () => {
const resourcesDir = await createResourcesDir()
await createServeSimPackage(join(resourcesDir, 'serve-sim'))
compressMacServeSimCameraDylibs(resourcesDir, 'win32')
compressMacServeSimCameraDylibs(resourcesDir, 'linux')
const dylibPath = join(
resourcesDir,
'serve-sim',
'dist',
'simcam',
'libSimCameraInjector.dylib'
)
expect(await readFile(dylibPath)).toEqual(DYLIB_CONTENT)
await expect(stat(join(resourcesDir, 'serve-sim', 'Sources'))).resolves.toBeDefined()
})
})

View File

@ -0,0 +1,35 @@
const { existsSync, readFileSync, rmSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
const { gzipSync } = require('node:zlib')
// Why: libSimCameraInjector.dylib targets the iOS-simulator platform, which
// Apple's notary service never tickets for arm64 — a raw copy anywhere in the
// bundle is permanently rejected by Gatekeeper assessment. Shipping it gzipped
// makes it plain data (unsigned, unassessed); the app materializes a
// quarantine-free copy at runtime before serve-sim injects it.
function compressMacServeSimCameraDylibs(resourcesDir, electronPlatformName) {
if (electronPlatformName !== 'darwin') {
return
}
const packageDirs = [
join(resourcesDir, 'serve-sim'),
join(resourcesDir, 'node_modules', 'serve-sim')
].filter((packageDir) => existsSync(packageDir))
if (packageDirs.length === 0) {
throw new Error(`No packaged serve-sim copies found under ${resourcesDir}`)
}
for (const packageDir of packageDirs) {
const dylibPath = join(packageDir, 'dist', 'simcam', 'libSimCameraInjector.dylib')
if (!existsSync(dylibPath)) {
throw new Error(`Expected serve-sim camera dylib missing: ${dylibPath}`)
}
writeFileSync(`${dylibPath}.gz`, gzipSync(readFileSync(dylibPath)))
rmSync(dylibPath)
// Why: with the prebuilt dylib gone, serve-sim's build-from-source fallback
// would compile into the sealed bundle and break its signature. Without
// Sources the fallback fails with serve-sim's own reinstall error instead.
rmSync(join(packageDir, 'Sources'), { recursive: true, force: true })
}
}
module.exports = { compressMacServeSimCameraDylibs }

View File

@ -12,6 +12,7 @@ import { app } from 'electron'
import { platform, tmpdir } from 'node:os'
import { delimiter, dirname, join } from 'node:path'
import { EmulatorError } from './emulator-errors'
import { materializeServeSimRuntime } from './serve-sim-runtime-materializer'
const EXEC_TIMEOUT_MS = 90_000
const MAC_OPEN_SHIM_DIR = join(tmpdir(), 'orca-serve-sim-open-shim')
@ -67,6 +68,27 @@ function getServeSimEnv(executable: ServeSimExecutable): NodeJS.ProcessEnv {
return env
}
// Cached per process: materialization is a one-time copy; later calls only stat.
let materializedServeSimPackageDir: string | null | undefined
function resolveMaterializedServeSimPackageDir(bundledPackageDir: string): string | null {
if (materializedServeSimPackageDir !== undefined) {
return materializedServeSimPackageDir
}
materializedServeSimPackageDir = materializeServeSimRuntime({
bundledPackageDir,
targetRootDir: join(app.getPath('userData'), 'serve-sim-runtime'),
version: app.getVersion()
})
if (materializedServeSimPackageDir === null) {
console.warn(
'[serve-sim] runtime materialization failed; running from the app bundle ' +
'(camera injection may be blocked by Gatekeeper)'
)
}
return materializedServeSimPackageDir
}
export function resolveServeSimExecutable(): ServeSimExecutable {
const bundledResourcesPath =
process.resourcesPath ??
@ -75,6 +97,21 @@ export function resolveServeSimExecutable(): ServeSimExecutable {
: join(app.getPath('exe'), '..', 'resources'))
const bundled = join(bundledResourcesPath, 'serve-sim', 'dist', 'serve-sim.js')
if (existsSync(bundled)) {
if (process.platform === 'darwin') {
// Why: the macOS bundle ships the camera dylib gzipped (an iOS-simulator
// binary Apple never notarizes), so serve-sim must run from a
// quarantine-free materialized copy for camera injection to work.
const materializedDir = resolveMaterializedServeSimPackageDir(
join(bundledResourcesPath, 'serve-sim')
)
if (materializedDir) {
return {
command: process.execPath,
baseArgs: [join(materializedDir, 'dist', 'serve-sim.js')],
usesElectronAsNode: true
}
}
}
return { command: process.execPath, baseArgs: [bundled], usesElectronAsNode: true }
}

View File

@ -0,0 +1,158 @@
import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { gzipSync } from 'node:zlib'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { materializeServeSimRuntime } from './serve-sim-runtime-materializer'
const DYLIB_CONTENT = Buffer.from('fake-simcam-dylib-mach-o-bytes')
async function createBundledServeSimPackage(root: string): Promise<string> {
const packageDir = join(root, 'bundled-serve-sim')
await mkdir(join(packageDir, 'dist', 'simcam'), { recursive: true })
await mkdir(join(packageDir, 'bin'), { recursive: true })
await writeFile(join(packageDir, 'dist', 'serve-sim.js'), 'console.log("serve-sim")')
await writeFile(
join(packageDir, 'dist', 'simcam', 'libSimCameraInjector.dylib.gz'),
gzipSync(DYLIB_CONTENT)
)
await writeFile(join(packageDir, 'dist', 'simcam', 'serve-sim-camera-helper'), 'helper', {
mode: 0o644
})
await writeFile(join(packageDir, 'bin', 'serve-sim-bin'), 'bin', { mode: 0o644 })
return packageDir
}
describe('materializeServeSimRuntime', () => {
const cleanupPaths: string[] = []
afterEach(async () => {
for (const path of cleanupPaths.splice(0)) {
await rm(path, { recursive: true, force: true })
}
})
async function createRoot(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'orca-simcam-materializer-'))
cleanupPaths.push(root)
return root
}
it('copies the package, restores the gzipped dylib, and clears quarantine', async () => {
const root = await createRoot()
const bundledPackageDir = await createBundledServeSimPackage(root)
const clearQuarantine = vi.fn()
const materialized = materializeServeSimRuntime({
bundledPackageDir,
targetRootDir: join(root, 'runtime'),
version: '1.2.3',
clearQuarantine
})
expect(materialized).toBe(join(root, 'runtime', '1.2.3'))
const dylibPath = join(materialized!, 'dist', 'simcam', 'libSimCameraInjector.dylib')
expect(await readFile(dylibPath)).toEqual(DYLIB_CONTENT)
await expect(stat(`${dylibPath}.gz`)).rejects.toThrow()
expect(clearQuarantine).toHaveBeenCalledTimes(1)
if (process.platform !== 'win32') {
for (const executable of [
join(materialized!, 'bin', 'serve-sim-bin'),
join(materialized!, 'dist', 'simcam', 'serve-sim-camera-helper')
]) {
expect(((await stat(executable)).mode & 0o111) !== 0).toBe(true)
}
}
})
it('returns the existing runtime without re-copying', async () => {
const root = await createRoot()
const bundledPackageDir = await createBundledServeSimPackage(root)
const clearQuarantine = vi.fn()
const options = {
bundledPackageDir,
targetRootDir: join(root, 'runtime'),
version: '1.2.3',
clearQuarantine
}
const first = materializeServeSimRuntime(options)
const second = materializeServeSimRuntime(options)
expect(second).toBe(first)
expect(clearQuarantine).toHaveBeenCalledTimes(1)
})
it('prunes runtimes left behind by older app versions', async () => {
const root = await createRoot()
const bundledPackageDir = await createBundledServeSimPackage(root)
const targetRootDir = join(root, 'runtime')
await mkdir(join(targetRootDir, '1.0.0', 'dist'), { recursive: true })
await writeFile(join(targetRootDir, '1.0.0', 'dist', 'serve-sim.js'), 'old')
materializeServeSimRuntime({
bundledPackageDir,
targetRootDir,
version: '1.2.3',
clearQuarantine: () => {}
})
await expect(stat(join(targetRootDir, '1.0.0'))).rejects.toThrow()
})
it('materializes packages that still ship a raw dylib', async () => {
const root = await createRoot()
const bundledPackageDir = await createBundledServeSimPackage(root)
await rm(join(bundledPackageDir, 'dist', 'simcam', 'libSimCameraInjector.dylib.gz'))
await writeFile(
join(bundledPackageDir, 'dist', 'simcam', 'libSimCameraInjector.dylib'),
DYLIB_CONTENT
)
const materialized = materializeServeSimRuntime({
bundledPackageDir,
targetRootDir: join(root, 'runtime'),
version: '1.2.3',
clearQuarantine: () => {}
})
expect(materialized).not.toBeNull()
expect(
await readFile(join(materialized!, 'dist', 'simcam', 'libSimCameraInjector.dylib'))
).toEqual(DYLIB_CONTENT)
})
it('returns null and leaves no staging behind when quarantine clearing fails', async () => {
const root = await createRoot()
const bundledPackageDir = await createBundledServeSimPackage(root)
const targetRootDir = join(root, 'runtime')
const materialized = materializeServeSimRuntime({
bundledPackageDir,
targetRootDir,
version: '1.2.3',
clearQuarantine: () => {
throw new Error('xattr failed')
}
})
expect(materialized).toBeNull()
await expect(stat(join(targetRootDir, '1.2.3'))).rejects.toThrow()
const { readdir } = await import('node:fs/promises')
const leftovers = (await readdir(targetRootDir)).filter((name) => name.startsWith('.staging'))
expect(leftovers).toEqual([])
})
it('returns null when the bundled package is missing', async () => {
const root = await createRoot()
const materialized = materializeServeSimRuntime({
bundledPackageDir: join(root, 'does-not-exist'),
targetRootDir: join(root, 'runtime'),
version: '1.2.3',
clearQuarantine: () => {}
})
expect(materialized).toBeNull()
})
})

View File

@ -0,0 +1,110 @@
import { execFileSync } from 'node:child_process'
import {
chmodSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
rmSync,
writeFileSync
} from 'node:fs'
import { join } from 'node:path'
import { gunzipSync } from 'node:zlib'
export type ServeSimRuntimeMaterializerOptions = {
bundledPackageDir: string
targetRootDir: string
version: string
clearQuarantine?: (dir: string) => void
}
const CAMERA_DYLIB_RELATIVE_PATH = join('dist', 'simcam', 'libSimCameraInjector.dylib')
const EXECUTABLE_RELATIVE_PATHS = [
join('bin', 'serve-sim-bin'),
join('dist', 'simcam', 'serve-sim-camera-helper')
]
function defaultClearQuarantine(dir: string): void {
if (process.platform !== 'darwin') {
return
}
// Why: bundle files carry com.apple.quarantine after download/update and
// cpSync can clone xattrs; a quarantined camera dylib injected into a
// simulator process is what syspolicyd malware-rejects.
execFileSync('/usr/bin/xattr', ['-cr', dir], { timeout: 30_000 })
}
function pruneStaleServeSimRuntimes(targetRootDir: string, keepVersion: string): void {
let entries: string[]
try {
entries = readdirSync(targetRootDir)
} catch {
return
}
for (const entryName of entries) {
if (entryName === keepVersion) {
continue
}
try {
rmSync(join(targetRootDir, entryName), { recursive: true, force: true })
} catch {
// Old-version cleanup is best-effort; a locked file must not block materialization.
}
}
}
// Copies the bundled serve-sim package to a per-version directory outside the
// signed app bundle, restores the gzipped camera dylib, and strips quarantine.
// Why: the bundle cannot ship the raw dylib (iOS-simulator platform, never
// notarizable), and serve-sim resolves camera assets relative to its own
// entry, so the whole package must run from the materialized copy.
export function materializeServeSimRuntime(
options: ServeSimRuntimeMaterializerOptions
): string | null {
const { bundledPackageDir, targetRootDir, version } = options
const clearQuarantine = options.clearQuarantine ?? defaultClearQuarantine
const targetDir = join(targetRootDir, version)
const entryPath = join(targetDir, 'dist', 'serve-sim.js')
if (existsSync(entryPath)) {
return targetDir
}
const stagingDir = join(targetRootDir, `.staging-${version}-${process.pid}`)
try {
mkdirSync(targetRootDir, { recursive: true })
pruneStaleServeSimRuntimes(targetRootDir, version)
rmSync(stagingDir, { recursive: true, force: true })
rmSync(targetDir, { recursive: true, force: true })
cpSync(bundledPackageDir, stagingDir, { recursive: true })
const gzippedDylibPath = join(stagingDir, `${CAMERA_DYLIB_RELATIVE_PATH}.gz`)
if (existsSync(gzippedDylibPath)) {
writeFileSync(
join(stagingDir, CAMERA_DYLIB_RELATIVE_PATH),
gunzipSync(readFileSync(gzippedDylibPath)),
{ mode: 0o755 }
)
rmSync(gzippedDylibPath)
}
for (const relativePath of EXECUTABLE_RELATIVE_PATHS) {
const executablePath = join(stagingDir, relativePath)
if (existsSync(executablePath)) {
chmodSync(executablePath, 0o755)
}
}
clearQuarantine(stagingDir)
try {
renameSync(stagingDir, targetDir)
} catch (error) {
// Another app instance sharing userData may have finished first.
if (!existsSync(entryPath)) {
throw error
}
}
return existsSync(entryPath) ? targetDir : null
} catch {
return null
} finally {
rmSync(stagingDir, { recursive: true, force: true })
}
}