mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
895 lines
37 KiB
JavaScript
895 lines
37 KiB
JavaScript
const { chmodSync, existsSync, readdirSync, readFileSync, writeFileSync } = require('node:fs')
|
||
const { execFileSync } = require('node:child_process')
|
||
const { join, resolve } = require('node:path')
|
||
const {
|
||
MACOS_FOLDER_USAGE_DESCRIPTIONS,
|
||
applyMacHelperFolderUsageDescriptions
|
||
} = require('./scripts/macos-folder-usage-descriptions.cjs')
|
||
const electronBuilderNativeRebuild = require('./scripts/electron-builder-native-rebuild.cjs')
|
||
const {
|
||
assertPackagedDaemonEntryExists,
|
||
verifyPackagedDaemonEntryBoots,
|
||
verifyPackagedMacTerminalHostBoots
|
||
} = require('./scripts/verify-packaged-daemon-entry.cjs')
|
||
const {
|
||
buildMacTerminalHost,
|
||
macTerminalHostSignIgnore,
|
||
verifyMacTerminalHost
|
||
} = require('./scripts/macos-terminal-host-bundle.cjs')
|
||
const {
|
||
assertPackagedNativeVariantsInstalled,
|
||
assertProcessHostOutputBuilt,
|
||
createPackagedRuntimeNodeModuleResources,
|
||
prunePackagedRuntimeNodeModules,
|
||
verifyPackagedMainRuntimeDeps
|
||
} = require('./packaged-runtime-node-modules.cjs')
|
||
const {
|
||
assertNoWindowsExecutablesInRuntimeNodeModules
|
||
} = require('./packaged-windows-only-runtime-files.cjs')
|
||
const { verifyLinuxGlibcFloor } = require('./scripts/verify-linux-glibc-floor.cjs')
|
||
const {
|
||
verifyPackagedWindowsAgentHook
|
||
} = require('./scripts/verify-packaged-windows-agent-hook.cjs')
|
||
const {
|
||
ensureBundledWaylandClipboard,
|
||
finalizePackagedWaylandClipboard
|
||
} = require('./wayland-clipboard-resources.cjs')
|
||
const { writeMacBuildCompatibility } = require('./scripts/mac-build-compatibility.cjs')
|
||
const {
|
||
MOBILE_WEB_BUNDLE_DIR,
|
||
assertMobileWebBundleBuilt
|
||
} = require('./scripts/verify-packaged-mobile-web-bundle.cjs')
|
||
const { verifyPackagedPluginResources } = require('./scripts/verify-packaged-plugin-resources.cjs')
|
||
const {
|
||
assertPackagedServeSelectionEntry,
|
||
assertPackagedWebClient
|
||
} = require('./scripts/verify-packaged-serve-payload.cjs')
|
||
const {
|
||
assertBundledRipgrepInstalled,
|
||
bundledRipgrepExtraResources,
|
||
bundledRipgrepMacSignIgnore,
|
||
finalizePackagedRipgrep
|
||
} = require('./bundled-ripgrep-resources.cjs')
|
||
const {
|
||
verifyPackagedWindowsNodePty
|
||
} = require('./scripts/verify-packaged-node-pty-job-ownership.cjs')
|
||
const {
|
||
assertOrcadTemplateBuilt,
|
||
finalizePackagedOrcadTemplate,
|
||
orcadTemplateExtraResource,
|
||
orcadTemplateNodeModulesExtraResource,
|
||
orcadTemplateMacSignIgnore
|
||
} = require('./scripts/packaged-orcad-template.cjs')
|
||
const { verifySkillsCliRuntime } = require('./scripts/verify-skills-cli-runtime.cjs')
|
||
const { assertNotDarkBuild } = require('./dark-build-switches.cjs')
|
||
const { verifyStaticAppImagePackage } = require('./scripts/static-appimage-package-contract.cjs')
|
||
const {
|
||
appImageNodeRuntimeFpmArgs,
|
||
placeAppImageNodeRuntime,
|
||
verifyAppImageNodeRuntimeServesPty
|
||
} = require('./scripts/appimage-node-runtime.cjs')
|
||
const { signWindowsUninstallerViaSignPath } = require('./scripts/windows-uninstaller-signing.cjs')
|
||
const { buildMacShipItScheduling } = require('./scripts/macos-shipit-scheduling.cjs')
|
||
|
||
// Dev channels need release bundle ID/signature/notarization so Squirrel.Mac accepts updates and macOS retains app identity.
|
||
const isMacHourly = process.env.ORCA_MAC_HOURLY === '1'
|
||
const isMacDaily = process.env.ORCA_MAC_DAILY === '1'
|
||
const isMacAdhoc = process.env.ORCA_MAC_ADHOC === '1'
|
||
// Keep Windows dev flags separate: mac flags enable hardened runtime/notarization/global signing, which would demand an unavailable Windows cert.
|
||
const isWinHourly = process.env.ORCA_WIN_HOURLY === '1'
|
||
const isWinDaily = process.env.ORCA_WIN_DAILY === '1'
|
||
const isWinAdhoc = process.env.ORCA_WIN_ADHOC === '1'
|
||
const isWinDevChannel = isWinHourly || isWinDaily || isWinAdhoc
|
||
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1' || isMacHourly || isMacDaily || isMacAdhoc
|
||
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
|
||
const localBuildVersion =
|
||
isMacRelease || isWinDevChannel ? undefined : process.env.ORCA_LOCAL_BUILD_VERSION
|
||
const isHourlyChannel = isMacHourly || isWinHourly
|
||
const isDailyChannel = isMacDaily || isWinDaily
|
||
const isAdhocChannel = isMacAdhoc || isWinAdhoc
|
||
const devChannelBuildVersion = isHourlyChannel
|
||
? process.env.ORCA_HOURLY_BUILD_VERSION
|
||
: isDailyChannel
|
||
? process.env.ORCA_DAILY_BUILD_VERSION
|
||
: isAdhocChannel
|
||
? process.env.ORCA_ADHOC_BUILD_VERSION
|
||
: undefined
|
||
// Separate dev-channel repos: 24 hourly tags/day would evict stable/RC from the ten-entry release feed; isolate adhoc/daily from hourly subscribers too.
|
||
const devChannelRepo = isHourlyChannel
|
||
? 'orca-hourly'
|
||
: isDailyChannel
|
||
? 'orca-daily'
|
||
: isAdhocChannel
|
||
? 'orca-adhoc'
|
||
: null
|
||
const appId = 'com.stablyai.orca'
|
||
const featureWallResources = {
|
||
from: 'resources/onboarding/feature-wall',
|
||
to: 'onboarding/feature-wall'
|
||
}
|
||
// Freshness checks need immutable build identity, not skill bytes or runtime network access.
|
||
const skillFreshnessResources = {
|
||
from: 'resources/skills',
|
||
to: 'skills'
|
||
}
|
||
// The bundled agent-browser binary finds its version-matched catalog via the nearest skills/ above it (Resources/).
|
||
const agentBrowserSkillResources = [
|
||
{ from: 'node_modules/agent-browser/skills/agent-browser', to: 'skills/agent-browser' },
|
||
{ from: 'node_modules/agent-browser/skill-data', to: 'skill-data' }
|
||
]
|
||
// Native agents load plugins by path and cannot read inside app.asar.
|
||
const nativeChatVisualsResource = {
|
||
from: 'resources/native-chat-visuals',
|
||
to: 'native-chat-visuals'
|
||
}
|
||
// SSH deploy resolves process.resourcesPath/relay; extraResources keeps bundles on real disk.
|
||
const relayExtraResource = {
|
||
from: 'out/relay',
|
||
to: 'relay'
|
||
}
|
||
// Bootstrap verifies/publishes immutable plugin bytes from ordinary directories.
|
||
const bundledPluginResources = {
|
||
from: 'resources/plugins/launch',
|
||
to: 'plugins/launch'
|
||
}
|
||
// Packaged main/CLI/SSH/speech paths lack pnpm links; copy runtime closure so bare requires cannot fall through to checkout dependencies.
|
||
// Ship only the 166KB emoji dataset used by worktree naming, not all 49MB of emojibase-data.
|
||
const emojiShortcodeDatasetResource = {
|
||
from: 'node_modules/emojibase-data/en/shortcodes/emojibase.json',
|
||
to: 'node_modules/emojibase-data/en/shortcodes/emojibase.json'
|
||
}
|
||
const commonExtraResources = [
|
||
relayExtraResource,
|
||
orcadTemplateExtraResource,
|
||
orcadTemplateNodeModulesExtraResource,
|
||
...bundledRipgrepExtraResources,
|
||
bundledPluginResources,
|
||
skillFreshnessResources,
|
||
...agentBrowserSkillResources,
|
||
nativeChatVisualsResource,
|
||
emojiShortcodeDatasetResource
|
||
]
|
||
// Native speech addons need real disk paths; copy only the artifact target’s variant.
|
||
const macSpeechNativeResource = {
|
||
from: 'node_modules/sherpa-onnx-darwin-${arch}',
|
||
to: 'node_modules/sherpa-onnx-darwin-${arch}'
|
||
}
|
||
const linuxSpeechNativeResource = {
|
||
from: 'node_modules/sherpa-onnx-linux-${arch}',
|
||
to: 'node_modules/sherpa-onnx-linux-${arch}'
|
||
}
|
||
const winSpeechNativeResource = {
|
||
from: 'node_modules/sherpa-onnx-win-x64',
|
||
to: 'node_modules/sherpa-onnx-win-x64'
|
||
}
|
||
// electron-builder replaces these defaults when `depends` is configured; retain
|
||
// Electron's loader requirements alongside Orca's headless-host dependencies.
|
||
const debElectronRuntimeDependencies = [
|
||
'libgtk-3-0',
|
||
'libnotify4',
|
||
'libnss3',
|
||
'libxss1',
|
||
'libxtst6',
|
||
'xdg-utils',
|
||
'libatspi2.0-0',
|
||
'libuuid1',
|
||
'libsecret-1-0',
|
||
// Electron links these but electron-builder's defaults omit them; t64 is Ubuntu 24.04+/Debian 13.
|
||
'libasound2t64 | libasound2',
|
||
'libgbm1'
|
||
]
|
||
const rpmElectronRuntimeDependencies = [
|
||
'gtk3',
|
||
'libnotify',
|
||
'nss',
|
||
'libXScrnSaver',
|
||
'(libXtst or libXtst6)',
|
||
'xdg-utils',
|
||
'at-spi2-core',
|
||
'(libuuid or libuuid1)',
|
||
'alsa-lib'
|
||
]
|
||
|
||
// Mirror OS document extensions: CJS packager cannot import TS; keep startup/os-opened-documents.ts and NSIS hooks aligned.
|
||
const MARKDOWN_FILE_EXTENSIONS = ['md', 'markdown', 'mdx']
|
||
const TABULAR_FILE_EXTENSIONS = ['csv', 'tsv']
|
||
|
||
// Config loads without unused Windows addons; beforePack enforces target natives. Only @vscode/windows-process-tree has os:win32; workspace links prove no binary exists.
|
||
const windowsRuntimeResources = existsSync(
|
||
join(__dirname, '..', 'node_modules', '@vscode', 'windows-process-tree', 'package.json')
|
||
)
|
||
? createPackagedRuntimeNodeModuleResources('win32')
|
||
: []
|
||
|
||
// Shared with the terminal host helper so its purpose strings cannot drift from the app's.
|
||
const macUsageDescriptions = {
|
||
NSAppleEventsUsageDescription:
|
||
'Orca allows terminal-launched developer tools to automate local apps when you request it.',
|
||
NSBluetoothAlwaysUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access Bluetooth devices when you request it.',
|
||
NSBluetoothPeripheralUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access Bluetooth devices when you request it.',
|
||
NSCalendarsUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your calendars when you request it.',
|
||
NSCalendarsFullAccessUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your calendars when you request it.',
|
||
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
||
NSContactsUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your contacts when you request it.',
|
||
NSLocationUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access location when you request it.',
|
||
NSLocalNetworkUsageDescription:
|
||
'Orca allows terminal-launched developer tools to discover and connect to local development servers when you request it.',
|
||
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
||
NSRemindersUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your reminders when you request it.',
|
||
NSRemindersFullAccessUsageDescription:
|
||
'Orca allows terminal-launched developer tools to access your reminders when you request it.',
|
||
NSAudioCaptureUsageDescription:
|
||
'Orca allows terminal-launched developer tools to capture desktop audio when you request it.',
|
||
NSBonjourServices: ['_http._tcp', '_https._tcp'],
|
||
...MACOS_FOLDER_USAGE_DESCRIPTIONS
|
||
}
|
||
// Developer ID team that signs every release-channel mac build.
|
||
const ORCA_MAC_TEAM_ID = '6CX3WHS9HZ'
|
||
|
||
/** @type {import('electron-builder').Configuration} */
|
||
module.exports = {
|
||
appId,
|
||
productName: 'Orca',
|
||
protocols: [{ name: 'Orca', schemes: ['orca'] }],
|
||
toolsets: { appimage: '1.0.3' },
|
||
...(devChannelBuildVersion
|
||
? { extraMetadata: { version: devChannelBuildVersion } }
|
||
: localBuildVersion
|
||
? { extraMetadata: { version: localBuildVersion } }
|
||
: {}),
|
||
directories: {
|
||
buildResources: 'resources/build'
|
||
},
|
||
files: [
|
||
'!**/.vscode/*',
|
||
// Repo inputs already bundled/copied through extraResources would bloat app.asar.
|
||
'!src{,/**/*}',
|
||
'!out/orcad{,/**/*}',
|
||
// Never in app.asar: the template ships via orcadTemplateExtraResource; prebuilds are build inputs.
|
||
'!out/orcad-*{,/**/*}',
|
||
'!out/.orcad-*{,/**/*}',
|
||
// Why: the pinned Node a local orcad build references (~120 MB) and its download cache.
|
||
'!out/runtimes{,/**/*}',
|
||
'!out/node-runtime-cache{,/**/*}',
|
||
'!config{,/**/*}',
|
||
// Cloud services deploy separately; their sources are not desktop runtime resources.
|
||
'!cloud{,/**/*}',
|
||
// Release archives and other build staging are not runtime resources.
|
||
'!.build{,/**/*}',
|
||
// Worktree-local Vite dependency caches are development inputs.
|
||
'!.cache/vite{,.lock}{,/**/*}',
|
||
'!docs{,/**/*}',
|
||
'!mobile{,/**/*}',
|
||
'!native{,/**/*}',
|
||
'!skills{,/**/*}',
|
||
// Authoring guides/stubs compile into runtime artifacts; source trees only duplicate content.
|
||
'!skill-guides{,/**/*}',
|
||
'!skill-stubs{,/**/*}',
|
||
'!tests{,/**/*}',
|
||
// Examples have no runtime consumer and include adversarial hostile-panel; ship only resources/plugins/launch.
|
||
'!examples{,/**/*}',
|
||
// Exclude gitignored local screenshot evidence defensively so packaging cannot pick up stray captures.
|
||
'!pr-evidence{,/**/*}',
|
||
// Local build logs and rollback copies are never application resources.
|
||
'!notes{,/**/*}',
|
||
// Local tooling directories are not runtime inputs and may contain symlink loops.
|
||
'!{.claude,.grok,.agents,.codex}{,/**/*}',
|
||
'!Casks{,/**/*}',
|
||
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md,ORCHESTRATION_IMPLEMENTATION_CHECKLIST.md,ORCHESTRATION_STRUCTURED_OUTPUT_DESIGN.md}',
|
||
'!out/**/*.test.js',
|
||
// Why: main builds with sourcemap:'hidden' so release CI can publish maps
|
||
// for decoding minified crash traces. The app never loads them (no
|
||
// sourceMappingURL is emitted), and packing them would add ~34MB to app.asar.
|
||
'!out/**/*.map',
|
||
// Why: Vite's manifest is only used to project the paired web client.
|
||
'!out/renderer/.vite{,/**/*}',
|
||
// The installed browser serves its allowlisted assets from the unpacked renderer.
|
||
'!out/web{,/**/*}',
|
||
// Local dev Electron.app caches (~270MB/branch) must not enter packaged builds.
|
||
'!out/electron-dev{,/**/*}',
|
||
// extraResources already ships relay; duplicating it in asar added 14MB and let script-heuristic quarantine gut the whole archive (#20966/#20972).
|
||
'!out/relay{,/**/*}',
|
||
'!electron.vite.config.{js,ts,mjs,cjs}',
|
||
'!{.oxlintrc.json,.oxfmtrc.json,components.json,opencode.json,orca.yaml,pnpm-workspace.yaml,vite.web.config.ts}',
|
||
'!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,CHANGELOG.md,README.md}',
|
||
'!{.env,.env.*,.npmrc,pnpm-lock.yaml}',
|
||
'!tsconfig.json',
|
||
// Feature media is read from process.resourcesPath via extraResources; exclude duplicate sources.
|
||
'!resources/onboarding/feature-wall/**',
|
||
'!resources/skills/**',
|
||
'!resources/native-chat-visuals/**',
|
||
// extraResources already ships plugin bytes; exclude source duplicates.
|
||
'!resources/plugins/launch/**',
|
||
// Platform extraResources selects speech natives; asar would duplicate it and ship other variants.
|
||
'!node_modules/sherpa-onnx*{,/**/*}',
|
||
// CLI shim belongs beside resources/bin/orca.exe; unpacking source creates a second .cmd without its executable (#7351).
|
||
'!resources/win32{,/**/*}'
|
||
],
|
||
// Node-mode CLI/daemon/foreign SQLite reader/serve entries cannot read asar; unpack their shared/main dependencies too.
|
||
// Keep out/package.json’s CommonJS boundary and remote CLI WebSocket/E2EE deps. Electron-only workers may stay packed.
|
||
// Renderer/mobile-web assets need real paths: swept temporary asar extracts are not recreated (#27976).
|
||
asarUnpack: [
|
||
'out/package.json',
|
||
'out/cli/**',
|
||
'out/shared/**',
|
||
'out/main/agent-hooks/**',
|
||
'out/main/antigravity/**',
|
||
'out/main/claude/**',
|
||
'out/main/codex/**',
|
||
'out/main/copilot/**',
|
||
'out/main/cursor/**',
|
||
'out/main/droid/**',
|
||
'out/main/gemini/**',
|
||
'out/main/gitlab/project-ref-parser.js',
|
||
'out/main/grok/**',
|
||
'out/main/hermes/**',
|
||
'out/main/asar-transparent-fs.js',
|
||
'out/main/macos-tree-clone.js',
|
||
'out/main/orca-profiles/profile-index-store.js',
|
||
'out/main/persistence/profile-state/**',
|
||
'out/main/skills/bundled-skill-install/cli-entry.js',
|
||
'out/main/startup/http1-compatibility-marker.js',
|
||
'out/main/daemon-entry.js',
|
||
'out/main/session-scanner-service-entry.js',
|
||
'out/main/wsl-transcript-fs-process-entry.js',
|
||
'out/main/foreign-sqlite-reader-entry.js',
|
||
'out/main/plugin-host-entry.js',
|
||
'out/main/browser-extension-native-host-supervisor-entry.js',
|
||
'out/main/computer-sidecar.js',
|
||
'out/main/parcel-watcher-process-entry.js',
|
||
'out/main/orcad/orcad-local-serve-selection-entry.js',
|
||
'out/main/orcad/orcad-stale-lock-reclaim.js',
|
||
'out/main/chunks/**',
|
||
'out/renderer/**',
|
||
'out/mobile-web/**',
|
||
'resources/**',
|
||
'node_modules/ws/**',
|
||
'node_modules/tweetnacl/**',
|
||
'node_modules/zod/**',
|
||
'node_modules/yaml/**'
|
||
],
|
||
artifactBuildCompleted: ({ file, arch }) => {
|
||
if (file.endsWith('.AppImage')) {
|
||
verifyStaticAppImagePackage(file, arch)
|
||
}
|
||
},
|
||
// electron-builder calls this with the context alone. The second parameter is the bundle root,
|
||
// so a test can point the guard at a scratch bundle instead of needing the repo's out/ built.
|
||
beforePack: (
|
||
context,
|
||
mobileWebBundleDir = MOBILE_WEB_BUNDLE_DIR,
|
||
ensureWaylandClipboard = ensureBundledWaylandClipboard,
|
||
orcadTemplateDir = join(__dirname, '..', orcadTemplateExtraResource.from)
|
||
) => {
|
||
// The template is the server every SSH host gets, so a dark test template must never ship.
|
||
assertNotDarkBuild([join(__dirname, '..', 'out', 'main'), mobileWebBundleDir, orcadTemplateDir])
|
||
assertPackagedNativeVariantsInstalled(context.electronPlatformName, context.arch)
|
||
assertBundledRipgrepInstalled()
|
||
assertProcessHostOutputBuilt()
|
||
assertOrcadTemplateBuilt()
|
||
assertMobileWebBundleBuilt(mobileWebBundleDir)
|
||
if (context.electronPlatformName === 'linux') {
|
||
ensureWaylandClipboard(context.arch)
|
||
}
|
||
},
|
||
afterPack: async (context) => {
|
||
const resourcesDir =
|
||
context.electronPlatformName === 'darwin'
|
||
? join(
|
||
context.appOutDir,
|
||
`${context.packager.appInfo.productFilename}.app`,
|
||
'Contents',
|
||
'Resources'
|
||
)
|
||
: join(context.appOutDir, 'resources')
|
||
if (!existsSync(resourcesDir)) {
|
||
throw new Error(`Missing packaged resources directory: ${resourcesDir}`)
|
||
}
|
||
// FpmTarget replaces this with deb/rpm while building those artifacts from the shared app tree.
|
||
if (context.electronPlatformName === 'linux') {
|
||
writeFileSync(join(resourcesDir, 'package-type'), 'AppImage')
|
||
}
|
||
if (context.electronPlatformName === 'darwin') {
|
||
applyMacHelperFolderUsageDescriptions(
|
||
join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`),
|
||
context.packager.appInfo.productFilename
|
||
)
|
||
const architecture = macSliceArch(context.arch, 'local-build compatibility')
|
||
const schedulingAddon = buildMacShipItScheduling(resourcesDir, architecture)
|
||
await signMacStandaloneHelper(schedulingAddon, 'ShipIt scheduling', context.packager)
|
||
const version = context.packager.appInfo.version
|
||
let commit = process.env.ORCA_BUILD_COMMIT || process.env.GITHUB_SHA || 'unknown'
|
||
if (commit === 'unknown') {
|
||
try {
|
||
commit = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], {
|
||
encoding: 'utf8'
|
||
}).trim()
|
||
} catch {
|
||
// Source archives can still produce a signed build with an explicit version.
|
||
}
|
||
}
|
||
writeMacBuildCompatibility(resourcesDir, { version, commit, architecture })
|
||
}
|
||
stampPackagedCliVersion(resourcesDir, context.packager.appInfo.version)
|
||
prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName, context.arch)
|
||
assertNoWindowsExecutablesInRuntimeNodeModules(resourcesDir, context.electronPlatformName)
|
||
// Gate native glibc floor after pruning foreign CPU variants; GLIBC_2.34 once broke Ubuntu 20.04 startup (#9902).
|
||
let appImageNodeRuntime = null
|
||
if (context.electronPlatformName === 'linux') {
|
||
const targetArch = { 1: 'x64', 3: 'arm64' }[context.arch]
|
||
// Before the floor gate, so the bundled Node is held to the same glibc floor.
|
||
appImageNodeRuntime = await placeAppImageNodeRuntime(resourcesDir, targetArch)
|
||
// Why the arch is passed: symbol-version checks pass happily on a wrong-architecture binary,
|
||
// so a cross-built slice could ship the host's pty.node and only fail at runtime.
|
||
finalizePackagedWaylandClipboard(resourcesDir)
|
||
verifyLinuxGlibcFloor(context.appOutDir, { targetArch })
|
||
}
|
||
verifyPackagedMainRuntimeDeps(resourcesDir)
|
||
// Smoke only host-runnable slices: daemon JS loads target-arch node-pty. Arch enum ia32=0, x64=1, armv7l=2, arm64=3, universal=4.
|
||
const archEnumByNodeArch = { ia32: 0, x64: 1, armv7l: 2, arm64: 3 }
|
||
const hostArchEnum = archEnumByNodeArch[process.arch]
|
||
const canExecuteTargetArch = context.arch === hostArchEnum || context.arch === 4
|
||
if (context.electronPlatformName === 'win32') {
|
||
verifyPackagedWindowsNodePty(resourcesDir, context.arch, { canExecuteTargetArch })
|
||
verifyPackagedWindowsAgentHook(resourcesDir, context.arch)
|
||
}
|
||
verifySkillsCliRuntime(join(resourcesDir, 'app.asar.unpacked', 'out'), resourcesDir, {
|
||
executeCommands: canExecuteTargetArch
|
||
})
|
||
if (!canExecuteTargetArch) {
|
||
console.log(
|
||
`[verify-skills-cli-runtime] skipped command probes on cross-arch slice (target ${context.arch}, host ${process.arch})`
|
||
)
|
||
}
|
||
if (canExecuteTargetArch) {
|
||
verifyPackagedDaemonEntryBoots(resourcesDir)
|
||
if (appImageNodeRuntime) {
|
||
await verifyAppImageNodeRuntimeServesPty(resourcesDir, appImageNodeRuntime)
|
||
}
|
||
} else {
|
||
// Cross-arch smoke cannot run, but missing unpacked entry is always a packaging error.
|
||
assertPackagedDaemonEntryExists(resourcesDir)
|
||
console.log(
|
||
`[verify-packaged-daemon-entry] skipped boot on cross-arch slice (target ${context.arch}, host ${process.arch})`
|
||
)
|
||
}
|
||
// Validate real electron-builder output so broken extraResources mappings fail before shipping.
|
||
verifyPackagedPluginResources(resourcesDir)
|
||
assertPackagedWebClient(resourcesDir)
|
||
assertPackagedServeSelectionEntry(resourcesDir)
|
||
finalizePackagedRipgrep(resourcesDir)
|
||
for (const filename of readdirSync(resourcesDir)) {
|
||
if (!filename.startsWith('agent-browser-')) {
|
||
continue
|
||
}
|
||
// Upstream executable bits vary (notably darwin-x64); restore them so packaged execFile works.
|
||
chmodSync(join(resourcesDir, filename), 0o755)
|
||
// The orcad template hashes this exact file next, so app signing must not rewrite it later.
|
||
if (context.electronPlatformName === 'darwin') {
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, filename),
|
||
filename,
|
||
context.packager,
|
||
resolve(__dirname, '../resources/build/entitlements.mac.plist')
|
||
)
|
||
}
|
||
}
|
||
await finalizePackagedOrcadTemplate(resourcesDir, {
|
||
platform: context.electronPlatformName,
|
||
signMacBinary: (path) =>
|
||
signMacStandaloneHelper(path, 'orcad template binary', context.packager)
|
||
})
|
||
chmodUnixCliLaunchers(resourcesDir, context.electronPlatformName)
|
||
if (context.electronPlatformName === 'darwin') {
|
||
await signMacComputerUseHelper(join(resourcesDir, 'Orca Computer Use.app'), context.packager)
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, '..', 'MacOS', 'orca-notification-status'),
|
||
'orca-notification-status',
|
||
context.packager
|
||
)
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, '..', 'MacOS', 'orca-keyboard-layout'),
|
||
'orca-keyboard-layout',
|
||
context.packager
|
||
)
|
||
await signMacStandaloneHelper(
|
||
join(resourcesDir, '..', 'MacOS', 'orca-process-footprint'),
|
||
'orca-process-footprint',
|
||
context.packager
|
||
)
|
||
await packageMacTerminalHost(join(resourcesDir, '..', '..'), context)
|
||
}
|
||
},
|
||
win: {
|
||
executableName: 'Orca',
|
||
// SignPath signs Windows installers after packaging, so set release publisherName explicitly.
|
||
// Unsigned dev channels omit it or updater signature checks reject dev updates and return to stable.
|
||
// Sign hook signs nothing: it relays transient NSIS uninstaller files to CI SignPath; unset relay env leaves local/dev builds inert.
|
||
signtoolOptions: {
|
||
sign: signWindowsUninstallerViaSignPath,
|
||
...(isWinDevChannel ? {} : { publisherName: 'SignPath Foundation' })
|
||
},
|
||
...(isWinDevChannel ? { verifyUpdateCodeSignature: false } : {}),
|
||
extraResources: [
|
||
...commonExtraResources,
|
||
...windowsRuntimeResources,
|
||
winSpeechNativeResource,
|
||
{
|
||
from: 'resources/win32/bin/orca.cmd',
|
||
to: 'bin/orca.cmd'
|
||
},
|
||
{
|
||
from: 'native/windows-cli-launcher/.build/orca.exe',
|
||
to: 'bin/orca.exe'
|
||
},
|
||
{
|
||
from: 'native/windows-cli-launcher/.build/orca-administrator-restart.exe',
|
||
to: 'bin/orca-administrator-restart.exe'
|
||
},
|
||
// Why resources/bin: SignPath signs every unsigned PE under win-unpacked, and the hook
|
||
// installer copies this signed file into ~/.orca/agent-hooks (#20913).
|
||
{
|
||
from: 'native/windows-agent-hook/.build/orca-agent-hook.exe',
|
||
to: 'bin/orca-agent-hook.exe'
|
||
},
|
||
{
|
||
from: 'node_modules/agent-browser/bin/agent-browser-win32-x64.exe',
|
||
to: 'agent-browser-win32-x64.exe'
|
||
},
|
||
{
|
||
from: 'native/computer-use-windows/runtime.ps1',
|
||
to: 'computer-use-windows/runtime.ps1'
|
||
},
|
||
featureWallResources
|
||
]
|
||
},
|
||
nsis: {
|
||
artifactName: 'orca-windows-setup.${ext}',
|
||
shortcutName: '${productName}',
|
||
uninstallDisplayName: '${productName}',
|
||
createDesktopShortcut: 'always',
|
||
// One NSIS include hosts update-guarded daemon cleanup and additive markdown Open With registration.
|
||
// Avoid fileAssociations: it would steal default .md ownership; see the include’s header.
|
||
include: resolve(__dirname, 'nsis', 'orca-installer-hooks.nsh')
|
||
},
|
||
mac: {
|
||
// Alternate rank preserves Finder’s default; one entry per extension because app-builder globs the value as a string.
|
||
fileAssociations: [
|
||
...MARKDOWN_FILE_EXTENSIONS.map((ext) => ({
|
||
ext,
|
||
name: 'Markdown Document',
|
||
description: 'Markdown Document',
|
||
role: 'Editor',
|
||
rank: 'Alternate'
|
||
})),
|
||
...TABULAR_FILE_EXTENSIONS.map((ext) => ({
|
||
ext,
|
||
name: `${ext.toUpperCase()} Document`,
|
||
description: `${ext.toUpperCase()} Document`,
|
||
role: 'Editor',
|
||
rank: 'Alternate'
|
||
}))
|
||
],
|
||
icon: 'resources/build/icon.icns',
|
||
entitlements: 'resources/build/entitlements.mac.plist',
|
||
entitlementsInherit: 'resources/build/entitlements.mac.plist',
|
||
...(isMacRelease ? { sign: resolve(__dirname, 'scripts/sign-mac-app.mjs') } : {}),
|
||
signIgnore: [
|
||
...bundledRipgrepMacSignIgnore,
|
||
...orcadTemplateMacSignIgnore,
|
||
// afterPack signs it before the orcad template hashes it.
|
||
'/Contents/Resources/agent-browser-darwin-[^/]+$',
|
||
...macTerminalHostSignIgnore
|
||
],
|
||
extendInfo: macUsageDescriptions,
|
||
// Local macOS builds need no release credentials; hardening/notarization stay on explicit release paths.
|
||
hardenedRuntime: isMacRelease,
|
||
// Dev notarization preserves TCC identifier/team identity across cdhash changes; unticketed hourly updates otherwise lose folder grants with EPERM and no prompt.
|
||
notarize: isMacRelease,
|
||
extraResources: [
|
||
...commonExtraResources,
|
||
...createPackagedRuntimeNodeModuleResources('darwin'),
|
||
macSpeechNativeResource,
|
||
{
|
||
from: 'resources/darwin/bin/orca',
|
||
to: 'bin/orca'
|
||
},
|
||
{
|
||
from: 'node_modules/agent-browser/bin/agent-browser-darwin-${arch}',
|
||
to: 'agent-browser-darwin-${arch}'
|
||
},
|
||
{
|
||
from: 'native/computer-use-macos/.build/release/Orca Computer Use.app',
|
||
to: 'Orca Computer Use.app'
|
||
},
|
||
featureWallResources
|
||
],
|
||
// macOS 26 UNUserNotificationCenter aborts helpers launched from Resources; execute from Contents/MacOS (#7929).
|
||
extraFiles: [
|
||
{
|
||
from: 'native/notification-status-macos/.build/release/orca-notification-status',
|
||
to: 'MacOS/orca-notification-status'
|
||
},
|
||
{
|
||
from: 'native/keyboard-layout-macos/.build/release/orca-keyboard-layout',
|
||
to: 'MacOS/orca-keyboard-layout'
|
||
},
|
||
{
|
||
from: 'native/process-footprint-macos/.build/release/orca-process-footprint',
|
||
to: 'MacOS/orca-process-footprint'
|
||
}
|
||
],
|
||
target: [
|
||
{
|
||
target: 'dmg',
|
||
arch: ['x64', 'arm64']
|
||
},
|
||
{
|
||
target: 'zip',
|
||
arch: ['x64', 'arm64']
|
||
}
|
||
]
|
||
},
|
||
// Why: release builds should fail if signing is unavailable instead of
|
||
// silently downgrading to ad-hoc artifacts that look shippable in CI logs.
|
||
forceCodeSigning: isMacRelease,
|
||
dmg: {
|
||
artifactName: 'orca-macos-${arch}.${ext}'
|
||
},
|
||
linux: {
|
||
// shared-mime-info already maps md/markdown: advertise handler only, leaving mimeapps.list defaults intact.
|
||
// Omit mdx because Ubuntu 24.04 maps it to application/x-genesis-32x-rom; claiming it would need a glob override.
|
||
mimeTypes: ['text/markdown', 'text/csv', 'text/tab-separated-values'],
|
||
// Avoid GNOME Orca’s orca package and /usr/bin/orca names.
|
||
executableName: 'orca-ide',
|
||
// Use icns to generate hicolor sizes; some Linux docks ignore a lone 1024px PNG.
|
||
icon: 'resources/build/icon.icns',
|
||
desktop: {
|
||
entry: {
|
||
// Why: Electron reports WM_CLASS=orca for the visible Linux window;
|
||
// GNOME docks need an exact match to group it with orca-ide.desktop.
|
||
StartupWMClass: 'orca'
|
||
}
|
||
},
|
||
extraResources: [
|
||
...commonExtraResources,
|
||
...createPackagedRuntimeNodeModuleResources('linux'),
|
||
linuxSpeechNativeResource,
|
||
{
|
||
from: 'native/wayland-clipboard/.build/${arch}/orca-wayland-clipboard',
|
||
to: 'bin/orca-wayland-clipboard'
|
||
},
|
||
{
|
||
from: 'resources/linux/bin/orca-ide',
|
||
to: 'bin/orca-ide'
|
||
},
|
||
{
|
||
from: 'node_modules/agent-browser/bin/agent-browser-linux-${arch}',
|
||
to: 'agent-browser-linux-${arch}'
|
||
},
|
||
{
|
||
from: 'native/computer-use-linux/runtime.py',
|
||
to: 'computer-use-linux/runtime.py'
|
||
},
|
||
featureWallResources
|
||
],
|
||
// Keep local artifacts aligned with the release pipeline.
|
||
target: ['AppImage', 'deb', 'rpm'],
|
||
maintainer: 'stablyai',
|
||
category: 'Utility'
|
||
},
|
||
appImage: {
|
||
artifactName: isLinuxArm64Release ? 'orca-linux-arm64.${ext}' : 'orca-linux.${ext}'
|
||
},
|
||
deb: {
|
||
packageName: 'orca-ide',
|
||
artifactName: 'orca-ide_${version}_${arch}.${ext}',
|
||
// Headless Chromium still needs display service; orca serve starts packaged Xvfb when available (ensure-virtual-display.ts).
|
||
depends: [
|
||
...debElectronRuntimeDependencies,
|
||
'python3',
|
||
'python3-gi',
|
||
'gir1.2-atspi-2.0',
|
||
'at-spi2-core',
|
||
'xdotool',
|
||
'xclip',
|
||
'xvfb'
|
||
],
|
||
// Install CLI onto PATH for headless hosts where GUI-triggered CliInstaller can never run.
|
||
afterInstall: 'resources/linux/packaging/after-install.sh',
|
||
afterRemove: 'resources/linux/packaging/after-remove.sh',
|
||
// Why: only an AppImage's files can vanish under a running daemon (see appimage-node-runtime.cjs).
|
||
fpm: appImageNodeRuntimeFpmArgs
|
||
},
|
||
rpm: {
|
||
packageName: 'orca-ide',
|
||
artifactName: 'orca-ide-${version}.${arch}.${ext}',
|
||
// Why: see deb depends. RPM distros ship Xvfb as xorg-x11-server-Xvfb (there
|
||
// is no `xvfb` package), so the name differs from the deb here.
|
||
depends: [
|
||
...rpmElectronRuntimeDependencies,
|
||
'python3',
|
||
'python3-gobject',
|
||
'xdotool',
|
||
'xclip',
|
||
'xorg-x11-server-Xvfb'
|
||
],
|
||
// Why: same headless CLI-on-PATH registration as deb; rpm runs these via fpm.
|
||
afterInstall: 'resources/linux/packaging/after-install.sh',
|
||
afterRemove: 'resources/linux/packaging/after-remove.sh',
|
||
// Why: only an AppImage's files can vanish under a running daemon (see appimage-node-runtime.cjs).
|
||
fpm: appImageNodeRuntimeFpmArgs
|
||
},
|
||
beforeBuild: electronBuilderNativeRebuild,
|
||
// Dual-arch builds need target node-pty rebuilds or Intel Macs get arm64 binaries and posix_spawnp failure.
|
||
// beforeBuild performs targeted rebuild then returns false to skip optional cpu-features.
|
||
npmRebuild: true,
|
||
publish: {
|
||
provider: 'github',
|
||
owner: 'stablyai',
|
||
repo: devChannelRepo ?? 'orca',
|
||
// Keep main-repo releases draft until release-cut verifies all assets; early --publish always would expose missing Windows installers.
|
||
releaseType: devChannelRepo ? 'prerelease' : 'draft'
|
||
}
|
||
}
|
||
|
||
// Stamp the effective channel version where node-mode CLI code can read it.
|
||
function stampPackagedCliVersion(resourcesDir, version) {
|
||
const packageJsonPath = join(resourcesDir, 'app.asar.unpacked', 'out', 'package.json')
|
||
if (!existsSync(packageJsonPath)) {
|
||
throw new Error(`Missing unpacked CLI package boundary: ${packageJsonPath}`)
|
||
}
|
||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
||
writeFileSync(packageJsonPath, `${JSON.stringify({ ...packageJson, version }, null, 2)}\n`)
|
||
}
|
||
|
||
function chmodUnixCliLaunchers(resourcesDir, electronPlatformName) {
|
||
if (electronPlatformName === 'win32') {
|
||
return
|
||
}
|
||
for (const launcherName of ['orca', 'orca-ide']) {
|
||
const launcherPath = join(resourcesDir, 'bin', launcherName)
|
||
if (!existsSync(launcherPath)) {
|
||
continue
|
||
}
|
||
// Public shell commands need executable extraResources modes despite source/packager drift.
|
||
chmodSync(launcherPath, 0o755)
|
||
}
|
||
}
|
||
|
||
async function signMacComputerUseHelper(helperAppPath, packager) {
|
||
if (!existsSync(helperAppPath)) {
|
||
if (isMacRelease) {
|
||
throw new Error(`Missing Orca Computer Use helper app at ${helperAppPath}`)
|
||
}
|
||
return
|
||
}
|
||
const codeSigningInfo =
|
||
isMacRelease && process.env.CSC_LINK && packager?.codeSigningInfo?.value
|
||
? await packager.codeSigningInfo.value
|
||
: null
|
||
const identity =
|
||
process.env.ORCA_COMPUTER_MACOS_SIGN_IDENTITY ??
|
||
process.env.CSC_NAME ??
|
||
findInstalledMacSigningIdentity(codeSigningInfo?.keychainFile) ??
|
||
(isMacRelease ? null : '-')
|
||
if (!identity) {
|
||
throw new Error('Missing signing identity for Orca Computer Use helper app')
|
||
}
|
||
// Why: TCC grants attach to this nested app's code identity. Sign it before
|
||
// the outer Orca.app is sealed so production builds preserve that identity.
|
||
execFileSync('codesign', codesignArgs(identity, helperAppPath), { stdio: 'inherit' })
|
||
execFileSync('codesign', ['--verify', '--deep', '--strict', helperAppPath], {
|
||
stdio: 'inherit'
|
||
})
|
||
}
|
||
|
||
// electron-builder `Arch` enum: x64=1, arm64=3. A universal (4) slice has no single arch.
|
||
function macSliceArch(archEnum, purpose) {
|
||
const arch = { 1: 'x64', 3: 'arm64' }[archEnum]
|
||
if (!arch) {
|
||
throw new Error(`Unsupported ${purpose} architecture: ${archEnum}`)
|
||
}
|
||
return arch
|
||
}
|
||
|
||
// Why after the node_modules prune: the helper copies this slice's node-pty.
|
||
async function packageMacTerminalHost(appPath, context) {
|
||
const arch = macSliceArch(context.arch, 'macOS terminal host')
|
||
await buildMacTerminalHost({
|
||
appPath,
|
||
arch,
|
||
iconPath: resolve(__dirname, '../resources/build/icon.icns'),
|
||
entitlementsPath: resolve(__dirname, '../resources/build/entitlements.mac.plist'),
|
||
usageDescriptions: macUsageDescriptions,
|
||
signCode: (path, { entitlements }) =>
|
||
signMacStandaloneHelper(path, 'terminal host', context.packager, entitlements)
|
||
})
|
||
// Only a local build may skip a slice this host cannot run; a shipped slice must serve a PTY.
|
||
await verifyPackagedMacTerminalHostBoots(appPath, {
|
||
arch,
|
||
required: isMacRelease || process.env.CI === 'true'
|
||
})
|
||
await verifyMacTerminalHost({
|
||
appPath,
|
||
arch,
|
||
usageDescriptions: macUsageDescriptions,
|
||
expectedTeamId: isMacRelease ? ORCA_MAC_TEAM_ID : null
|
||
})
|
||
}
|
||
|
||
async function signMacStandaloneHelper(helperPath, helperName, packager, entitlements) {
|
||
if (!existsSync(helperPath)) {
|
||
if (isMacRelease) {
|
||
throw new Error(`Missing ${helperName} helper at ${helperPath}`)
|
||
}
|
||
return
|
||
}
|
||
const codeSigningInfo =
|
||
isMacRelease && process.env.CSC_LINK && packager?.codeSigningInfo?.value
|
||
? await packager.codeSigningInfo.value
|
||
: null
|
||
const identity =
|
||
process.env.CSC_NAME ??
|
||
findInstalledMacSigningIdentity(codeSigningInfo?.keychainFile) ??
|
||
(isMacRelease ? null : '-')
|
||
if (!identity) {
|
||
throw new Error(`Missing signing identity for ${helperName} helper`)
|
||
}
|
||
// Why: nested executables must be signed before the outer app bundle is sealed.
|
||
const args = ['--force', '--sign', identity]
|
||
if (isMacRelease) {
|
||
args.push('--options', 'runtime', '--timestamp')
|
||
}
|
||
if (entitlements) {
|
||
args.push('--entitlements', entitlements)
|
||
}
|
||
args.push(helperPath)
|
||
execFileSync('codesign', args, { stdio: 'inherit' })
|
||
execFileSync('codesign', ['--verify', '--strict', helperPath], { stdio: 'inherit' })
|
||
}
|
||
|
||
function codesignArgs(identity, targetPath) {
|
||
const args = ['--force', '--deep', '--sign', identity]
|
||
if (isMacRelease) {
|
||
args.push(
|
||
'--options',
|
||
'runtime',
|
||
'--timestamp',
|
||
'--entitlements',
|
||
resolve(__dirname, '../resources/build/entitlements.computer-use.mac.plist')
|
||
)
|
||
}
|
||
args.push(targetPath)
|
||
return args
|
||
}
|
||
|
||
function findInstalledMacSigningIdentity(keychainFile) {
|
||
try {
|
||
const output = execFileSync(
|
||
'security',
|
||
['find-identity', '-v', '-p', 'codesigning', ...(keychainFile ? [keychainFile] : [])],
|
||
{
|
||
encoding: 'utf8'
|
||
}
|
||
)
|
||
const releaseMatch =
|
||
output.match(/"([^"]*Developer ID Application:[^"]+)"/) ??
|
||
output.match(/"([^"]*Apple Distribution:[^"]+)"/)
|
||
if (releaseMatch?.[1]) {
|
||
return releaseMatch[1]
|
||
}
|
||
if (!isMacRelease) {
|
||
return output.match(/"([^"]*Apple Development:[^"]+)"/)?.[1] ?? null
|
||
}
|
||
} catch {}
|
||
return null
|
||
}
|