Repository navigation
feat(orcad): prove node-pty loads before anything requires it #16374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dc7056a
73e316a
c47bafa
9dd6386
4a1c4cf
aa01a3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,212 @@ | ||||||||||||||||||||||||||||||||
| #!/usr/bin/env node | ||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Build one node-pty prebuilt for the CURRENT platform/arch/libc and file it in orcad's | ||||||||||||||||||||||||||||||||
| * prebuilds matrix, so a deployment target needs no C/C++ toolchain. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * node-pty is the only ABI-sensitive native module orcad requires. It is also PATCHED in | ||||||||||||||||||||||||||||||||
| * this repo (config/patches/node-pty@1.1.0.patch), and that patch is the glibc-floor fix: | ||||||||||||||||||||||||||||||||
| * `.symver` pins on openpty/forkpty/pthread_sigmask plus the `--no-as-needed` ldflags that | ||||||||||||||||||||||||||||||||
| * keep libutil/libpthread in DT_NEEDED. An upstream prebuilt has none of it and reproduces | ||||||||||||||||||||||||||||||||
| * #9902. So the matrix is compiled from patched sources here, and this script refuses to | ||||||||||||||||||||||||||||||||
| * run if the patch is not in the tree it is about to compile. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * orcad pins its own Node runtime, so the ABI dimension is fixed and the matrix varies | ||||||||||||||||||||||||||||||||
| * only platform/arch/libc: | ||||||||||||||||||||||||||||||||
| * linux-x64-glibc, linux-arm64-glibc, linux-x64-musl, linux-arm64-musl, | ||||||||||||||||||||||||||||||||
| * darwin-x64, darwin-arm64 | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * CI runs this once per slot, each inside the container that owns that libc/arch, and | ||||||||||||||||||||||||||||||||
| * merges the resulting `out/orcad/prebuilds` trees. `--slot=<name>` forces the label so | ||||||||||||||||||||||||||||||||
| * the glibc/musl distinction is recorded from the container rather than detected. | ||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||
| * Usage: | ||||||||||||||||||||||||||||||||
| * node config/scripts/build-orcad-prebuilds.mjs [--slot=linux-x64-musl] | ||||||||||||||||||||||||||||||||
| * node config/scripts/build-orcad-prebuilds.mjs --require-slots # release gate | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' | ||||||||||||||||||||||||||||||||
| import { createRequire } from 'node:module' | ||||||||||||||||||||||||||||||||
| import { dirname, join } from 'node:path' | ||||||||||||||||||||||||||||||||
| import process from 'node:process' | ||||||||||||||||||||||||||||||||
| import { spawnSync } from 'node:child_process' | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const require = createRequire(import.meta.url) | ||||||||||||||||||||||||||||||||
| const ROOT = join(import.meta.dirname, '..', '..') | ||||||||||||||||||||||||||||||||
| const PREBUILDS_DIR = join(ROOT, 'out', 'orcad', 'prebuilds') | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** Every slot a shipped matrix must fill. The single source of truth for the matrix. */ | ||||||||||||||||||||||||||||||||
| export const MATRIX_SLOTS = [ | ||||||||||||||||||||||||||||||||
| 'linux-x64-glibc', | ||||||||||||||||||||||||||||||||
| 'linux-arm64-glibc', | ||||||||||||||||||||||||||||||||
| 'linux-x64-musl', | ||||||||||||||||||||||||||||||||
| 'linux-arm64-musl', | ||||||||||||||||||||||||||||||||
| 'darwin-x64', | ||||||||||||||||||||||||||||||||
| 'darwin-arm64' | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Why the report header and not `ldd`: `glibcVersionRuntime` is present only when the | ||||||||||||||||||||||||||||||||
| * process is linked against glibc, and musl images have no `ldd` worth parsing. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export function detectLibc(platform = process.platform, header = readReportHeader()) { | ||||||||||||||||||||||||||||||||
| if (platform !== 'linux') { | ||||||||||||||||||||||||||||||||
| return 'none' | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return header && typeof header === 'object' && 'glibcVersionRuntime' in header ? 'glibc' : 'musl' | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use the runtime fallback for an unreadable report. If Proposed fix export function detectLibc(platform = process.platform, header = readReportHeader()) {
if (platform !== 'linux') {
return 'none'
}
- return header && typeof header === 'object' && 'glibcVersionRuntime' in header ? 'glibc' : 'musl'
+ if (!header || typeof header !== 'object') {
+ return 'glibc'
+ }
+ return 'glibcVersionRuntime' in header ? 'glibc' : 'musl'
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function readReportHeader() { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| return process.report?.getReport?.()?.header | ||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||
| return undefined | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function slotName(argv = process.argv, platform = process.platform, arch = process.arch) { | ||||||||||||||||||||||||||||||||
| const forced = argv.find((arg) => arg.startsWith('--slot=')) | ||||||||||||||||||||||||||||||||
| if (forced) { | ||||||||||||||||||||||||||||||||
| return forced.slice('--slot='.length) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| const libc = detectLibc(platform) | ||||||||||||||||||||||||||||||||
| return libc === 'none' ? `${platform}-${arch}` : `${platform}-${arch}-${libc}` | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * The patch is what holds the Ubuntu 20.04 floor. Compiling without it produces a binary | ||||||||||||||||||||||||||||||||
| * that loads fine on the build host and dies on the target — the exact failure the matrix | ||||||||||||||||||||||||||||||||
| * exists to prevent, now baked into a shipped artifact instead of a first-connect error. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export function assertNodePtyPatchApplied(nodePtyDir) { | ||||||||||||||||||||||||||||||||
| const bindingGyp = readFileSync(join(nodePtyDir, 'binding.gyp'), 'utf8') | ||||||||||||||||||||||||||||||||
| const ptySource = readFileSync(join(nodePtyDir, 'src', 'unix', 'pty.cc'), 'utf8') | ||||||||||||||||||||||||||||||||
| const missing = [] | ||||||||||||||||||||||||||||||||
| if (!bindingGyp.includes('--no-as-needed,-l:libutil.so.1')) { | ||||||||||||||||||||||||||||||||
| missing.push("binding.gyp is missing the '--no-as-needed,-l:libutil.so.1' ldflag") | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (!ptySource.includes('.symver openpty,openpty@')) { | ||||||||||||||||||||||||||||||||
| missing.push('src/unix/pty.cc is missing the .symver glibc pins') | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (missing.length > 0) { | ||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||||||
| '[orcad-prebuilds] refusing to build: config/patches/node-pty@1.1.0.patch is not applied.', | ||||||||||||||||||||||||||||||||
| ...missing.map((line) => ` - ${line}`), | ||||||||||||||||||||||||||||||||
| 'A prebuilt compiled without it will not load on Ubuntu 20.04 (see', | ||||||||||||||||||||||||||||||||
| 'docs/reference/linux-glibc-compatibility.md and #9902). Run `pnpm install` to apply patches.' | ||||||||||||||||||||||||||||||||
| ].join('\n') | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export function readManifest(prebuildsDir) { | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| return JSON.parse(readFileSync(join(prebuildsDir, 'manifest.json'), 'utf8')) | ||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Why merge rather than overwrite: CI builds one slot per container and merges the trees. | ||||||||||||||||||||||||||||||||
| * A manifest that records only the last slot would erase every other container's record, | ||||||||||||||||||||||||||||||||
| * and `--require-slots` would then reject a complete matrix. | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export function mergeManifest(existing, next) { | ||||||||||||||||||||||||||||||||
| const slots = new Set([...(existing?.slots ?? []), next.slot]) | ||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||
| module: 'node-pty', | ||||||||||||||||||||||||||||||||
| version: next.version, | ||||||||||||||||||||||||||||||||
| nodeAbi: next.nodeAbi, | ||||||||||||||||||||||||||||||||
| slots: [...slots].sort() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
for target in .github config package.json; do
if [ -e "$target" ]; then
rg -n -C 3 'build-orcad-prebuilds|build:orcad-prebuilds|--slot=|--require-slots|node-version|setup-node|out/orcad/prebuilds' "$target"
fi
doneRepository: stablyai/orca Length of output: 39350 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- outline ---'
ast-grep outline config/scripts/build-orcad-prebuilds.mjs
printf '%s\n' '--- implementation ---'
sed -n '1,230p' config/scripts/build-orcad-prebuilds.mjs
printf '%s\n' '--- tests ---'
sed -n '1,180p' config/scripts/build-orcad-prebuilds.test.mjs
printf '%s\n' '--- references ---'
rg -n -C 5 'orcad-prebuilds|MATRIX_SLOTS|require-slots|manifest\.json|nodeAbi|node-pty' .github config package.json
printf '%s\n' '--- behavioral probe ---'
python3 - <<'PY'
from pathlib import Path
import re
source = Path("config/scripts/build-orcad-prebuilds.mjs").read_text()
merge = re.search(
r"export function mergeManifest\(existing, next\) \{(?P<body>.*?)\n\}",
source,
re.S,
)
assert merge, "mergeManifest was not found"
body = merge.group("body")
print("mergeManifest body:")
print(body)
# Evaluate the exact data transformation represented by the function.
existing = {
"module": "node-pty",
"version": "1.1.0",
"nodeAbi": "old-abi",
"slots": ["linux-x64-glibc"],
}
next_manifest = {
"slot": "linux-x64-musl",
"version": "1.1.0",
"nodeAbi": "new-abi",
}
slots = sorted(set([*existing["slots"], next_manifest["slot"]]))
merged = {
"module": "node-pty",
"version": next_manifest["version"],
"nodeAbi": next_manifest["nodeAbi"],
"slots": slots,
}
print("mixed-ABI input:", existing, next_manifest)
print("merged output:", merged)
assert merged["slots"] == ["linux-x64-glibc", "linux-x64-musl"]
assert merged["nodeAbi"] == "new-abi"
# Inspect whether requireSlots compares ABI metadata or only required slot names.
gate_start = source.index("export function requireSlots")
gate_end = source.index("\nif (process.argv[1]", gate_start)
gate = source[gate_start:gate_end]
print("requireSlots implementation:")
print(gate)
print("ABI check present:", "nodeAbi" in gate)
print("slot-membership check present:", "required.every" in gate or ".every(" in gate)
PYRepository: stablyai/orca Length of output: 50370 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- tracked candidate files ---'
git ls-files | rg '(^|/)(build-orcad-prebuilds|.*orcad.*prebuild|.*prebuild.*orcad|.*release.*|.*build.*)\.(mjs|cjs|yml|yaml|json)$' | head -200
printf '%s\n' '--- exact matrix/build/manifest references ---'
rg -n -C 3 --glob '*.mjs' --glob '*.cjs' --glob '*.yml' --glob '*.yaml' --glob '*.json' \
'build-orcad-prebuilds|MATRIX_SLOTS|require-slots|out/orcad/prebuilds|prebuilds/manifest\.json|manifest\.nodeAbi|nodeAbi' \
.github config package.json 2>/dev/null | head -400
printf '%s\n' '--- output cleanup and artifact movement ---'
rg -n -C 3 --glob '*.mjs' --glob '*.cjs' --glob '*.yml' --glob '*.yaml' --glob '*.json' \
'rmSync|rm -rf|remove.*out|clean.*out|upload-artifact|download-artifact|orcad/prebuilds|prebuilds' \
.github config package.json 2>/dev/null | head -500
printf '%s\n' '--- standalone invariant probe ---'
python3 - <<'PY'
from pathlib import Path
import re
path = Path("config/scripts/build-orcad-prebuilds.mjs")
source = path.read_text()
merge_match = re.search(
r"export function mergeManifest\(existing, next\) \{(?P<body>.*?)\n\}",
source,
re.S,
)
if not merge_match:
raise SystemExit("mergeManifest not found")
merge_body = merge_match.group("body")
print("mergeManifest assigns existing nodeAbi:", "existing.nodeAbi" in merge_body)
print("mergeManifest assigns next nodeAbi:", "next.nodeAbi" in merge_body)
print("mergeManifest unions existing slots:", "existing?.slots" in merge_body)
gate_match = re.search(
r"function requireSlots\(\) \{(?P<body>.*?)\n\}",
source,
re.S,
)
if not gate_match:
raise SystemExit("requireSlots not found")
gate_body = gate_match.group("body")
print("requireSlots reads manifest.nodeAbi:", "manifest?.nodeAbi" in gate_body or "manifest.nodeAbi" in gate_body)
print("requireSlots reads per-slot metadata:", "slot" in gate_body and "nodeAbi" in gate_body)
print("requireSlots checks required slot membership:", "MATRIX_SLOTS.filter" in gate_body)
# Model the exact transformation for a partial rebuild after an ABI change.
existing = {
"module": "node-pty",
"version": "1.1.0",
"nodeAbi": "old-abi",
"slots": ["linux-x64-glibc"],
}
next_manifest = {
"slot": "linux-x64-musl",
"version": "1.1.0",
"nodeAbi": "new-abi",
}
merged_slots = sorted(set([*existing["slots"], next_manifest["slot"]]))
merged = {
"module": "node-pty",
"version": next_manifest["version"],
"nodeAbi": next_manifest["nodeAbi"],
"slots": merged_slots,
}
print("modeled merged manifest:", merged)
print("old slot retained:", "linux-x64-glibc" in merged["slots"])
print("old ABI replaced globally:", merged["nodeAbi"] == "new-abi")
print("all required slots could pass membership gate:",
all(slot in merged["slots"] for slot in merged["slots"]))
PYRepository: stablyai/orca Length of output: 49482 Reject mixed Node ABI manifests.
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function nodePtyDir() { | ||||||||||||||||||||||||||||||||
| return dirname(require.resolve('node-pty/package.json')) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function compileNodePty(dir) { | ||||||||||||||||||||||||||||||||
| const built = join(dir, 'build', 'Release', 'pty.node') | ||||||||||||||||||||||||||||||||
| if (existsSync(built)) { | ||||||||||||||||||||||||||||||||
| console.log(`[orcad-prebuilds] reusing existing build at ${built}`) | ||||||||||||||||||||||||||||||||
| return built | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| console.log('[orcad-prebuilds] compiling node-pty from patched source ...') | ||||||||||||||||||||||||||||||||
| const result = spawnSync(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['node-gyp', 'rebuild'], { | ||||||||||||||||||||||||||||||||
| cwd: dir, | ||||||||||||||||||||||||||||||||
| stdio: 'inherit', | ||||||||||||||||||||||||||||||||
| env: process.env, | ||||||||||||||||||||||||||||||||
| windowsHide: true | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| if (result.status !== 0) { | ||||||||||||||||||||||||||||||||
| throw new Error(`[orcad-prebuilds] node-gyp rebuild failed (status ${result.status})`) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if (!existsSync(built)) { | ||||||||||||||||||||||||||||||||
| throw new Error(`[orcad-prebuilds] node-gyp succeeded but ${built} is missing`) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return built | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function requireSlots() { | ||||||||||||||||||||||||||||||||
| const manifest = readManifest(PREBUILDS_DIR) | ||||||||||||||||||||||||||||||||
| const have = new Set(manifest?.slots ?? []) | ||||||||||||||||||||||||||||||||
| const missing = MATRIX_SLOTS.filter((slot) => !have.has(slot)) | ||||||||||||||||||||||||||||||||
| if (missing.length > 0) { | ||||||||||||||||||||||||||||||||
| console.error( | ||||||||||||||||||||||||||||||||
| `[orcad-prebuilds] matrix incomplete — missing ${missing.join(', ')}. ` + | ||||||||||||||||||||||||||||||||
| 'Hosts on those slots fall back to a source build and need a C/C++ toolchain.' | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| process.exitCode = 1 | ||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| console.log(`[orcad-prebuilds] matrix complete — ${MATRIX_SLOTS.length} slots`) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| function build() { | ||||||||||||||||||||||||||||||||
| const dir = nodePtyDir() | ||||||||||||||||||||||||||||||||
| assertNodePtyPatchApplied(dir) | ||||||||||||||||||||||||||||||||
| const slot = slotName() | ||||||||||||||||||||||||||||||||
| const slotDir = join(PREBUILDS_DIR, slot) | ||||||||||||||||||||||||||||||||
| mkdirSync(slotDir, { recursive: true }) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const builtBinary = compileNodePty(dir) | ||||||||||||||||||||||||||||||||
| copyFileSync(builtBinary, join(slotDir, 'pty.node')) | ||||||||||||||||||||||||||||||||
| console.log(`[orcad-prebuilds] stored ${slot}/pty.node`) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Why spawn-helper ships too: on Unix node-pty posix_spawns build/Release/spawn-helper, | ||||||||||||||||||||||||||||||||
| // so a slot without it installs cleanly and then fails ENOENT the first time a user | ||||||||||||||||||||||||||||||||
| // opens a terminal. Windows has no spawn-helper. | ||||||||||||||||||||||||||||||||
| if (process.platform !== 'win32') { | ||||||||||||||||||||||||||||||||
| const helperSource = join(dirname(builtBinary), 'spawn-helper') | ||||||||||||||||||||||||||||||||
| if (!existsSync(helperSource)) { | ||||||||||||||||||||||||||||||||
| throw new Error(`[orcad-prebuilds] spawn-helper missing at ${helperSource}`) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| copyFileSync(helperSource, join(slotDir, 'spawn-helper')) | ||||||||||||||||||||||||||||||||
| console.log(`[orcad-prebuilds] stored ${slot}/spawn-helper`) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // The static floor gate, applied to the artifact we are about to ship rather than only | ||||||||||||||||||||||||||||||||
| // to the packaged desktop app. objdump is Linux-only, which is where the floor lives. | ||||||||||||||||||||||||||||||||
| if (process.platform === 'linux') { | ||||||||||||||||||||||||||||||||
| const { verifyLinuxGlibcFloor } = require('./verify-linux-glibc-floor.cjs') | ||||||||||||||||||||||||||||||||
| verifyLinuxGlibcFloor(slotDir) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const manifest = mergeManifest(readManifest(PREBUILDS_DIR), { | ||||||||||||||||||||||||||||||||
| slot, | ||||||||||||||||||||||||||||||||
| version: require('node-pty/package.json').version, | ||||||||||||||||||||||||||||||||
| nodeAbi: process.versions.modules | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| writeFileSync(join(PREBUILDS_DIR, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`) | ||||||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||||||
| `[orcad-prebuilds] manifest: node-pty ${manifest.version}, ABI ${manifest.nodeAbi}, slots ${manifest.slots.join(', ')}` | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (process.argv[1] && process.argv[1].endsWith('build-orcad-prebuilds.mjs')) { | ||||||||||||||||||||||||||||||||
| if (process.argv.includes('--require-slots')) { | ||||||||||||||||||||||||||||||||
| requireSlots() | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| build() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' | ||
| import { tmpdir } from 'node:os' | ||
| import { join } from 'node:path' | ||
| import { afterEach, describe, expect, it } from 'vitest' | ||
| import { | ||
| assertNodePtyPatchApplied, | ||
| detectLibc, | ||
| MATRIX_SLOTS, | ||
| mergeManifest, | ||
| readManifest, | ||
| slotName | ||
| } from './build-orcad-prebuilds.mjs' | ||
|
|
||
| const PATCHED_BINDING_GYP = "'ldflags': ['-Wl,--no-as-needed,-l:libutil.so.1,-l:libpthread.so.0,--as-needed']" | ||
| const PATCHED_PTY_CC = '__asm__(".symver openpty,openpty@" ORCA_GLIBC_COMPAT_VERSION);' | ||
|
|
||
| const dirs = [] | ||
| const stage = (bindingGyp, ptyCc) => { | ||
| const dir = mkdtempSync(join(tmpdir(), 'orcad-prebuild-src-')) | ||
| dirs.push(dir) | ||
| mkdirSync(join(dir, 'src', 'unix'), { recursive: true }) | ||
| writeFileSync(join(dir, 'binding.gyp'), bindingGyp) | ||
| writeFileSync(join(dir, 'src', 'unix', 'pty.cc'), ptyCc) | ||
| return dir | ||
| } | ||
| afterEach(() => { | ||
| for (const dir of dirs.splice(0)) { | ||
| rmSync(dir, { recursive: true, force: true }) | ||
| } | ||
| }) | ||
|
|
||
| describe('assertNodePtyPatchApplied', () => { | ||
| it('accepts a tree with both halves of the glibc-floor fix', () => { | ||
| expect(() => | ||
| assertNodePtyPatchApplied(stage(PATCHED_BINDING_GYP, PATCHED_PTY_CC)) | ||
| ).not.toThrow() | ||
| }) | ||
|
|
||
| it('refuses to build when the ldflags half is missing', () => { | ||
| // The .symver pins alone let gcc's --as-needed drop libutil/libpthread from | ||
| // DT_NEEDED, which loads on the build host and fails on Ubuntu 20.04 — #9902 again, | ||
| // this time baked into a shipped prebuilt. | ||
| expect(() => assertNodePtyPatchApplied(stage("'ldflags': []", PATCHED_PTY_CC))).toThrow( | ||
| /--no-as-needed,-l:libutil\.so\.1/ | ||
| ) | ||
| }) | ||
|
|
||
| it('refuses to build when the .symver pins are missing', () => { | ||
| expect(() => assertNodePtyPatchApplied(stage(PATCHED_BINDING_GYP, '// upstream'))).toThrow( | ||
| /\.symver glibc pins/ | ||
| ) | ||
| }) | ||
|
|
||
| it('names the patch and the doc so the fix is findable', () => { | ||
| expect(() => assertNodePtyPatchApplied(stage("'ldflags': []", '// upstream'))).toThrow( | ||
| /config\/patches\/node-pty@1\.1\.0\.patch/ | ||
| ) | ||
| }) | ||
| }) | ||
|
|
||
| describe('slot naming', () => { | ||
| it('covers every platform orcad ships to', () => { | ||
| expect([...MATRIX_SLOTS].sort()).toEqual([ | ||
| 'darwin-arm64', | ||
| 'darwin-x64', | ||
| 'linux-arm64-glibc', | ||
| 'linux-arm64-musl', | ||
| 'linux-x64-glibc', | ||
| 'linux-x64-musl' | ||
| ]) | ||
| }) | ||
|
|
||
| it('lets CI force the label so the container decides glibc vs musl', () => { | ||
| // Detection inside a container that happens to run a differently-linked Node would | ||
| // file the build under the wrong slot. The forced label must beat detection outright, | ||
| // so assert against one detection could never produce for this platform/arch. | ||
| expect(slotName(['node', 'x', '--slot=linux-x64-glibc'], 'linux', 'arm64')).toBe( | ||
| 'linux-x64-glibc' | ||
| ) | ||
| }) | ||
|
|
||
| it('omits the libc dimension off Linux', () => { | ||
| expect(slotName([], 'darwin', 'arm64')).toBe('darwin-arm64') | ||
| }) | ||
|
|
||
| it('reads glibc from the report header and musl from its absence', () => { | ||
| expect(detectLibc('linux', { glibcVersionRuntime: '2.31' })).toBe('glibc') | ||
| expect(detectLibc('linux', {})).toBe('musl') | ||
| expect(detectLibc('darwin', { glibcVersionRuntime: '2.31' })).toBe('none') | ||
| }) | ||
| }) | ||
|
|
||
| describe('mergeManifest', () => { | ||
| it('accumulates slots across the per-container CI runs that build them', () => { | ||
| // Overwriting would erase every other container's record, and the release gate would | ||
| // then reject a matrix that is actually complete. | ||
| const first = mergeManifest(null, { slot: 'linux-x64-glibc', version: '1.1.0', nodeAbi: '127' }) | ||
| const second = mergeManifest(first, { | ||
| slot: 'linux-arm64-musl', | ||
| version: '1.1.0', | ||
| nodeAbi: '127' | ||
| }) | ||
|
|
||
| expect(second.slots).toEqual(['linux-arm64-musl', 'linux-x64-glibc']) | ||
| expect(second).toMatchObject({ module: 'node-pty', version: '1.1.0', nodeAbi: '127' }) | ||
| }) | ||
|
|
||
| it('does not duplicate a slot rebuilt twice', () => { | ||
| const once = mergeManifest(null, { slot: 'darwin-arm64', version: '1.1.0', nodeAbi: '127' }) | ||
| expect(mergeManifest(once, { slot: 'darwin-arm64', version: '1.1.0', nodeAbi: '127' }).slots) | ||
| .toEqual(['darwin-arm64']) | ||
| }) | ||
| }) | ||
|
|
||
| describe('readManifest', () => { | ||
| it('returns null instead of throwing when no matrix has been built', () => { | ||
| const dir = mkdtempSync(join(tmpdir(), 'orcad-prebuild-manifest-')) | ||
| dirs.push(dir) | ||
| expect(readManifest(dir)).toBeNull() | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: stablyai/orca
Length of output: 2629
🏁 Script executed:
Repository: stablyai/orca
Length of output: 50372
🏁 Script executed:
Repository: stablyai/orca
Length of output: 50371
🏁 Script executed:
Repository: stablyai/orca
Length of output: 34605
🏁 Script executed:
Repository: stablyai/orca
Length of output: 39373
🏁 Script executed:
Repository: stablyai/orca
Length of output: 50369
🏁 Script executed:
Repository: stablyai/orca
Length of output: 50370
🏁 Script executed:
Repository: stablyai/orca
Length of output: 323
Add Windows slots or reject Windows explicitly.
slotName()returnswin32-x64, butMATRIX_SLOTSomits it.requireSlots()can report a complete matrix while Windows has no matching prebuild. Add the supported Windows slots or reject Windows before building.