Files
orca/config/scripts/shared-tree-publish.mjs
T
Neil df0c44571b fix(build): shared native packages get one immutable build instead of in-place rebuilds (#30226)
* refactor(build): extract the lock-free shared tree publish from the Electron dist cache

Native builds in package dirs shared across worktrees need the same
stage, validate, rename-into-place publish; one implementation keeps the
never-replace-a-good-entry and quarantine-restore rules in one place.

* fix(build): never rebuild a native addon in a package dir other worktrees load

Under pnpm's global virtual store node_modules/node-pty is one store slot
shared by every worktree, so an in-place node-gyp or @electron/rebuild run
in one worktree replaced the pty.node another was running. A shared
package dir now gets an immutable per-ABI build/orca-napi-<platform>-<arch>
compiled in a scratch copy, load-checked in a fresh process, and published
atomically; a usable entry is reused, never rebuilt. The patched node-pty
and windows-process-tree loaders try that dir before build/Release, and
packaging renames it to build/Release so packaged layouts are unchanged.
Private (non-GVS, CI) installs keep the existing in-place path.

* fix(build): find shared native packages through the project's own node_modules link

require.resolve with paths fell back to another install under Bun, so a
test fixture's private node-pty was judged shared.

* ci: run native checks when the shared native build modules change

* fix(build): pin the shared native slot and keep sibling publishes intact

Resolve the package's physical dir once so a concurrent relink cannot publish
one slot's build into another, verify the quarantined tree is the one judged
unusable before replacing it, and keep in-flight staging/quarantine trees out
of the packaged resource copy.

* fix(build): never move a sibling's fresh native entry, and refuse off-platform builds

Re-check the entry's identity right before quarantining it, and fail instead
of publishing a host-OS binary under another platform's entry name.
2026-10-18 20:21:50 -07:00

86 lines
3.0 KiB
JavaScript

import { randomUUID } from 'node:crypto'
import { existsSync, renameSync, rmSync, statSync } from 'node:fs'
/** A uniquely named sibling of `entryDir`, so concurrent publishers never share a staging tree. */
export function sharedTreeStagingPath(entryDir, uuid = randomUUID) {
return `${entryDir}.staging-${process.pid}-${uuid()}`
}
/**
* Move a fully built, validated `stagingDir` into place as `entryDir`, lock-free.
*
* Every worktree that shares the entry may be reading it, so a usable entry is never replaced,
* and the entry is either absent, the old tree, or the new tree -- never half-written. A rename
* onto a populated directory fails, so of two racing publishers one wins and the other just
* removes its own staging tree. An unusable entry is quarantined first, and put back if the swap
* fails, because a bad entry the next publisher can re-judge beats an empty one. The quarantined
* tree must be the one judged unusable; a sibling's fresh publish that landed mid-judgement is
* put back untouched.
*
* Re-checks the entry here, immediately before the rename, because staging takes long enough for
* a sibling to publish a good one in the meantime. Returns true only when this call published.
*/
export function publishSharedTree(stagingDir, entryDir, options) {
const { isUsable } = options
const uuid = options.uuid ?? randomUUID
const rename = options.rename ?? renameSync
let quarantinePath = null
if (existsSync(entryDir)) {
const judged = treeIdentity(entryDir)
if (isUsable(entryDir)) {
rmSync(stagingDir, { recursive: true, force: true })
return false
}
// Why: the probe takes long enough for a sibling to publish; don't even briefly move theirs.
if (judged === null || treeIdentity(entryDir) !== judged) {
rmSync(stagingDir, { recursive: true, force: true })
return false
}
quarantinePath = `${entryDir}.unusable-${process.pid}-${uuid()}`
try {
renameSync(entryDir, quarantinePath)
} catch {
rmSync(stagingDir, { recursive: true, force: true })
return false // Another publisher is already replacing it, or (Windows) it is loaded.
}
if (judged === null || treeIdentity(quarantinePath) !== judged) {
rmSync(stagingDir, { recursive: true, force: true })
try {
renameSync(quarantinePath, entryDir)
} catch {
rmSync(quarantinePath, { recursive: true, force: true }) // A third publisher refilled it.
}
return false
}
}
try {
rename(stagingDir, entryDir)
} catch {
rmSync(stagingDir, { recursive: true, force: true })
if (quarantinePath !== null) {
try {
renameSync(quarantinePath, entryDir)
return false
} catch {
rmSync(quarantinePath, { recursive: true, force: true })
}
}
return false
}
if (quarantinePath !== null) {
rmSync(quarantinePath, { recursive: true, force: true })
}
return true
}
function treeIdentity(dir) {
try {
const { dev, ino } = statSync(dir, { bigint: true })
return `${dev}:${ino}`
} catch {
return null
}
}