mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* 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.
86 lines
3.0 KiB
JavaScript
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
|
|
}
|
|
}
|