test(linux): judge per-arch vendored binaries against their own path

The first CI run of the architecture gate failed the x64 package job on
`@parcel/watcher-linux-arm64-glibc/watcher.node`. That binary is arm64 on
purpose: the package ships every architecture and its loader picks the match,
so its presence in an x64 build is correct.

Judge a binary against the architecture its own path names, falling back to
the slice when the path names none. That keeps the case this gate exists for
-- `bin/linux-arm64-*/node-pty.node` holding an x86-64 binary, which is what
shipped to a Raspberry Pi 5 -- while letting multi-arch dependencies through.

Dry-run over the real dependency tree flags nothing for either target arch.
This commit is contained in:
Neil
2026-09-01 22:55:16 -07:00
parent 264e69e7ce
commit eed8b33f69
2 changed files with 57 additions and 3 deletions
+30 -3
View File
@@ -199,8 +199,28 @@ function readElfMachine(filePath) {
}
}
// Arch tokens that appear in vendored per-architecture package/directory names.
const ARCH_TOKEN_PATTERN = /(?:^|[^a-z0-9])(arm64|aarch64|x64|x86_64)(?:[^a-z0-9]|$)/i
const ARCH_BY_TOKEN = Object.freeze({ arm64: 'arm64', aarch64: 'arm64', x64: 'x64', x86_64: 'x64' })
/**
* The architecture a path advertises, or null when it advertises none.
*
* Why this matters: some dependencies ship every architecture and let their loader pick
* (`@parcel/watcher-linux-arm64-glibc/watcher.node` is arm64 on purpose inside an x64 build). Those
* must be judged against the arch their own path declares, not against the slice.
*/
function declaredArchFromPath(filePath) {
const match = ARCH_TOKEN_PATTERN.exec(filePath)
return match ? ARCH_BY_TOKEN[match[1].toLowerCase()] : null
}
function findArchViolation(filePath, targetArch) {
const expected = ELF_MACHINE_BY_ARCH[targetArch]
// A path that names an architecture is judged against that name, so a per-arch vendored package
// is fine while `bin/linux-arm64-.../node-pty.node` holding an x86-64 binary is still caught.
const declared = declaredArchFromPath(filePath)
const expectedArch = declared ?? targetArch
const expected = ELF_MACHINE_BY_ARCH[expectedArch]
if (expected === undefined) {
return null
}
@@ -208,7 +228,12 @@ function findArchViolation(filePath, targetArch) {
if (machine === null || machine === expected) {
return null
}
return { machine, actual: ARCH_BY_ELF_MACHINE[machine] ?? `0x${machine.toString(16)}` }
return {
machine,
actual: ARCH_BY_ELF_MACHINE[machine] ?? `0x${machine.toString(16)}`,
expectedArch,
declared: declared !== null
}
}
function isElfFile(filePath) {
@@ -384,7 +409,8 @@ function verifyLinuxGlibcFloor(rootDir, options = {}) {
const detail = archOffenders
.map(
({ filePath, violation }) =>
` ${relative(rootDir, filePath) || filePath} is ${violation.actual}`
` ${relative(rootDir, filePath) || filePath} is ${violation.actual}, expected ` +
`${violation.expectedArch}${violation.declared ? ' (from its own path)' : ''}`
)
.join('\n')
throw new Error(
@@ -446,6 +472,7 @@ module.exports = {
MIN_GLIBC,
ELF_MACHINE_BY_ARCH,
readElfMachine,
declaredArchFromPath,
findArchViolation,
VERSION_FLOORS,
FLOOR_LABEL,
@@ -7,6 +7,7 @@ import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const {
readElfMachine,
declaredArchFromPath,
findArchViolation,
ELF_MACHINE_BY_ARCH,
parseGlibcVersion,
@@ -348,6 +349,32 @@ describe('bundled native binary architecture', () => {
// The observed failure: cross-building arm64 on an x64 host packed an x86-64 pty.node, whose
// symbol versions are valid, so every other gate here passed it.
// Real CI hit: @parcel/watcher ships every architecture and its loader picks the match, so the
// arm64 copy is present in an x64 build on purpose.
it('accepts a per-arch vendored package that matches its own path', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const pkg = join(dir, '@parcel', 'watcher-linux-arm64-glibc')
await mkdir(pkg, { recursive: true })
const file = join(pkg, 'watcher.node')
await writeFile(file, elfHeader(ELF_MACHINE_BY_ARCH.arm64))
expect(declaredArchFromPath(file)).toBe('arm64')
expect(findArchViolation(file, 'x64')).toBeNull()
await rm(dir, { recursive: true, force: true })
})
// But a path that names an arch must actually hold it — this is the Pi 5 failure.
it('flags a binary that contradicts the architecture its own path names', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const nested = join(dir, 'bin', 'linux-arm64-148')
await mkdir(nested, { recursive: true })
const file = join(nested, 'node-pty.node')
await writeFile(file, elfHeader(ELF_MACHINE_BY_ARCH.x64))
expect(findArchViolation(file, 'arm64')).toMatchObject({ actual: 'x64', expectedArch: 'arm64' })
// Still caught even when the slice being built is x64.
expect(findArchViolation(file, 'x64')).toMatchObject({ actual: 'x64', expectedArch: 'arm64' })
await rm(dir, { recursive: true, force: true })
})
it('flags an x86-64 binary in an arm64 slice', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-elf-arch-'))
const file = join(dir, 'pty.node')