Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,15 @@ jobs:
run: npm install

- name: Install native addon over published binary
# Pass NATIVE_BUILD_VERSION so the script also rewrites the platform
# package.json's version to match the binary's CARGO_PKG_VERSION
# (build-native bumps Cargo.toml to this same value before building).
# Without this, the JS-side getNativePackageVersion() returns the
# last-published version while the binary reports the bumped version,
# and the Rust orchestrator's check_version_mismatch forces every
# incremental rebuild back through the full pipeline (#1066).
env:
NATIVE_BUILD_VERSION: ${{ needs.compute-version.outputs.version }}
run: node scripts/ci-install-native.mjs

# Build dist/ so benchmarks load the same compiled JS that ships to npm.
Expand Down
61 changes: 59 additions & 2 deletions scripts/ci-install-native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
* Used by the CI `test` and `parity` jobs so they exercise the native engine
* built from the PR's Rust source rather than the last-published binary,
* which lags behind PR changes and causes false parity failures.
*
* Also rewrites the platform package's `package.json` `version` field to
* match the just-built binary's `CARGO_PKG_VERSION`. Without this step the
* JS-side `getNativePackageVersion()` returns the published version while
* the binary reports the bumped version, and the Rust orchestrator's
* check_version_mismatch then forces every incremental rebuild back through
* the full pipeline (~2s floor in #1066).
*
* The version is read from `NATIVE_BUILD_VERSION` if set (use this when the
* artifact was built from a workflow that bumped Cargo.toml — e.g.
* publish.yml's build-native job sets it from compute-version output),
* falling back to `crates/codegraph-core/Cargo.toml` for flows that build
* locally without a version bump.
*/

import fs from 'node:fs';
Expand Down Expand Up @@ -59,8 +72,52 @@ if (built.length > 1) {

const src = built[0];
const pkg = resolvePackage();
const dest = path.join('node_modules', pkg, 'codegraph-core.node');
const destDir = path.join('node_modules', pkg);
const dest = path.join(destDir, 'codegraph-core.node');

fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.mkdirSync(destDir, { recursive: true });
fs.copyFileSync(src, dest);
console.log(`[ci-install-native] copied ${src} -> ${dest}`);

// Resolve the binary's CARGO_PKG_VERSION. We can't read it from the .node
// directly, so we accept it via env var (preferred) or fall back to the
// Cargo.toml on disk — which is correct for flows that build the artifact
// in the same checkout (no version bump between Cargo read and build).
function resolveBinaryVersion() {
const envVersion = process.env.NATIVE_BUILD_VERSION?.trim();
if (envVersion) return envVersion;
const cargoPath = path.join('crates', 'codegraph-core', 'Cargo.toml');
try {
const cargoToml = fs.readFileSync(cargoPath, 'utf8');
// Match the first `version = "X.Y.Z"` after the [package] header so we
// don't accidentally pick up a dependency's version pin.
const pkgSection = cargoToml.split(/^\[/m)[1] ?? cargoToml;
const m = pkgSection.match(/version\s*=\s*"([^"]+)"/);
return m?.[1] ?? null;
} catch (e) {
console.warn(`[ci-install-native] failed to read ${cargoPath}: ${e.message}`);
return null;
}
}

const binaryVersion = resolveBinaryVersion();
const pkgJsonPath = path.join(destDir, 'package.json');
if (binaryVersion && fs.existsSync(pkgJsonPath)) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
const prev = pkgJson.version;
if (prev !== binaryVersion) {
pkgJson.version = binaryVersion;
fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
console.log(
`[ci-install-native] updated ${pkgJsonPath} version: ${prev} -> ${binaryVersion}`,
);
} else {
console.log(
`[ci-install-native] ${pkgJsonPath} version already ${binaryVersion} — no rewrite needed`,
);
}
} else if (!binaryVersion) {
console.warn(
'[ci-install-native] could not resolve binary version (NATIVE_BUILD_VERSION unset and Cargo.toml unreadable) — leaving platform package.json untouched',
);
}
10 changes: 10 additions & 0 deletions src/domain/graph/builder/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export class PipelineContext {
engineOpts!: EngineOpts;
engineName!: 'native' | 'wasm';
engineVersion!: string | null;
/**
* The version reported by the native binary itself (CARGO_PKG_VERSION at
* build time), as opposed to `engineVersion` which prefers the platform
* package.json. The Rust orchestrator's check_version_mismatch compares
* `build_meta.engine_version` against CARGO_PKG_VERSION, so build_meta
* writes must use this value to avoid a perpetual full-rebuild loop when
* the binary and platform package.json drift apart (e.g., CI hot-swap
* via ci-install-native.mjs — #1066).
*/
nativeBinaryVersion!: string | null;
aliases!: PathAliases;
incremental!: boolean;
forceFullRebuild: boolean = false;
Expand Down
45 changes: 30 additions & 15 deletions src/domain/graph/builder/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@ function initializeEngine(ctx: PipelineContext): void {
suspendJsDb: undefined,
resumeJsDb: undefined,
};
const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
const {
name: engineName,
version: engineVersion,
binaryVersion: nativeBinaryVersion,
} = getActiveEngine(ctx.engineOpts);
ctx.engineName = engineName as 'native' | 'wasm';
ctx.engineVersion = engineVersion;
ctx.nativeBinaryVersion = nativeBinaryVersion;
info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
}

Expand All @@ -105,13 +110,15 @@ function checkEngineSchemaMismatch(ctx: PipelineContext): void {
);
ctx.forceFullRebuild = true;
}
// When the native engine is active, the Rust addon's version (ctx.engineVersion)
// is written into codegraph_version by setBuildMeta after a native orchestrator
// build. The check must compare against the same version, otherwise JS and Rust
// fight over which version to record — causing every incremental build to be
// promoted to a full rebuild when npm and crate versions diverge.
// When the native engine is active, the Rust orchestrator writes
// build_meta.codegraph_version = CARGO_PKG_VERSION (the binary's own value).
// Compare against the same value here so a CI hot-swap that leaves the
// platform package.json behind doesn't trigger a perpetual full-rebuild
// loop on every incremental (#1066).
const effectiveVersion =
ctx.engineName === 'native' && ctx.engineVersion ? ctx.engineVersion : CODEGRAPH_VERSION;
ctx.engineName === 'native' && ctx.nativeBinaryVersion
? ctx.nativeBinaryVersion
: CODEGRAPH_VERSION;
const prevVersion = meta('codegraph_version');
if (prevVersion && prevVersion !== effectiveVersion) {
info(
Expand Down Expand Up @@ -665,16 +672,24 @@ async function tryNativeOrchestrator(
const p = result.phases;

// Sync build_meta so JS-side version/engine checks work on next build.
// Use the Rust addon version as codegraph_version when the native
// orchestrator performed the build — the Rust side's check_version_mismatch
// compares this value against CARGO_PKG_VERSION. Writing the JS
// CODEGRAPH_VERSION here would create a permanent mismatch whenever the
// npm package version diverges from the Rust crate version, forcing every
// subsequent native build to be a full rebuild (no incremental).
// Use the binary's CARGO_PKG_VERSION (ctx.nativeBinaryVersion), not the
// platform package.json version (ctx.engineVersion). The Rust side's
// check_version_mismatch compares against CARGO_PKG_VERSION; writing
// the package.json value would create a permanent mismatch whenever
// the binary and platform package.json diverge — e.g., CI hot-swap
// via ci-install-native.mjs (#1066) — forcing every subsequent build
// to be a full rebuild.
//
// When the native addon doesn't expose engineVersion() (older addon),
// fall back to CODEGRAPH_VERSION — same fallback used by both
// checkEngineSchemaMismatch (read path) and persistBuildMetadata
// (the JS-pipeline write path in finalize.ts). Using ctx.engineVersion
// here would re-introduce the asymmetry this PR fixes for that case.
const nativeVersionForMeta = ctx.nativeBinaryVersion || CODEGRAPH_VERSION;
setBuildMeta(ctx.db, {
engine: ctx.engineName,
engine_version: ctx.engineVersion || '',
codegraph_version: ctx.engineVersion || CODEGRAPH_VERSION,
engine_version: nativeVersionForMeta,
codegraph_version: nativeVersionForMeta,
schema_version: String(ctx.schemaVersion),
built_at: new Date().toISOString(),
});
Expand Down
15 changes: 9 additions & 6 deletions src/domain/graph/builder/stages/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,16 @@ function persistBuildMetadata(
): void {
const useNativeDb = ctx.engineName === 'native' && !!ctx.nativeDb;
if (!ctx.isFullBuild && ctx.allSymbols.size <= 3) return;
// When the native engine is active, persist the Rust addon version so that
// checkEngineSchemaMismatch compares against the same value on the next build.
// Writing CODEGRAPH_VERSION (the npm package version) here would create a
// permanent mismatch whenever npm and crate versions diverge, forcing every
// subsequent build to be a full rebuild.
// When the native engine is active, persist the binary's CARGO_PKG_VERSION
// (ctx.nativeBinaryVersion). The Rust orchestrator's check_version_mismatch
// compares against that exact value, so writing the platform package.json
// version (ctx.engineVersion) — which can drift from the binary in CI
// hot-swap flows (#1066) — would force every subsequent native build to
// be a full rebuild.
const codeVersionToWrite =
ctx.engineName === 'native' && ctx.engineVersion ? ctx.engineVersion : CODEGRAPH_VERSION;
ctx.engineName === 'native' && ctx.nativeBinaryVersion
? ctx.nativeBinaryVersion
: CODEGRAPH_VERSION;
// Persist the repo root so downstream commands (e.g. `codegraph embed`)
// can resolve relative file paths regardless of the invoking cwd.
// Use realpathSync (symlink-resolving) to match the Rust engine's
Expand Down
18 changes: 10 additions & 8 deletions src/domain/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,23 +1197,25 @@ export async function parseFilesAuto(
export function getActiveEngine(opts: ParseEngineOpts = {}): {
name: 'native' | 'wasm';
version: string | null;
binaryVersion: string | null;
} {
const { name, native } = resolveEngine(opts);
let version: string | null = native
? typeof native.engineVersion === 'function'
? native.engineVersion()
: null
: null;
// Prefer platform package.json version over binary-embedded version
// to handle stale binaries that weren't recompiled during a release
const binaryVersion: string | null =
native && typeof native.engineVersion === 'function' ? native.engineVersion() : null;
// The display version prefers the platform package.json so the "Using native
// engine (vX)" log matches the npm release the user installed. The Rust
// orchestrator's check_version_mismatch compares against CARGO_PKG_VERSION
// (the binary's own value), so build_meta writes must use `binaryVersion`,
// not this display value — see pipeline.ts and finalize.ts (#1066).
let version: string | null = binaryVersion;
if (native) {
try {
version = getNativePackageVersion() ?? version;
} catch (e: unknown) {
debug(`getNativePackageVersion failed: ${(e as Error).message}`);
}
}
return { name, version };
return { name, version, binaryVersion };
}

/**
Expand Down
Loading