mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* docs(windows): record the measured MSYS job-breakaway mechanism The per-PTY job already denies JOB_OBJECT_LIMIT_BREAKAWAY_OK for Cygwin/MSYS shells (#19068), but nothing records why, and a conpty.node built before that commit fails windows-msys-job.win32.test.ts in a way that reads as a source defect. Measured on a real Windows 11 host: both the plain and the exec- replacement Git Bash shapes leak, the escape is the MSYS runtime's own spawn/exec (fork keeps membership), and a single-variable A/B on usesCygwinRuntime flips the result 0/2 -> 4/4. Also names the gap the failure hid behind: node-pty-job-ownership.cjs asserts symbol presence, which cannot distinguish patch revisions. * fix(windows): reject a node-pty addon that predates the MSYS breakaway denial The native-runtime gate asserted only that terminateJob, listJobProcessIds and assignCurrentProcessToJob were exported. All three predate the Cygwin/MSYS breakaway denial, so an addon built before it passes every gate, isPtyJobOwnershipAvailable() returns true, and windows-pty-job.win32.test.ts passes 6/6 -- while every Git Bash child is created outside its pane's job and survives terminatePtyJob. Read the resolved .node and require the wide msys-2.0.dll literal that usesCygwinRuntime holds, the way stagedRelayAddonIsUnpatched() already tells a patched windows-process-tree addon from a published one. An addon the caller cannot name is refused rather than skipped: a gate that cannot see its subject is not a gate. Verified against real binaries on a Windows 11 host: the shared checkout's pre-#19068 build errors, a build from current patched source passes, a missing path errors. Also closes the cross-host packaging skip. The export half has to load the addon so it cannot run when the packaging host is not the target, which is how a Windows release built elsewhere could ship this. The marker is a file read and needs neither; an unrecognised layout warns rather than fails a release that was packaging fine. * fix(windows): check the MSYS breakaway denial on the rebuild path too The Electron probe carried the marker check, but it lives inside probeElectronNativeModules, which returns early whenever the Electron package binary is unusable. Covered by another path is not this path checks -- and the defect this whole change closes was a gate that looked like it checked. Reading the binary needs neither a loadable Electron nor an executable target arch, so assert it after the rebuild, beside the windows-process-tree assertion that exists for the same reason: this is the addon copied into the packaged app. Absent warns (a cross-platform rebuild need not leave a win32 addon on this disk); present and unmarked is fatal. The fixtures now write a real addon file, because the gate reads the binary it was told about rather than trusting the exports. Verified against the two real binaries measured on the Windows host: the pre-#19068 build fails this path, the build from current patched source passes. * fix(windows): check the marker on every ConPTY path the packaged app can load The packaged marker check read one hard-coded path, `build/Release/conpty.node`, and warned when it was absent. `loadNativeModule` tries `build/Release`, then `build/Debug`, then `prebuilds/win32-<arch>`, swallowing each failure, and `prunePackagedNodePty` drops the published prebuild only when a same-arch `build/Release` exists to replace it. So the two packages the check was added for were the two it could not see: - cross-host: no host but Windows can build conpty.node, so there is no `build/Release` and the prebuild is what ships. The check warned and returned. - cross-arch: `build/Release` is the packaging host's own arch, patched and marked, so the check printed OK -- while the target app cannot load it and falls through to the unmarked prebuild underneath. Measured, not assumed: both published Windows prebuilds in the node-pty tarball contain neither `msys-2.0.dll` nor `cygwin1.dll` in any encoding. They are the binary that leaks every MSYS pane child out of its job. It now sweeps every candidate present for the *target* arch and refuses a package with no candidate at all, which is a package with no ConPTY backend rather than a layout to shrug at. It runs for every Windows slice instead of only the branch the export check skips, so deleting the export check cannot silently take it too. A stale source build keeps the rebuild advice; the prebuild gets the advice that actually works, which is to package the slice on a Windows host of that arch. Also: the marker constant was re-typed in four places and was tied to the C++ literal that produces it by nothing at all, so editing the patch would have left a gate that fails every correctly rebuilt addon and tells the developer to do the one thing that cannot help. The fixtures now take the constant from the gate, and a test asserts the patch still adds `L"msys-2.0.dll"` to conpty.cc. And the rebuild path treated a missing addon as a warning even on the host that will run the install, where node-pty would fall through to that same prebuild. The verdict is now a value, so it is tested without a platform gate. * fix(windows): resolve the packaged ConPTY the way its loader does Sweeping every candidate and demanding the marker on all of them was wrong in the one case it was meant to make safe. `beforeBuild` runs `rebuild-native-deps.mjs --platform=win32 --arch=<target>`, so a cross-arch slice normally does get a patched `build/Release` for the target; `prunePackagedNodePty` keeps the prebuild anyway because its guard is `electronArch === process.arch` rather than the arch of the binary. That package is correct and its leftover prebuild is never reached, and the sweep failed it -- telling whoever ran it to package on a Windows arm64 host, which is both the wrong remedy and one no runner here can offer. Presence cannot separate that package from the one whose cross-arch rebuild quietly emitted the host's architecture, because the only difference is the arch of `build/Release`. So the gate now resolves the addon the way `loadNativeModule` does -- first candidate whose PE `IMAGE_FILE_HEADER.Machine` matches the target, walking root-then-lib for each layout in node-pty's own order -- and checks the marker on the one that will actually run. A package with no candidate, or none of the target's architecture, is refused: it has no ConPTY backend either way, and the second is exactly what a silently host-arch cross-build looks like. The PE machine reader already existed, privately, in the relay addon builder that needed the same "a cross-build cannot silently emit host arch" guarantee. It is now shared rather than copied. Two seams were unreachable from anything but Windows, so nothing tested them: - the afterPack hook's win32 block was an inline if/else that only a source-text assertion could inspect, and that assertion could not tell the difference between the check running and the check being wrapped in `try {} catch {}`. It is now `verifyPackagedWindowsNodePty`, and "the marker check runs even where the export check cannot" is four spied assertions instead of a string match. - the rebuild path's verdict read `process` directly, so the branch that fires only on the host being rebuilt for was dead on every other host. It now takes the host as arguments, and the fs checks, the warning and the failure are all exercised from macOS. Fixtures write a real PE header rather than `MZ fake addon`, since the gate now reads one. The machine table is pinned to the documented IMAGE_FILE_MACHINE values, because every fixture builds its header from that table and a table wrong in both entries would otherwise agree with itself. * fix(windows): say why the packaged ConPTY fell back, not just that it did The previous commit resolved the addon by architecture but still had one message for every way the resolution could land on the published prebuild. Those ways want opposite remedies, and the one it printed was the remedy the commit before it had just called wrong: - no source build in the package at all — the slice has to be built somewhere that can build node-pty for the target arch. - a source build that is there but is the packaging host's architecture, because the cross-arch rebuild did not honour `--arch` — re-running that rebuild is the fix, and "package on a Windows arm64 host" is neither necessary nor possible. The second is the common one, since node-pty publishes a prebuild for both Windows arches and prune keeps the target's on every cross-arch package. So the old text fired mostly on the case it described least. It now reports which source builds were skipped and the machine field each carried, and names the rebuild command. "Nothing the target can load" had the same problem in reverse: a zero-length or truncated `conpty.node` got a cross-architecture diagnosis. Every candidate is now named with what was actually read, including "not a PE image". The rebuild path asserts the architecture too. A rebuild that ignored `--arch` was otherwise only visible at packaging, two steps from the command that fixes it. Arches with no known machine value are left unjudged rather than guessed at. Two things the extraction broke or nearly broke, both found by mutation: - the shared PE reader answers `null` where the relay builder's private copy returned a number, which would have turned its "node-gyp ignored --arch" error into a `TypeError`. Both callers now go through `describePeMachine`. - the rebuild fixtures stage a script's co-located modules by walking its imports, and the walker only understood `from '...'` — so the gate's new `require('./windows-pe-machine.cjs')` was left behind and every subprocess test failed with a resolution error, which is the exact failure its own comment warns about. It now follows `require` and bare side-effect `import` as well, and has tests; the fixture stages the gate by walking it rather than by naming one file. Fixtures write real PE headers through one shared builder instead of three hand-rolled ones. * fix(windows): run the node-pty addon gates on the Windows job that can `rebuild-native-deps-node-pty.test.mjs` carries four `skipIf(platform !== 'win32')` tests. The full suite runs on ubuntu, and the Windows PR job runs an explicit file list that never named this file -- so those tests were skipped on Linux and never reached anywhere else. Three of them predate this branch. The Windows job is added the four node-pty addon suites plus the module-walker one; the comment above that list already says why it is the right place, which is that the addon assertions only hold once natives have been rebuilt. Running the path-joining suites there also covers the separator this gate's candidate list is built from. The rest is round-three review: - the rebuild-time arch assertion told a reader "node-gyp did not honour --arch" about a file that was not a PE image at all, which is a truncated or quarantined artifact and a different command to run. The two now read differently, and neither claims the other's cause. Same fix the packaged gate had one commit ago, in the place that had not had it yet. - the missing-addon error said node-pty "would load" a prebuild without checking it is there. It says "fall through to" now, which is true either way. - `isLoadableByArch` had no caller left once the packaged gate started needing the raw machine field for its message. Removed rather than kept warm. - each candidate's header is read once instead of up to three times. - the module walker's comment claimed every shape that reaches a co-located module; it does not follow `projectRequire`/`requireLocal`, and it must not -- those specifiers resolve against the project root, so following one stages the wrong path and the copy fails. Proven by trying: widening the pattern to require-shaped names broke nine tests on `projectRequire('./config/scripts/...')`. The comment now says what it follows and why it stops there. - a new test resolved a file URL with `.pathname`, which keeps the drive-letter slash on Windows -- the very job this commit adds it to. * docs(windows): put the superseded export-only gate in the past tense It describes what used to pass a broken addon, so present tense reads as a description of the gate the same document then explains replacing it. * fix(windows): repair what running the node-pty suites on Windows exposed Putting these files on the Windows job turned four assertions red on the first run. Three of them were in tests that carried `skipIf(platform !== 'win32')` and had therefore never executed anywhere, on any branch. - `writeFakeElectronRebuild` emitted the `windows-process-tree` addon a real rebuild leaves but never node-pty's, so every Windows test of the rebuild path ran against a tree no real rebuild can produce: node-pty "rebuilt" with nothing in `build/Release`. The new same-host check reads that state correctly and said so. The fake rebuild now writes `build/Release/conpty.node` when it was asked to rebuild node-pty for win32, with the marker and the target machine. - `mkTempProject` never staged `windows-process-tree-creation-time.cjs`. The rebuild script reaches it through `projectRequire`, which resolves against the project root, so the module walker cannot follow it and must not try. Staged by name, with a comment saying which of the two it is. Without it the windows-process-tree probe failed to load its own checker and the module joined `modulesToRebuild`, which is the second and third red assertion. - the two `nodePtyAddonPath` cases compared against a literal POSIX string. `resolve` returns a drive letter and backslashes on Windows, so they could only ever pass off it. Built from segments now, which still pins the `..` traversal that is the point of the test. Verified on macOS: ensure-native-runtime-job-ownership, verify-packaged-node-pty-job-ownership, windows-pe-machine, script-module-dependencies, rebuild-native-deps-node-pty, rebuild-native-deps, rebuild-native-deps-windows-process-tree, ensure-native-runtime -- 109 passed, 6 skipped. The 6 are the Windows-gated rebuild tests, which is the job this change is aimed at; Windows CI is the arbiter. * fix(windows): give the packaged fallback a third verdict, for a file that is no image The packaged gate had two remedies for landing on the published prebuild and picked between them on `!prebuilt`, which puts a truncated, empty or quarantined `build/Release/conpty.node` in the cross-arch bucket: "the source build beside it is the wrong architecture ... re-run with --arch". It is not the wrong architecture, it is not an architecture, and `--arch` is not the command. The rebuild-path gate was split for exactly this a commit ago; this is the same split in the place that had not had it. Also from review of the settled state: - the stale-source-build branch ended in a call that happened to throw, so a reader could not see it was terminal and the file was read twice to get there. The verdict is now an Error the caller throws, built once from the read it already did, and shared with `assertCygwinBreakawayDenied` rather than copied. - four injection seams had no consumer in production or in tests (`deniesBreakaway`, `peMachine`, and `exists`/`peMachine` on the rebuild verdict). An unused seam is a way for the tested path and the real one to drift apart; the tests drive both with real files. Removed. - the loader table existed in a docblock and in the reference doc, already disagreeing about row four. The docblock cites the doc now. - `peImage` stamped machine `0x0000` for an arch it had no value for, because `writeUInt16LE(undefined)` coerces to zero. A fixture that quietly invents the field the gates read is the same species of silent lie the gates exist to catch; it throws, and a test holds it to that. - a test named for refusing an unreadable candidate asserted only that something threw. Renamed to what it proves. * fix(windows): make the rebuild fixtures represent a tree that can exist Second round of what running these suites on Windows exposed. The module the walker could not stage is now staged, so the probe reached its own checker and the real reasons surfaced: - `writeFakeWindowsProcessTree` exported `{}`. The creation-time gate reads `supportedProcessDataFlags` off the addon and calls its absence "the tarball prebuilt, not a build of the patched source" — correctly. The fixture predates that gate and, being Windows-only, never met it. The healthy fake now reports the flag, taken from the gate's own constant. Two tests were failing on this, the second only because the module then joined `modulesToRebuild`. - `rebuilds a loadable ConPTY native that lacks Orca job ownership` asked for a node-pty rebuild in a tree where node-pty had none of the payload its package ships. It gets `writeFakeNodePtyConptyPayload` like its two siblings. I also tried making the fake rebuild emit `build/Release/conpty.node` the way a real one does, and backed it out: `restoreNodePtyWindowsConptyRuntime` keys off that file and then reads `third_party/conpty`, so emitting it in a tree without the package payload turns one honest gap into an ENOENT two steps away. The payload fixture is where "node-pty has its addon" belongs. macOS: ensure-native-runtime-job-ownership, verify-packaged-node-pty-job-ownership, windows-pe-machine, script-module-dependencies, rebuild-native-deps-node-pty, rebuild-native-deps, rebuild-native-deps-windows-process-tree, ensure-native-runtime — 112 passed, 6 skipped. The 6 are the Windows-gated rebuild tests; Windows CI is the arbiter and is why they are on that job now. * fix(windows): register the node-pty addon suites in the scope list too Putting the five suites in the Windows lane's vitest argv gets them run once the job starts; `WINDOWS_PACKAGE_TESTS` in `pr-code-change-scope.mjs` is what decides whether the job starts at all. Only the argv was updated, so a PR touching just `rebuild-native-deps-node-pty.test.mjs` would not have started the Windows job, and its four Windows-only cases — including the same-host-absent one added here — would have run on no machine for that PR. Exactly the shape of gap this branch is about. Both lists now name all five, and `windows-pe-machine`, `windows-pe-image-fixture` and `script-module-dependencies` join `NATIVE_RUNTIME_PREFIXES` so a change to the modules themselves starts it too. `win32-test-lane-registration.test.mjs` exists to catch precisely this and did not, because its matcher only recognises suite-level gates (`describe.runIf` / `describe.skipIf`) and a `.win32.` filename. These tests gate per `it`. Widening it is not this branch's change to make: about thirty files across the repo carry per-`it` Windows gates and are unregistered, so the ratchet would move far beyond node-pty. Flagged rather than done. Message repairs from the same review: - the non-PE arm of the rebuild-time arch error read "... is not a PE image, so nothing can load it, so node-pty would fall back ...". The shared consequence clause already opens with ", so". - the no-source-build packaging error ended "Package this Windows slice on such a host", which is wrong advice for the case where the host IS such a host and the rebuild simply left nothing — reachable when the artifact is removed before prune runs. It now names both readings and points at the beforeBuild output. - the relay-addon builder blamed `--arch` for a build output that is not a PE at all, the same guess the node-pty gate was taught to stop making. - the patch-drift assertion was a bare `toBe(true)`, so a real drift read as "expected false to be true". It now names the two things that can have drifted and what happens until they agree.
620 lines
23 KiB
JavaScript
620 lines
23 KiB
JavaScript
const {
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
realpathSync,
|
|
rmSync
|
|
} = require('node:fs')
|
|
const { dirname, join, resolve } = require('node:path')
|
|
const { builtinModules, createRequire } = require('node:module')
|
|
|
|
const projectDir = resolve(__dirname, '..')
|
|
const requireFromProject = createRequire(join(projectDir, 'package.json'))
|
|
|
|
const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
|
|
'@anthropic-ai/claude-agent-sdk',
|
|
'@electron-toolkit/utils',
|
|
'@linear/sdk',
|
|
'@parcel/watcher',
|
|
'electron-updater',
|
|
'i18next',
|
|
'jsonc-parser',
|
|
'node-pty',
|
|
'posthog-node',
|
|
'proper-lockfile',
|
|
// serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too.
|
|
'serve-sim',
|
|
'qrcode',
|
|
'ssh2',
|
|
'tweetnacl',
|
|
'ws',
|
|
'yaml',
|
|
'zod'
|
|
]
|
|
const WINDOWS_PACKAGED_RUNTIME_PACKAGE_ROOTS = [
|
|
'@vscode/windows-process-tree',
|
|
'@orca/windows-registry'
|
|
]
|
|
|
|
const NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM = {
|
|
darwin: 'darwin-',
|
|
linux: 'linux-',
|
|
win32: 'win32-'
|
|
}
|
|
const NODE_PTY_CONPTY_RUNTIME_FILES = ['conpty.dll', 'OpenConsole.exe']
|
|
const PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM = {
|
|
darwin: 'watcher-darwin',
|
|
linux: 'watcher-linux',
|
|
win32: 'watcher-win32'
|
|
}
|
|
const ELECTRON_ARCHITECTURE_BY_ENUM = {
|
|
0: 'ia32',
|
|
1: 'x64',
|
|
2: 'arm',
|
|
3: 'arm64',
|
|
4: 'universal'
|
|
}
|
|
const PACKAGED_NATIVE_ARCHITECTURES = new Set(['ia32', 'x64', 'arm', 'arm64'])
|
|
const PACKAGED_MAIN_REQUIRED_FILES = [
|
|
'out/main/index.js',
|
|
'out/main/agent-hooks/managed-agent-hook-controls.js'
|
|
]
|
|
const PACKAGED_MAIN_SOURCE_RE = /^out\/main\/.+\.js$/
|
|
const TYPE_DECLARATION_ARTIFACT_RE = /\.d\.(?:c|m)?ts(?:\.map)?$/
|
|
const JS_SOURCE_MAP_ARTIFACT_RE = /\.(?:c|m)?js\.map$/
|
|
const VERSIONED_ONNXRUNTIME_DYLIB_RE = /^libonnxruntime\.\d[\d.]*\.dylib$/
|
|
|
|
const NODE_BUILTINS = new Set([
|
|
...builtinModules,
|
|
...builtinModules.map((moduleName) => `node:${moduleName}`)
|
|
])
|
|
|
|
function packageNameFromSpecifier(specifier) {
|
|
if (specifier.startsWith('@')) {
|
|
const [scope, name] = specifier.split('/')
|
|
return scope && name ? `${scope}/${name}` : specifier
|
|
}
|
|
return specifier.split('/')[0]
|
|
}
|
|
|
|
function isPackagedExternalSpecifier(specifier) {
|
|
return (
|
|
!specifier.startsWith('.') &&
|
|
!specifier.startsWith('/') &&
|
|
specifier !== 'electron' &&
|
|
!NODE_BUILTINS.has(specifier)
|
|
)
|
|
}
|
|
|
|
function resolvePackageJsonPath(packageName, fromDir = projectDir) {
|
|
const nested = join(fromDir, 'node_modules', packageName, 'package.json')
|
|
if (existsSync(nested)) {
|
|
return nested
|
|
}
|
|
// Why: published serve-sim has no "." export (only ./middleware and ./state), so
|
|
// require.resolve('serve-sim') fails even though the package is present for bridge exec.
|
|
if (packageName === 'serve-sim') {
|
|
const direct = join(projectDir, 'node_modules', 'serve-sim', 'package.json')
|
|
if (existsSync(direct)) {
|
|
return direct
|
|
}
|
|
}
|
|
try {
|
|
return requireFromProject.resolve(`${packageName}/package.json`, { paths: [fromDir] })
|
|
} catch {
|
|
let entryPath
|
|
try {
|
|
entryPath = requireFromProject.resolve(packageName, { paths: [fromDir] })
|
|
} catch {
|
|
throw new Error(`Could not resolve package ${packageName} from ${fromDir}`)
|
|
}
|
|
let dir = dirname(entryPath)
|
|
while (dir !== dirname(dir)) {
|
|
const packageJsonPath = join(dir, 'package.json')
|
|
if (existsSync(packageJsonPath)) {
|
|
return packageJsonPath
|
|
}
|
|
dir = dirname(dir)
|
|
}
|
|
throw new Error(`Could not find package.json for ${packageName}`)
|
|
}
|
|
}
|
|
|
|
function readPackage(packageName, fromDir = projectDir) {
|
|
const packageJsonPath = resolvePackageJsonPath(packageName, fromDir)
|
|
const packageDir = realpathSync(dirname(packageJsonPath))
|
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
return {
|
|
name: packageJson.name ?? packageName,
|
|
packageDir,
|
|
dependencies: Object.keys(packageJson.dependencies ?? {})
|
|
}
|
|
}
|
|
|
|
function isKnownOmittedServeSimDependency(packageName, fromDir) {
|
|
if (packageName !== 'inspect-webkit') {
|
|
return false
|
|
}
|
|
const serveSimPackageJsonPath = join(projectDir, 'node_modules', 'serve-sim', 'package.json')
|
|
if (!existsSync(serveSimPackageJsonPath)) {
|
|
return false
|
|
}
|
|
try {
|
|
return realpathSync(fromDir) === realpathSync(dirname(serveSimPackageJsonPath))
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function collectPackagedRuntimePackages(electronPlatformName = process.platform) {
|
|
const packages = new Map()
|
|
const visit = (packageName, fromDir = projectDir) => {
|
|
if (packageName === 'electron' || packages.has(packageName)) {
|
|
return
|
|
}
|
|
|
|
let packageInfo
|
|
try {
|
|
packageInfo = readPackage(packageName, fromDir)
|
|
} catch (error) {
|
|
// Why: serve-sim declares inspect-webkit, but current installs omit it.
|
|
// Keep that escape hatch narrow so broken packages still fail packaging.
|
|
if (isKnownOmittedServeSimDependency(packageName, fromDir)) {
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
if (packages.has(packageInfo.name)) {
|
|
return
|
|
}
|
|
packages.set(packageInfo.name, packageInfo.packageDir)
|
|
|
|
for (const dependencyName of packageInfo.dependencies) {
|
|
visit(dependencyName, packageInfo.packageDir)
|
|
}
|
|
}
|
|
|
|
// Why: cross-builds must select native dependencies from the artifact target, not the build host.
|
|
const packageRoots = [
|
|
...PACKAGED_RUNTIME_PACKAGE_ROOTS,
|
|
...(electronPlatformName === 'win32' ? WINDOWS_PACKAGED_RUNTIME_PACKAGE_ROOTS : [])
|
|
]
|
|
for (const packageName of packageRoots) {
|
|
visit(packageName)
|
|
}
|
|
|
|
// Why: @parcel/watcher loads its native .node addon from a platform-specific
|
|
// optionalDependency (e.g. @parcel/watcher-linux-x64-glibc) that the
|
|
// dependencies graph above never reaches. Include the ones installed for the
|
|
// build's supported architectures; afterPack pruning trims non-target
|
|
// platform/architecture variants. Without this the packaged main bundle's import of
|
|
// '@parcel/watcher' resolves at runtime but throws loading its binary.
|
|
const parcelWatcherDir = packages.get('@parcel/watcher')
|
|
if (parcelWatcherDir) {
|
|
const parcelWatcherPackage = JSON.parse(
|
|
readFileSync(join(parcelWatcherDir, 'package.json'), 'utf8')
|
|
)
|
|
for (const optionalName of Object.keys(parcelWatcherPackage.optionalDependencies ?? {})) {
|
|
try {
|
|
visit(optionalName)
|
|
} catch {
|
|
// Optional platform subpackage is not installed for this build; skip it.
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...packages.entries()].sort(([left], [right]) => left.localeCompare(right))
|
|
}
|
|
|
|
function createPackagedRuntimeNodeModuleResources(electronPlatformName = process.platform) {
|
|
return collectPackagedRuntimePackages(electronPlatformName).map(([packageName, packageDir]) => ({
|
|
from: packageDir,
|
|
to: join('node_modules', ...packageName.split('/'))
|
|
}))
|
|
}
|
|
|
|
function normalizeAsarEntryPath(entry) {
|
|
return entry.replace(/\\/g, '/').replace(/^\/+/, '')
|
|
}
|
|
|
|
function findAsarEntry(entries, expectedPath) {
|
|
return entries.find((entry) => normalizeAsarEntryPath(entry) === expectedPath)
|
|
}
|
|
|
|
function verifyPackagedMainRuntimeDeps(resourcesDir, asar = require('@electron/asar')) {
|
|
const asarPath = join(resourcesDir, 'app.asar')
|
|
if (!existsSync(asarPath)) {
|
|
return
|
|
}
|
|
|
|
const entries = asar.listPackage(asarPath)
|
|
for (const file of PACKAGED_MAIN_REQUIRED_FILES) {
|
|
if (!findAsarEntry(entries, file)) {
|
|
throw new Error(`Packaged main file ${file} was not found in ${asarPath}`)
|
|
}
|
|
}
|
|
|
|
const missing = new Set()
|
|
// Why every emitted main file rather than the entry points alone: rolldown hoists
|
|
// modules shared by two entries into out/main/chunks, so an entry's own bare imports
|
|
// move out from under a fixed file list and silently stop being checked.
|
|
for (const entry of entries) {
|
|
if (!PACKAGED_MAIN_SOURCE_RE.test(normalizeAsarEntryPath(entry))) {
|
|
continue
|
|
}
|
|
|
|
// Why: @electron/asar lists entries with host separators; Windows returns
|
|
// backslashes, and extractFile expects that same host-style path.
|
|
const internalPath = entry.replace(/^[\\/]+/, '')
|
|
const source = asar.extractFile(asarPath, internalPath).toString('utf8')
|
|
// Why the lookbehind: Orca has its own registry methods named `require`, so a
|
|
// minified `registry.require('some-id')` must not read as a bare specifier.
|
|
// Why it readmits `...`: a dot that ends a spread is not member access, and
|
|
// the two error directions are not symmetric -- a false positive fails the
|
|
// release build loudly, a false negative is this guard going blind.
|
|
// Known limit: a specifier inside an embedded source string counts too, and
|
|
// ssh-relay-deploy's remote probe names node-pty that way. A remote-only
|
|
// dependency added to that script would fail desktop packaging here; telling
|
|
// the two apart needs a parser, not a wider pattern.
|
|
for (const match of source.matchAll(
|
|
/(?:(?<![.\w])|(?<=\.\.\.))(?:require|import)\s*\(\s*(["'`])([^"'`$]+)\1\s*\)/g
|
|
)) {
|
|
const specifier = match[2]
|
|
if (!isPackagedExternalSpecifier(specifier)) {
|
|
continue
|
|
}
|
|
const packageName = packageNameFromSpecifier(specifier)
|
|
if (!existsSync(join(resourcesDir, 'node_modules', ...packageName.split('/')))) {
|
|
missing.add(packageName)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (missing.size > 0) {
|
|
throw new Error(
|
|
`Packaged main bundle has bare runtime imports without copied node_modules: ${[
|
|
...missing
|
|
].join(', ')}`
|
|
)
|
|
}
|
|
}
|
|
|
|
function normalizeNodePtyWindowsArch(electronArch) {
|
|
const architecture = normalizeElectronArchitecture(electronArch)
|
|
if (architecture !== 'x64' && architecture !== 'arm64') {
|
|
throw new Error(`Unsupported packaged node-pty Windows architecture: ${architecture}`)
|
|
}
|
|
return architecture
|
|
}
|
|
|
|
function normalizeElectronArchitecture(electronArch) {
|
|
const architecture =
|
|
typeof electronArch === 'number'
|
|
? ELECTRON_ARCHITECTURE_BY_ENUM[electronArch]
|
|
: electronArch === 'armv7l'
|
|
? 'arm'
|
|
: electronArch
|
|
if (!PACKAGED_NATIVE_ARCHITECTURES.has(architecture)) {
|
|
throw new Error(`Unsupported packaged runtime architecture: ${String(electronArch)}`)
|
|
}
|
|
return architecture
|
|
}
|
|
|
|
function pruneNodePtyNativeDirectories(directory, platformPrefix, electronArch, allowsSuffix) {
|
|
if (!existsSync(directory)) {
|
|
return
|
|
}
|
|
const architecture = normalizeElectronArchitecture(electronArch)
|
|
const targetPrefix = `${platformPrefix}${architecture}`
|
|
const platformPrefixes = Object.values(NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM)
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || !platformPrefixes.some((prefix) => entry.name.startsWith(prefix))) {
|
|
continue
|
|
}
|
|
const matchesTarget =
|
|
entry.name.startsWith(platformPrefix) &&
|
|
(entry.name === targetPrefix || (allowsSuffix && entry.name.startsWith(`${targetPrefix}-`)))
|
|
if (!matchesTarget) {
|
|
rmSync(join(directory, entry.name), { recursive: true, force: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
function findNodePtyConptySourceDir(nodePtyDir, windowsArch) {
|
|
const conptyRoot = join(nodePtyDir, 'third_party', 'conpty')
|
|
if (!existsSync(conptyRoot)) {
|
|
throw new Error(`Packaged node-pty is missing ${conptyRoot}`)
|
|
}
|
|
for (const entry of readdirSync(conptyRoot, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) {
|
|
continue
|
|
}
|
|
const sourceDir = join(conptyRoot, entry.name, `win10-${windowsArch}`)
|
|
if (existsSync(sourceDir)) {
|
|
return sourceDir
|
|
}
|
|
}
|
|
throw new Error(`Packaged node-pty has no ConPTY payload for win10-${windowsArch}`)
|
|
}
|
|
|
|
function ensurePackagedNodePtyConptyRuntime(nodePtyDir, electronArch) {
|
|
const releaseDir = join(nodePtyDir, 'build', 'Release')
|
|
if (!existsSync(join(releaseDir, 'conpty.node'))) {
|
|
return
|
|
}
|
|
|
|
const runtimeDir = join(releaseDir, 'conpty')
|
|
const missingRuntimeFiles = NODE_PTY_CONPTY_RUNTIME_FILES.filter(
|
|
(filename) => !existsSync(join(runtimeDir, filename))
|
|
)
|
|
if (missingRuntimeFiles.length === 0) {
|
|
return
|
|
}
|
|
|
|
const windowsArch = normalizeNodePtyWindowsArch(electronArch)
|
|
const sourceDir = findNodePtyConptySourceDir(nodePtyDir, windowsArch)
|
|
mkdirSync(runtimeDir, { recursive: true })
|
|
for (const filename of missingRuntimeFiles) {
|
|
const sourceFile = join(sourceDir, filename)
|
|
if (!existsSync(sourceFile)) {
|
|
throw new Error(`Packaged node-pty is missing ${sourceFile}`)
|
|
}
|
|
// Why: node-pty's Windows addon loads conpty.dll relative to conpty.node,
|
|
// but its install script can run before electron-builder gathers resources.
|
|
copyFileSync(sourceFile, join(runtimeDir, filename))
|
|
}
|
|
}
|
|
|
|
function prunePackagedNodePty(resourcesDir, electronPlatformName, electronArch) {
|
|
const nodePtyDir = join(resourcesDir, 'node_modules', 'node-pty')
|
|
if (!existsSync(nodePtyDir)) {
|
|
return
|
|
}
|
|
|
|
// Why delete only conpty.node: node-pty's loader tries build/Release, then
|
|
// build/Debug, then prebuilds/<platform>-<arch>, swallowing every failure in
|
|
// between. Only the source build carries Orca's job-object exports, so an ABI
|
|
// mismatch or an AV quarantine of build/Release/conpty.node would silently
|
|
// fall through to the UNPATCHED prebuild -- teardown back to guessing by PID
|
|
// ancestry, with no error anywhere.
|
|
//
|
|
// Why NOT the whole prebuilds/ tree: Orca's own patch deletes the
|
|
// `conpty_console_list` and winpty `pty` gyp targets, so a Windows source
|
|
// build emits conpty.node and nothing else. conpty_console_list.node,
|
|
// pty.node, winpty.dll and winpty-agent.exe exist ONLY here. Removing them
|
|
// silently kills console-membership probing (the forked agent throws at
|
|
// require, and its caller resolves null with silent: true), and removes the
|
|
// winpty backend that node-pty still selects below Windows build 18309.
|
|
//
|
|
// Why the arch check: a cross-arch package copies the host's build/Release,
|
|
// so its mere presence does not mean it matches electronArch -- deleting the
|
|
// target-arch prebuild would then remove the only loadable binary.
|
|
if (
|
|
electronPlatformName === 'win32' &&
|
|
electronArch === process.arch &&
|
|
existsSync(join(nodePtyDir, 'build', 'Release', 'conpty.node'))
|
|
) {
|
|
const prebuildDir = join(nodePtyDir, 'prebuilds', `win32-${electronArch}`)
|
|
for (const staleFallback of ['conpty.node', 'conpty.pdb']) {
|
|
rmSync(join(prebuildDir, staleFallback), { force: true })
|
|
}
|
|
}
|
|
|
|
const allowedPrebuildPrefix = NODE_PTY_PREBUILD_PREFIX_BY_PLATFORM[electronPlatformName]
|
|
if (allowedPrebuildPrefix) {
|
|
pruneNodePtyNativeDirectories(
|
|
join(nodePtyDir, 'prebuilds'),
|
|
allowedPrebuildPrefix,
|
|
electronArch,
|
|
false
|
|
)
|
|
// Why: sequential cross-arch rebuilds accumulate ABI-tagged outputs here.
|
|
pruneNodePtyNativeDirectories(
|
|
join(nodePtyDir, 'bin'),
|
|
allowedPrebuildPrefix,
|
|
electronArch,
|
|
true
|
|
)
|
|
}
|
|
|
|
if (electronPlatformName === 'win32') {
|
|
ensurePackagedNodePtyConptyRuntime(nodePtyDir, electronArch)
|
|
} else {
|
|
// Why: conpty is Windows-only and node-pty resolves runtime binaries from
|
|
// build/Release or prebuilds/<platform>-<arch>, not third_party/conpty.
|
|
rmSync(join(nodePtyDir, 'third_party', 'conpty'), { recursive: true, force: true })
|
|
rmSync(join(nodePtyDir, 'deps', 'winpty'), { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
function prunePackagedParcelWatcher(resourcesDir, electronPlatformName, electronArch) {
|
|
const parcelDir = join(resourcesDir, 'node_modules', '@parcel')
|
|
if (!existsSync(parcelDir)) {
|
|
return
|
|
}
|
|
|
|
// Why: we package every installed @parcel/watcher-<platform> optional
|
|
// subpackage (pnpm install:release fetches every CPU), but each build only needs
|
|
// its own platform/architecture binaries. Keep the core package and matching
|
|
// native variants; drop the rest.
|
|
const keepPrefix = PARCEL_WATCHER_PLATFORM_PREFIX_BY_PLATFORM[electronPlatformName]
|
|
const architecture = normalizeElectronArchitecture(electronArch)
|
|
const targetPrefix = keepPrefix ? `${keepPrefix}-${architecture}` : null
|
|
for (const entry of readdirSync(parcelDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || entry.name === 'watcher') {
|
|
continue
|
|
}
|
|
// Why: only ever prune the watcher's own platform subpackages. Guards against
|
|
// nuking an unrelated @parcel/* runtime dep if one is added to the roots later.
|
|
if (!entry.name.startsWith('watcher-')) {
|
|
continue
|
|
}
|
|
if (
|
|
keepPrefix &&
|
|
entry.name.startsWith(keepPrefix) &&
|
|
(entry.name === targetPrefix || entry.name.startsWith(`${targetPrefix}-`))
|
|
) {
|
|
continue
|
|
}
|
|
rmSync(join(parcelDir, entry.name), { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
// Why type declarations: they are compile-time only; the packaged app never resolves them.
|
|
// Why source maps: they embed the original sources (megabytes for @linear/sdk alone) and
|
|
// nothing in the packaged app turns on Node's source-map support, so they are never read.
|
|
// Orca's own main-process maps live outside node_modules and ship as a separate release artifact.
|
|
function isPrunableTypeOrSourceMapArtifact(filename) {
|
|
return TYPE_DECLARATION_ARTIFACT_RE.test(filename) || JS_SOURCE_MAP_ARTIFACT_RE.test(filename)
|
|
}
|
|
|
|
// Why one walk: pruneMatchingFiles only ever deletes files, so passes over the same tree
|
|
// commute — a second recursive traversal costs seconds for no extra deletions.
|
|
function prunePackagedRuntimeTypeAndSourceMapArtifacts(resourcesDir) {
|
|
const nodeModulesDir = join(resourcesDir, 'node_modules')
|
|
if (!existsSync(nodeModulesDir)) {
|
|
return
|
|
}
|
|
pruneMatchingFiles(nodeModulesDir, isPrunableTypeOrSourceMapArtifact)
|
|
}
|
|
|
|
function prunePackagedSherpaOnnx(resourcesDir, electronPlatformName) {
|
|
if (electronPlatformName !== 'darwin') {
|
|
return
|
|
}
|
|
const nodeModulesDir = join(resourcesDir, 'node_modules')
|
|
if (!existsSync(nodeModulesDir)) {
|
|
return
|
|
}
|
|
for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || !entry.name.startsWith('sherpa-onnx-darwin-')) {
|
|
continue
|
|
}
|
|
const packageDir = join(nodeModulesDir, entry.name)
|
|
const packageEntries = readdirSync(packageDir)
|
|
const hasVersionedOnnxRuntime = packageEntries.some((filename) =>
|
|
VERSIONED_ONNXRUNTIME_DYLIB_RE.test(filename)
|
|
)
|
|
if (hasVersionedOnnxRuntime) {
|
|
// Why: darwin sherpa-onnx binaries link to the versioned ONNX Runtime
|
|
// install name; the unversioned dylib is a duplicate fallback copy.
|
|
rmSync(join(packageDir, 'libonnxruntime.dylib'), { force: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
function prunePackagedZodSources(resourcesDir) {
|
|
// Why: Zod's src tree is TypeScript source only selected by the @zod/source
|
|
// condition; packaged runtime import/require paths resolve to built JS.
|
|
rmSync(join(resourcesDir, 'node_modules', 'zod', 'src'), { recursive: true, force: true })
|
|
}
|
|
|
|
// Why: electron-builder only warns on a missing extraResources source, so a host-only
|
|
// install would silently ship a foreign-arch slice without its native addons.
|
|
function assertPackagedNativeVariantsInstalled(electronPlatformName, electronArch) {
|
|
const architecture = normalizeElectronArchitecture(electronArch)
|
|
const nodeModulesDir = join(projectDir, 'node_modules')
|
|
const isInstalled = (name) => existsSync(join(nodeModulesDir, name, 'package.json'))
|
|
const missing = []
|
|
|
|
const rootOptionalDependencies =
|
|
JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf8')).optionalDependencies ?? {}
|
|
// Why win32 is always x64: winSpeechNativeResource packages sherpa-onnx-win-x64 for every
|
|
// Windows target (there is no sherpa-onnx-win-arm64; it runs under emulation).
|
|
const sherpaName =
|
|
electronPlatformName === 'win32'
|
|
? 'sherpa-onnx-win-x64'
|
|
: `sherpa-onnx-${electronPlatformName}-${architecture}`
|
|
if (sherpaName in rootOptionalDependencies && !isInstalled(sherpaName)) {
|
|
missing.push(sherpaName)
|
|
}
|
|
|
|
// Why prefix, not equality: linux variants carry a libc suffix (watcher-linux-x64-glibc),
|
|
// mirroring what prunePackagedParcelWatcher keeps.
|
|
const watcherPrefix = `watcher-${electronPlatformName}-${architecture}`
|
|
const parcelDir = join(nodeModulesDir, '@parcel')
|
|
if (isInstalled('@parcel/watcher')) {
|
|
const watcherOptionalDependencies = Object.keys(
|
|
JSON.parse(readFileSync(join(parcelDir, 'watcher', 'package.json'), 'utf8'))
|
|
.optionalDependencies ?? {}
|
|
)
|
|
const expectedVariants = watcherOptionalDependencies.filter((name) =>
|
|
name.startsWith(`@parcel/${watcherPrefix}`)
|
|
)
|
|
// Why not withFileTypes: pnpm links the variants, so isDirectory() is false for them.
|
|
const hasVariant = readdirSync(parcelDir).some(
|
|
(name) => name.startsWith(watcherPrefix) && isInstalled(`@parcel/${name}`)
|
|
)
|
|
if (expectedVariants.length > 0 && !hasVariant) {
|
|
missing.push(...expectedVariants)
|
|
}
|
|
}
|
|
|
|
// Why one package: @vscode/windows-process-tree is the only os: win32 npm addon;
|
|
// @orca/windows-registry is a workspace link present on every host, so its presence proves nothing.
|
|
const missingWindowsAddons = []
|
|
if (electronPlatformName === 'win32' && !isInstalled('@vscode/windows-process-tree')) {
|
|
missingWindowsAddons.push('@vscode/windows-process-tree')
|
|
}
|
|
|
|
if (missing.length === 0 && missingWindowsAddons.length === 0) {
|
|
return
|
|
}
|
|
// Why separate remedies: install:release widens only the CPU set, so the os: win32 addon
|
|
// never arrives on a non-Windows host and is compiled only by the Windows-only rebuild.
|
|
const remedies = []
|
|
if (missing.length > 0) {
|
|
remedies.push('Run pnpm install:release to install another architecture.')
|
|
}
|
|
if (missingWindowsAddons.length > 0) {
|
|
remedies.push(
|
|
'Windows packaging requires a Windows host: the Windows addons are installed only where ' +
|
|
'os: win32 matches and compiled only by the Windows-only rebuild.'
|
|
)
|
|
}
|
|
throw new Error(
|
|
`Packaging ${electronPlatformName}/${architecture} requires native variants that are not installed: ` +
|
|
`${[...new Set([...missing, ...missingWindowsAddons])].sort().join(', ')}. ${remedies.join(' ')}`
|
|
)
|
|
}
|
|
|
|
function prunePackagedRuntimeNodeModules(resourcesDir, electronPlatformName, electronArch) {
|
|
const architecture = normalizeElectronArchitecture(electronArch)
|
|
prunePackagedNodePty(resourcesDir, electronPlatformName, architecture)
|
|
prunePackagedParcelWatcher(resourcesDir, electronPlatformName, architecture)
|
|
// Why before the filename walk: zod/src is deleted wholesale, so walking it first is wasted work.
|
|
prunePackagedZodSources(resourcesDir)
|
|
prunePackagedRuntimeTypeAndSourceMapArtifacts(resourcesDir)
|
|
prunePackagedSherpaOnnx(resourcesDir, electronPlatformName)
|
|
}
|
|
|
|
function pruneMatchingFiles(directory, shouldPrune) {
|
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
const entryPath = join(directory, entry.name)
|
|
if (entry.isDirectory()) {
|
|
pruneMatchingFiles(entryPath, shouldPrune)
|
|
} else if (entry.isFile() && shouldPrune(entry.name)) {
|
|
rmSync(entryPath, { force: true })
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
PACKAGED_RUNTIME_PACKAGE_ROOTS,
|
|
assertPackagedNativeVariantsInstalled,
|
|
createPackagedRuntimeNodeModuleResources,
|
|
findAsarEntry,
|
|
isPackagedExternalSpecifier,
|
|
normalizeNodePtyWindowsArch,
|
|
packageNameFromSpecifier,
|
|
prunePackagedNodePty,
|
|
prunePackagedParcelWatcher,
|
|
prunePackagedRuntimeNodeModules,
|
|
prunePackagedRuntimeTypeAndSourceMapArtifacts,
|
|
prunePackagedSherpaOnnx,
|
|
prunePackagedZodSources,
|
|
verifyPackagedMainRuntimeDeps
|
|
}
|