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.
124 lines
4.2 KiB
JavaScript
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'])
|
|
})
|
|
})
|