mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
* fix(install): validate and package Linux ARM64 parser replacements * fix(install): add rebuilt ARM64 grammar binaries * fix(install): place ARM64 binaries in versioned bundle paths * refactor(install): share ARM64 prebuild validation * fix(review): keep grammar rebuild provenance accurate * test: pin audited ARM64 repair provenance (#3601)
200 lines
8.1 KiB
JavaScript
200 lines
8.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Prepare the tested native parser dependencies for installation and publication.
|
|
*
|
|
* A dependency's npm overrides do not apply in a consumer project. Several
|
|
* ABI-compatible grammars still advertise older runtime peers, so bundling
|
|
* alone leaves an invalid dependency tree. Admit only the runtime version we
|
|
* tested, in only the audited manifests below. Five audited upstream Linux
|
|
* ARM64 binaries are repaired before packing.
|
|
*/
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { createHash } = require('node:crypto');
|
|
const { isArm64NapiSharedObject } = require('./arm64-prebuild.cjs');
|
|
|
|
const RUNTIME = '0.25.1';
|
|
// Upstream metadata is an input to validate, never the peer range we ship.
|
|
const AUDITED_PEERS = new Map([
|
|
['tree-sitter-c@0.23.6', '^0.22.1'],
|
|
['tree-sitter-cpp@0.23.4', '^0.21.1'],
|
|
['tree-sitter-java@0.23.5', '^0.21.1'],
|
|
['tree-sitter-php@0.24.2', '^0.22.4'],
|
|
['tree-sitter-ruby@0.23.1', '^0.21.1'],
|
|
['tree-sitter-rust@0.24.0', '^0.22.1'],
|
|
['tree-sitter-javascript@0.23.1', '^0.21.1'],
|
|
['tree-sitter-typescript@0.23.2', '^0.21.0'],
|
|
]);
|
|
// These npm releases put x86-64 bytes in their linux-arm64 prebuild directory.
|
|
// Keep the replacement set explicit: upstream upgrades require a new audit.
|
|
const ARM64_REPAIRS = new Set([
|
|
'tree-sitter-cpp@0.23.4',
|
|
'tree-sitter-java@0.23.5',
|
|
'tree-sitter-ruby@0.23.1',
|
|
'tree-sitter-javascript@0.23.1',
|
|
'tree-sitter-typescript@0.23.2',
|
|
]);
|
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
|
|
function readArm64Repairs(packageRoot) {
|
|
const root = path.join(packageRoot, 'prebuilds', 'bundled');
|
|
const manifest = readJson(path.join(root, 'manifest.json'));
|
|
if (
|
|
manifest.schemaVersion !== 1 ||
|
|
manifest.tuple !== 'linux-arm64' ||
|
|
!Array.isArray(manifest.packages) ||
|
|
manifest.packages.length !== ARM64_REPAIRS.size
|
|
) {
|
|
throw new Error('Invalid bundled ARM64 prebuild manifest');
|
|
}
|
|
const repairs = new Map();
|
|
for (const entry of manifest.packages) {
|
|
const identity = `${entry.name}@${entry.version}`;
|
|
const expectedFile = `${identity}/linux-arm64/${entry.name}.node`;
|
|
if (!ARM64_REPAIRS.has(identity) || repairs.has(identity) || entry.file !== expectedFile) {
|
|
throw new Error(`Unexpected bundled ARM64 prebuild identity: ${identity}`);
|
|
}
|
|
const bytes = fs.readFileSync(path.join(root, entry.file));
|
|
if (createHash('sha256').update(bytes).digest('hex') !== entry.sha256) {
|
|
throw new Error(`Bundled ARM64 prebuild checksum mismatch: ${identity}`);
|
|
}
|
|
if (!isArm64NapiSharedObject(bytes)) {
|
|
throw new Error(`Bundled prebuild is not an AArch64 N-API shared object: ${identity}`);
|
|
}
|
|
repairs.set(identity, bytes);
|
|
}
|
|
return repairs;
|
|
}
|
|
|
|
function prepareTreeSitterBundle(
|
|
packageRoot = path.resolve(__dirname, '..'),
|
|
{ forPublication = false } = {},
|
|
) {
|
|
const modulesRoot = path.join(packageRoot, 'node_modules');
|
|
const manifest = readJson(path.join(packageRoot, 'package.json'));
|
|
if (manifest.dependencies['tree-sitter'] !== RUNTIME) {
|
|
throw new Error(`Tree-sitter bundle is only validated for runtime ${RUNTIME}`);
|
|
}
|
|
const grammars = Object.keys(manifest.dependencies).filter(
|
|
(name) => name === 'tree-sitter' || name.startsWith('tree-sitter-'),
|
|
);
|
|
for (const name of grammars) {
|
|
if (
|
|
!Array.isArray(manifest.bundleDependencies) ||
|
|
!manifest.bundleDependencies.includes(name)
|
|
) {
|
|
throw new Error(`${name} must be bundled; consumers do not inherit npm overrides`);
|
|
}
|
|
const installed = readJson(path.join(modulesRoot, name, 'package.json'));
|
|
if (installed.version !== manifest.dependencies[name]) {
|
|
throw new Error(`${name}: installed version differs from the exact package.json pin`);
|
|
}
|
|
}
|
|
|
|
const patches = [];
|
|
const visited = new Set();
|
|
function visit(modules) {
|
|
if (!fs.existsSync(modules)) return;
|
|
modules = fs.realpathSync(modules);
|
|
if (visited.has(modules)) return;
|
|
visited.add(modules);
|
|
for (const entry of fs.readdirSync(modules, { withFileTypes: true })) {
|
|
if (entry.name.startsWith('.')) continue;
|
|
const dir = path.join(modules, entry.name);
|
|
if (
|
|
!entry.isDirectory() &&
|
|
(!entry.isSymbolicLink() || !fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory())
|
|
) {
|
|
continue;
|
|
}
|
|
if (entry.name.startsWith('@')) {
|
|
visit(dir);
|
|
continue;
|
|
}
|
|
const realDir = fs.realpathSync(dir);
|
|
if (visited.has(realDir)) continue;
|
|
visited.add(realDir);
|
|
const file = path.join(realDir, 'package.json');
|
|
if (fs.existsSync(file)) {
|
|
const pkg = readJson(file);
|
|
const original = AUDITED_PEERS.get(`${pkg.name}@${pkg.version}`);
|
|
if (original) {
|
|
// Also accept bundles prepared before peers were pinned exactly, so
|
|
// an existing checkout can migrate through ordinary npm install.
|
|
const accepted = [original, `${original} || ${RUNTIME}`, RUNTIME];
|
|
if (!accepted.includes(pkg.peerDependencies?.['tree-sitter'])) {
|
|
throw new Error(`Unexpected tree-sitter peer metadata for ${pkg.name}@${pkg.version}`);
|
|
}
|
|
pkg.peerDependencies['tree-sitter'] = RUNTIME;
|
|
patches.push({ file, pkg });
|
|
}
|
|
}
|
|
visit(path.join(realDir, 'node_modules'));
|
|
// pnpm links packages into node_modules but places their dependencies
|
|
// beside the real package. Follow that container without traversing the
|
|
// entire virtual store; realpaths deduplicate aliases and break cycles.
|
|
let parent = path.dirname(realDir);
|
|
if (path.basename(parent).startsWith('@')) parent = path.dirname(parent);
|
|
if (path.basename(parent) === 'node_modules') visit(parent);
|
|
}
|
|
}
|
|
visit(modulesRoot);
|
|
const identities = new Set(patches.map(({ pkg }) => `${pkg.name}@${pkg.version}`));
|
|
if (patches.length !== AUDITED_PEERS.size || identities.size !== AUDITED_PEERS.size) {
|
|
throw new Error('Tree-sitter dependency layout changed; revalidate the audited peer manifests');
|
|
}
|
|
|
|
const repairs = readArm64Repairs(packageRoot);
|
|
const binaries = [];
|
|
const shadows = [];
|
|
for (const { pkg, file } of patches) {
|
|
const dir = path.dirname(file);
|
|
const bytes = repairs.get(`${pkg.name}@${pkg.version}`);
|
|
if (!bytes) continue;
|
|
const target = path.join(dir, 'prebuilds', 'linux-arm64', `${pkg.name}.node`);
|
|
// A changed upstream layout needs review rather than creating a second
|
|
// candidate that node-gyp-build may not select.
|
|
const candidates = fs
|
|
.readdirSync(path.dirname(target))
|
|
.filter((name) => name.endsWith('.node'));
|
|
if (candidates.length !== 1 || candidates[0] !== path.basename(target)) {
|
|
throw new Error(`Unexpected ARM64 prebuild layout for ${pkg.name}@${pkg.version}`);
|
|
}
|
|
binaries.push({ target, bytes });
|
|
if (forPublication) {
|
|
// node-gyp-build prefers local build outputs over prebuilds, even when
|
|
// those outputs were compiled on the publisher's other architecture.
|
|
// Preserve legitimate consumer source builds during ordinary postinstall.
|
|
for (const mode of ['Release', 'Debug']) {
|
|
const build = path.join(dir, 'build', mode);
|
|
if (fs.existsSync(build)) {
|
|
for (const name of fs.readdirSync(build)) {
|
|
if (name.endsWith('.node')) shadows.push(path.join(build, name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate the complete set before writing, so dependency drift cannot leave
|
|
// a partially prepared bundle behind. Repeated pack operations are idempotent.
|
|
for (const { file, pkg } of patches) {
|
|
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');
|
|
}
|
|
for (const { target, bytes } of binaries) {
|
|
if (!fs.readFileSync(target).equals(bytes)) fs.writeFileSync(target, bytes);
|
|
}
|
|
for (const file of shadows) fs.unlinkSync(file);
|
|
return patches.map(({ pkg }) => `${pkg.name}@${pkg.version}`);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const prepared = prepareTreeSitterBundle(undefined, {
|
|
forPublication: process.argv.includes('--for-publication'),
|
|
});
|
|
console.log(
|
|
`[tree-sitter-bundle] Validated runtime ${RUNTIME}; prepared ${prepared.length} peer manifests.`,
|
|
);
|
|
}
|
|
|
|
module.exports = { prepareTreeSitterBundle };
|