Files
orca/config/scripts/shared-tree-publish.test.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

124 lines
4.2 KiB
JavaScript

import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { publishSharedTree, sharedTreeStagingPath } from './shared-tree-publish.mjs'
const roots = []
afterEach(() => {
while (roots.length > 0) {
rmSync(roots.pop(), { recursive: true, force: true })
}
})
function makeRoot() {
const root = mkdtempSync(path.join(tmpdir(), 'orca-shared-tree-'))
roots.push(root)
return root
}
function writeTree(dir, content) {
mkdirSync(dir, { recursive: true })
writeFileSync(path.join(dir, 'payload'), content)
return dir
}
const readPayload = (dir) => readFileSync(path.join(dir, 'payload'), 'utf8')
const isGood = (dir) => existsSync(path.join(dir, 'payload')) && readPayload(dir) !== 'bad'
describe('publishSharedTree', () => {
it('names staging trees uniquely beside the entry', () => {
expect(sharedTreeStagingPath('/x/entry', () => 'u1')).toBe(`/x/entry.staging-${process.pid}-u1`)
})
it('renames a staged tree into an empty slot', () => {
const root = makeRoot()
const entry = path.join(root, 'entry')
const staging = writeTree(sharedTreeStagingPath(entry), 'new')
expect(publishSharedTree(staging, entry, { isUsable: isGood })).toBe(true)
expect(readPayload(entry)).toBe('new')
expect(readdirSync(root)).toEqual(['entry'])
})
it('never replaces a usable entry, and drops its own staging tree', () => {
const root = makeRoot()
const entry = writeTree(path.join(root, 'entry'), 'old')
const staging = writeTree(sharedTreeStagingPath(entry), 'new')
expect(publishSharedTree(staging, entry, { isUsable: isGood })).toBe(false)
expect(readPayload(entry)).toBe('old')
expect(readdirSync(root)).toEqual(['entry'])
})
it('lets exactly one of two racing publishers win', () => {
const root = makeRoot()
const entry = path.join(root, 'entry')
const first = writeTree(
sharedTreeStagingPath(entry, () => 'a'),
'first'
)
const second = writeTree(
sharedTreeStagingPath(entry, () => 'b'),
'second'
)
// The second passes its pre-check, then the first lands just before its own rename does.
const raced = publishSharedTree(second, entry, {
isUsable: isGood,
rename: (from, to) => {
expect(publishSharedTree(first, entry, { isUsable: isGood })).toBe(true)
renameSync(from, to)
}
})
expect(raced).toBe(false)
expect(readPayload(entry)).toBe('first')
expect(readdirSync(root)).toEqual(['entry'])
})
it('quarantines an unusable entry and publishes over it', () => {
const root = makeRoot()
const entry = writeTree(path.join(root, 'entry'), 'bad')
const staging = writeTree(sharedTreeStagingPath(entry), 'new')
expect(publishSharedTree(staging, entry, { isUsable: isGood })).toBe(true)
expect(readPayload(entry)).toBe('new')
expect(readdirSync(root)).toEqual(['entry'])
})
it('puts the quarantined entry back when the swap fails', () => {
const root = makeRoot()
const entry = writeTree(path.join(root, 'entry'), 'bad')
const staging = writeTree(sharedTreeStagingPath(entry), 'new')
const failingRename = () => {
throw new Error('EPERM')
}
expect(publishSharedTree(staging, entry, { isUsable: isGood, rename: failingRename })).toBe(
false
)
expect(readPayload(entry)).toBe('bad')
expect(readdirSync(root)).toEqual(['entry'])
})
it('never quarantines a good entry a sibling published while the bad one was judged', () => {
const root = makeRoot()
const entry = writeTree(path.join(root, 'entry'), 'bad')
const staging = writeTree(sharedTreeStagingPath(entry), 'mine')
const sibling = writeTree(path.join(root, 'sibling'), 'sibling')
const judgeThenRace = (dir) => {
const usable = isGood(dir)
rmSync(entry, { recursive: true, force: true })
renameSync(sibling, entry)
return usable
}
expect(publishSharedTree(staging, entry, { isUsable: judgeThenRace })).toBe(false)
expect(readPayload(entry)).toBe('sibling')
expect(readdirSync(root)).toEqual(['entry'])
})
})