mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +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.
140 lines
5.1 KiB
JavaScript
140 lines
5.1 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { makeTreeReadOnly, shareTree } from './space-sharing-copy.mjs'
|
|
import { publishSharedTree, sharedTreeStagingPath } from './shared-tree-publish.mjs'
|
|
|
|
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
// Sibling of path.txt, never inside dist: an install replaces dist wholesale.
|
|
const MARKER_FILENAME = '.orca-shared-dist'
|
|
|
|
/**
|
|
* Where sibling worktrees of one repository keep their shared extracted Electron.
|
|
*
|
|
* Null means "install normally" -- every caller treats a missing entry as "do what you did before".
|
|
*/
|
|
export function resolveSharedElectronDistEntry(options) {
|
|
const { repoRoot, version, targetPlatform, targetArch } = options
|
|
const env = options.env ?? process.env
|
|
// Packaging jobs get a fresh checkout per run, so a cache only adds a failure mode.
|
|
if (env.CI === '1' || env.CI === 'true') {
|
|
return null
|
|
}
|
|
if (![version, targetPlatform, targetArch].every((part) => IDENTITY_PATTERN.test(part ?? ''))) {
|
|
return null
|
|
}
|
|
let gitCommonDir
|
|
try {
|
|
gitCommonDir = resolveGitCommonDir(repoRoot, options.execFile ?? execFileSync)
|
|
} catch {
|
|
return null // Folder workspace, or no Git on PATH.
|
|
}
|
|
const cacheRoot = path.join(gitCommonDir, 'orca-cache', 'electron')
|
|
return {
|
|
cacheRoot,
|
|
entryPath: path.join(cacheRoot, `${version}-${targetPlatform}-${targetArch}`),
|
|
markerPath: path.join(options.electronPackageDir, MARKER_FILENAME)
|
|
}
|
|
}
|
|
|
|
export function resolveGitCommonDir(repoRoot, execFile = execFileSync) {
|
|
const rawPath = execFile('git', ['-C', repoRoot, 'rev-parse', '--git-common-dir'], {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
}).trim()
|
|
if (!rawPath) {
|
|
throw new Error('Git returned an empty common directory')
|
|
}
|
|
return path.resolve(repoRoot, rawPath)
|
|
}
|
|
|
|
/** True once this worktree's dist is already a clone of the current cache entry. */
|
|
export function hasAdoptedSharedElectronDist(entry) {
|
|
try {
|
|
return readFileSync(entry.markerPath, 'utf8') === path.basename(entry.entryPath)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function recordAdoptedSharedElectronDist(entry, write) {
|
|
try {
|
|
write(entry.markerPath, path.basename(entry.entryPath))
|
|
} catch {
|
|
// The marker is only an optimization: a missing one costs one extra clone.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Share a validated cache entry into `stagePath`. Deliberately never falls back to a byte copy --
|
|
* with no storage to share the caller is better off with its normal install, which this must not
|
|
* slow down.
|
|
*/
|
|
export function shareElectronDistFromCache(entry, stagePath, options) {
|
|
const { version, platformPath } = options
|
|
if (!isUsableElectronDist(entry.entryPath, version, platformPath)) {
|
|
return false
|
|
}
|
|
try {
|
|
;(options.share ?? shareTree)(entry.entryPath, stagePath)
|
|
} catch {
|
|
return false
|
|
}
|
|
return isUsableElectronDist(stagePath, version, platformPath)
|
|
}
|
|
|
|
/**
|
|
* Publish this worktree's dist as the shared entry, best effort.
|
|
*
|
|
* No lock: staging names are unique and `rename` onto a populated directory fails with ENOTEMPTY,
|
|
* so a concurrent publisher either wins the rename or cleans up its own staging tree. Neither can
|
|
* observe a half-written entry, and a usable entry already in place is never overwritten.
|
|
*/
|
|
export function publishSharedElectronDist(distPath, entry, options = {}) {
|
|
const { version, platformPath } = options
|
|
const uuid = options.uuid ?? randomUUID
|
|
const canValidate = Boolean(version) && Boolean(platformPath)
|
|
// Without an identity to check against, "unusable" is unknowable -- never discard on a guess.
|
|
if (existsSync(entry.entryPath) && (!canValidate || isUsable(entry, version, platformPath))) {
|
|
return false
|
|
}
|
|
|
|
const stagePath = sharedTreeStagingPath(entry.entryPath, uuid)
|
|
try {
|
|
mkdirSync(entry.cacheRoot, { recursive: true })
|
|
;(options.share ?? shareTree)(distPath, stagePath)
|
|
// Before publishing, not after: an entry is visible the instant the rename lands, and under
|
|
// hardlink sharing this is the only thing standing between a stray write and every worktree.
|
|
;(options.protect ?? makeTreeReadOnly)(stagePath)
|
|
} catch {
|
|
rmSync(stagePath, { recursive: true, force: true })
|
|
return false
|
|
}
|
|
|
|
// An entry we cannot judge counts as good: another worktree got there first, so keep theirs.
|
|
return publishSharedTree(stagePath, entry.entryPath, {
|
|
isUsable: () => !canValidate || isUsable(entry, version, platformPath),
|
|
uuid,
|
|
rename: options.rename
|
|
})
|
|
}
|
|
|
|
function isUsable(entry, version, platformPath) {
|
|
return isUsableElectronDist(entry.entryPath, version, platformPath)
|
|
}
|
|
|
|
export function isUsableElectronDist(distPath, version, platformPath) {
|
|
try {
|
|
if (!lstatSync(distPath).isDirectory()) {
|
|
return false
|
|
}
|
|
const installedVersion = readFileSync(path.join(distPath, 'version'), 'utf8')
|
|
.trim()
|
|
.replace(/^v/, '')
|
|
return installedVersion === version && existsSync(path.join(distPath, platformPath))
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|