feat(install): vendor tree-sitter-c at 0.21.4 with GitNexus-built prebuilds (#2116)

tree-sitter-c is the one grammar dependency upstream ships incomplete prebuilds
for (4/6 — no linux-arm64/win32-arm64), AND it is a REQUIRED grammar: its own
`install` (node-gyp-build) compiles from source when no prebuild matches and
exits non-zero, so on a toolchain-less ARM host `npm install gitnexus` HARD-FAILS
at the c step — during npm's dependency phase, before any GitNexus postinstall
runs (so a postinstall "supplement" can't help).

Fix: vendor c prebuild-only at the pinned 0.21.4 (Kotlin pattern), with all six
prebuilds GitNexus-cross-built, and drop it from `dependencies`:
- vendor/tree-sitter-c/ (bindings + node-types + manifest + prebuilds); build
  probe scripts/build-tree-sitter-c.cjs; added to the build workflow registry
  (kind 'npm' — built from c@0.21.4 source).
- materialize-vendor-grammars.cjs: c is REQUIRED, so it is always materialized,
  even under GITNEXUS_SKIP_OPTIONAL_GRAMMARS (it needs no toolchain).
- Removed from package.json dependencies + lockfile (nothing else needs npm c —
  tree-sitter-cpp's dep on c is dev-only and not installed). Preserves the #1242
  ABI pin: vendoring 0.21.4 keeps the good ABI while closing the ARM gap.
- parser-loader note + the prebuild-coverage guard + a cli-commands assertion
  updated; c moves from the npm-gap allow-list into the vendored 6/6 cohort.

Verified: tsc clean, 31 unit tests pass, c loads/parses; the guard is RED for
c/dart/proto/kotlin until the workflow populates prebuilds (the must-fill gate).
Closes the operational risk in #2116.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-09 10:27:07 +00:00
parent ebbaa4a7ae
commit 5370ef5ff6
15 changed files with 4762 additions and 45 deletions

View file

@ -60,6 +60,7 @@ on:
branches: [main]
paths:
# Vendored grammars: their version lives in the vendor snapshot package.json.
- 'gitnexus/vendor/tree-sitter-c/package.json'
- 'gitnexus/vendor/tree-sitter-dart/package.json'
- 'gitnexus/vendor/tree-sitter-proto/package.json'
- 'gitnexus/vendor/tree-sitter-kotlin/package.json'
@ -117,6 +118,11 @@ jobs:
// 'vendored' builds from gitnexus/vendor/<name> (which carries the C
// source + binding.gyp). Extend this list to cover a new grammar.
const REGISTRY = {
// c is vendored prebuild-only but BUILT from the published npm
// package (kind 'npm'), held at 0.21.4 — it closes upstream's 4/6
// ARM gap (#2116) for a REQUIRED grammar that otherwise hard-fails
// install on toolchain-less ARM.
c: { name: 'tree-sitter-c', kind: 'npm' },
dart: { name: 'tree-sitter-dart', kind: 'vendored' },
proto: { name: 'tree-sitter-proto', kind: 'vendored' },
kotlin: { name: 'tree-sitter-kotlin', kind: 'npm' },
@ -317,6 +323,7 @@ jobs:
// Catch an emulated x64 Node silently mis-passing on an arm64 runner.
if (process.arch !== expect) throw new Error(`runner arch ${process.arch} != ${expect}`);
const snippets = {
c: "int main(void) { return 0; }",
dart: "void main() { print(\"hi\"); }",
proto: "syntax = \"proto3\";\nmessage M { int32 id = 1; }",
kotlin: "fun main() { println(\"hi\") }",

View file

@ -33,7 +33,6 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"tree-sitter": "0.21.1",
"tree-sitter-c": "0.21.4",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.2",
"tree-sitter-go": "^0.23.0",
@ -4970,25 +4969,6 @@
"node-gyp-build": "^4.8.0"
}
},
"node_modules/tree-sitter-c": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.21.4.tgz",
"integrity": "sha512-IahxFIhXiY15SUlrt2upBiKSBGdOaE1fjKLK1Ik5zxqGHf6T1rvr3IJrovbsE5sXhypx7Hnmf50gshsppaIihA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.1"
},
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-c-sharp": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz",

View file

@ -49,7 +49,7 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:cross-platform": "tsx scripts/run-cross-platform.ts",
"postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs && node scripts/build-tree-sitter-swift.cjs && node scripts/build-tree-sitter-kotlin.cjs",
"postinstall": "node scripts/materialize-vendor-grammars.cjs && node scripts/build-tree-sitter-c.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs && node scripts/build-tree-sitter-swift.cjs && node scripts/build-tree-sitter-kotlin.cjs",
"prepare": "node scripts/build.js",
"prepack": "node scripts/build.js"
},
@ -77,7 +77,6 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"tree-sitter": "0.21.1",
"tree-sitter-c": "0.21.4",
"tree-sitter-c-sharp": "0.23.1",
"tree-sitter-cpp": "0.23.2",
"tree-sitter-go": "^0.23.0",

View file

@ -0,0 +1,38 @@
#!/usr/bin/env node
/**
* Probe tree-sitter-c prebuild availability at install time.
*
* tree-sitter-c is vendored prebuild-only (like swift/kotlin), held at 0.21.4
* for ABI compatibility with the bundled tree-sitter@0.21.1 runtime (#1242).
* It is vendored — rather than left as a plain npm dependency — because upstream
* ships prebuilds for only 4 of 6 platform-archs (#2116) and tree-sitter-c is a
* REQUIRED grammar whose source build hard-fails `npm install` on a toolchain-less
* ARM host. GitNexus cross-builds all six prebuilds (build-tree-sitter-prebuilds
* workflow) and materialize-vendor-grammars.cjs copies them into node_modules/;
* node-gyp-build selects the right binary at require time.
*
* This probe calls node-gyp-build once so a missing/unloadable prebuild surfaces
* as a single install-time warning rather than a first-use runtime error. It
* MUST NEVER throw or exit non-zero — it must never break `gitnexus` install.
*/
const fs = require('fs');
const path = require('path');
// No GITNEXUS_SKIP_OPTIONAL_GRAMMARS gate: tree-sitter-c is REQUIRED and always
// materialized (it is not a user-opt-out grammar), so we always verify it.
const cDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-c');
try {
if (!fs.existsSync(path.join(cDir, 'bindings', 'node', 'index.js'))) {
process.exit(0);
}
const nodeGypBuild = require('node-gyp-build');
nodeGypBuild(cDir);
} catch (err) {
console.warn('[tree-sitter-c] Prebuild probe failed:', err.message);
console.warn(
'[tree-sitter-c] C parsing will be unavailable (no prebuild matches this platform-arch). Other languages are unaffected.',
);
process.exit(0);
}

View file

@ -13,19 +13,28 @@ const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const VENDORED_GRAMMARS = [
// tree-sitter-c is a REQUIRED grammar that we vendor prebuild-only purely to
// close upstream's ARM prebuild gap (#2116) — it needs no toolchain and is not a
// language the user opts out of, so it is always materialized, even under
// GITNEXUS_SKIP_OPTIONAL_GRAMMARS. The rest are optional (user-skippable, and
// Dart/Proto compile from source) and honor the skip flag.
const REQUIRED_VENDORED = ['tree-sitter-c'];
const OPTIONAL_VENDORED = [
'tree-sitter-dart',
'tree-sitter-proto',
'tree-sitter-swift',
'tree-sitter-kotlin',
];
if (process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1') {
const skipOptional = process.env.GITNEXUS_SKIP_OPTIONAL_GRAMMARS === '1';
if (skipOptional) {
console.warn(
'[gitnexus] Skipping vendored grammar materialize (GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1). Dart/Proto/Swift/Kotlin parsing will be unavailable.',
'[gitnexus] GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1: skipping optional Dart/Proto/Swift/Kotlin materialize (required C is still materialized).',
);
process.exit(0);
}
const VENDORED_GRAMMARS = skipOptional
? REQUIRED_VENDORED
: [...REQUIRED_VENDORED, ...OPTIONAL_VENDORED];
for (const name of VENDORED_GRAMMARS) {
const src = path.join(ROOT, 'vendor', name);

View file

@ -121,25 +121,26 @@ const SOURCES: Record<string, GrammarSource> = {
'Vue parsing piggybacks on `tree-sitter-typescript`. Check the install and native binding.',
},
// tree-sitter-c is a required dependency, but its native binding has
// historically been ABI-incompatible with the bundled tree-sitter@0.21.1
// runtime on some platforms (#1242, #858). Loading it through the
// optional machinery turns a would-be segfault into a clean degradation
// while preserving every other language's analysis. Severity is pinned
// to `error` because the package is in `dependencies`: a failure here
// is always an install/platform problem the user needs to see, never an
// expected "user opted out" condition like Swift/Dart/Kotlin.
// tree-sitter-c is a core grammar, vendored prebuild-only (under
// gitnexus/vendor/tree-sitter-c) with GitNexus-built prebuilds for every
// supported platform-arch — upstream ships only 4/6 (#2116) and C is a
// required grammar whose source build hard-fails install on a toolchain-less
// ARM host. Loading through the optional machinery turns a would-be ABI
// segfault (#1242, #858) into a clean degradation while preserving every
// other language's analysis. Severity stays `error` because C is not a
// user-opt-out grammar like Swift/Dart/Kotlin: a failure here is always an
// install/platform problem the user needs to see.
[SupportedLanguages.C]: {
load: () => _require('tree-sitter-c'),
optional: true,
severity: 'error',
unavailableNote:
'C parsing disabled: `tree-sitter-c` could not be loaded. ' +
'This package is in `dependencies` and prebuilds ship for all supported ' +
'platforms (win32/darwin/linux x64+arm64, Node 18/20/22), so this ' +
'usually indicates a corrupted install, an unsupported Node version, ' +
'or a native ABI mismatch with the bundled tree-sitter runtime. ' +
'Try `npm rebuild tree-sitter-c` or reinstalling, then re-run analyze. ' +
'C parsing disabled: vendored `tree-sitter-c` (under ' +
'`gitnexus/vendor/tree-sitter-c`) could not be loaded. GitNexus ships ' +
'prebuilt binaries for all supported platforms (win32/darwin/linux ' +
'x64+arm64, N-API), so this usually indicates a corrupted install or a ' +
'native ABI mismatch with the bundled tree-sitter@0.21.1 runtime. ' +
'Try reinstalling, then re-run analyze. ' +
`If the failure persists, file details at ${ISSUES_URL}/1242.`,
},

View file

@ -117,6 +117,21 @@ describe('CLI commands', () => {
expect(kotlinPkg.default.dependencies).toBeUndefined();
expect(kotlinPkg.default.peerDependencies['tree-sitter']).toContain('^0.21');
});
it('vendors tree-sitter-c prebuild-only at the 0.21.4 ABI pin instead of an npm dependency (#2116/#1242)', async () => {
const pkg = await import('../../package.json', { with: { type: 'json' } });
const cPkg = await import('../../vendor/tree-sitter-c/package.json', {
with: { type: 'json' },
});
// c is a REQUIRED grammar that hard-fails install on toolchain-less ARM
// (upstream ships 4/6). Vendored with GitNexus-built prebuilds for all 6,
// held at 0.21.4 for ABI safety (#1242) — so it is NOT an npm dependency.
expect(pkg.default.dependencies['tree-sitter-c']).toBeUndefined();
expect(pkg.default.scripts.postinstall).toContain('build-tree-sitter-c.cjs');
expect(cPkg.default.version).toBe('0.21.4');
expect(cPkg.default.scripts?.install).toBeUndefined();
expect(cPkg.default.dependencies).toBeUndefined();
});
});
describe('analyzeCommand', () => {

View file

@ -46,12 +46,11 @@ const NODE_MODULES = path.join(GITNEXUS_ROOT, 'node_modules');
* grammar drops MORE than its allow-listed gap (a new silent regression) OR if
* an allow-listed gap is closed upstream (prompting allow-list removal).
*
* tree-sitter-c@0.21.4 ships only 4/6 (no linux-arm64 / win32-arm64); pinned to
* the 0.21.x line pending the tree-sitter 0.21->0.23 runtime upgrade. See #2116.
* (tree-sitter-c@0.21.4 ships only 4/6 — no linux-arm64/win32-arm64, #2116 — but
* it is now VENDORED with GitNexus-built prebuilds for all 6, so it falls under
* the vendored cohort below, not here.)
*/
const KNOWN_NPM_GAPS: Record<string, string[]> = {
'tree-sitter-c': ['linux-arm64', 'win32-arm64'],
};
const KNOWN_NPM_GAPS: Record<string, string[]> = {};
function isNapiBinary(file: string): boolean {
return readFileSync(file).includes(NAPI_SYMBOL);

21
gitnexus/vendor/tree-sitter-c/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Max Brunsfeld
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

39
gitnexus/vendor/tree-sitter-c/README.md vendored Normal file
View file

@ -0,0 +1,39 @@
## GitNexus vendor notice
This directory is a GitNexus-managed minimal **runtime** package derived from
`tree-sitter-c@0.21.4` (tree-sitter/tree-sitter-c). It carries only the runtime
files (`bindings/node/`, `src/node-types.json`, `LICENSE`) plus the native
`prebuilds/`. The C source (`parser.c`, `binding.gyp`) is not vendored — the
prebuilds are produced from the published npm package.
### Why this is vendored (unlike the other npm grammars)
`tree-sitter-c` is the one grammar dependency upstream ships **incomplete**
prebuilds for: only 4 of 6 platform-archs (no `linux-arm64` / `win32-arm64`,
[#2116](https://github.com/abhigyanpatwari/GitNexus/issues/2116)). And unlike
the optional grammars, `tree-sitter-c` is a **required** grammar whose own
`install` script (`node-gyp-build`) compiles from source when no prebuild
matches — which **hard-fails `npm install`** on a toolchain-less ARM host
(`node-gyp rebuild` exits non-zero for a required dependency). To make C parsing
toolchain-free on every platform, GitNexus builds all six prebuilds itself (via
the `build-tree-sitter-prebuilds` workflow) and vendors them; `node-gyp-build`
selects the right `.node` at require time.
### Held at 0.21.4 (do not bump here)
The version is pinned to **0.21.4** for ABI compatibility with the bundled
`tree-sitter@0.21.1` runtime — `tree-sitter-c@0.23.x` prebuilds segfault under
0.21.1 on Windows ([#1242](https://github.com/abhigyanpatwari/GitNexus/issues/1242),
[#858](https://github.com/abhigyanpatwari/GitNexus/issues/858)). Vendoring 0.21.4
*preserves* that pin while closing the ARM prebuild gap. Bump only as part of the
deliberate tree-sitter 0.21→0.23 runtime upgrade.
### Updating this vendor package
1. (Runtime upgrade only) bump `version` in `package.json` + refresh
`bindings/node/*` and `src/node-types.json` from the new `tree-sitter-c`
release, and refresh `_vendoredBy`.
2. Regenerate the six prebuilds by running the **`build-tree-sitter-prebuilds`**
workflow.
3. Verify the packed tarball can `require('tree-sitter-c')` and parse C on each
target platform-arch (the workflow's validate step does this in CI).

View file

@ -0,0 +1,28 @@
type BaseNode = {
type: string;
named: boolean;
};
type ChildNode = {
multiple: boolean;
required: boolean;
types: BaseNode[];
};
type NodeInfo =
| (BaseNode & {
subtypes: BaseNode[];
})
| (BaseNode & {
fields: { [name: string]: ChildNode };
children: ChildNode[];
});
type Language = {
name: string;
language: unknown;
nodeTypeInfo: NodeInfo[];
};
declare const language: Language;
export = language;

View file

@ -0,0 +1,7 @@
const root = require("path").join(__dirname, "..", "..");
module.exports = require("node-gyp-build")(root);
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View file

@ -0,0 +1,18 @@
{
"name": "tree-sitter-c",
"version": "0.21.4",
"description": "C grammar for tree-sitter",
"repository": "https://github.com/tree-sitter/tree-sitter-c",
"license": "MIT",
"main": "bindings/node/index.js",
"types": "bindings/node/index.d.ts",
"_vendoredBy": "gitnexus - minimal runtime package derived from tree-sitter-c@0.21.4 (tree-sitter/tree-sitter-c). HELD at 0.21.4 for ABI compatibility with the bundled tree-sitter@0.21.1 runtime (#1242/#858) — do not bump without the runtime upgrade. Vendored prebuild-only because upstream ships native prebuilds for only 4 of 6 platforms (no linux-arm64/win32-arm64, #2116), and tree-sitter-c is a REQUIRED grammar whose source build hard-fails `npm install` on a toolchain-less ARM host; GitNexus cross-builds all six via .github/workflows/build-tree-sitter-prebuilds.yml. Copied to node_modules/ by materialize-vendor-grammars.cjs; prebuild activation via build-tree-sitter-c.cjs (no scripts.install here — #836/#1728).",
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
}

View file

File diff suppressed because it is too large Load diff