fix(add-lang): the extraction check finds codegraph's npm shim on Windows, and takes CODEGRAPH_BIN (#3047)

scripts/add-lang/verify-extraction.mjs ran execFileSync('codegraph', ...).
On Windows, npm installs codegraph as a .cmd shim (so do npm link and
local-install.sh), and Node neither finds a .cmd by bare name nor runs
one without a shell. The check always died with "spawnSync codegraph
ENOENT" (exit 2), and /add-lang's bench.sh read that as broken
extraction and skipped its A/B.

The script now asks, in order:
- CODEGRAPH_BIN, when set. A .js, .mjs or .cjs path runs with the
  current node; anything else runs directly (a .cmd or .bat through
  cmd.exe).
- On Windows, the codegraph Windows itself would pick: the first PATH
  folder holding codegraph under one of PATHEXT's runnable extensions.
  A .cmd or .bat runs through cmd.exe (shell: true) as one command
  string. Each token is quoted for the C runtime and caret-escaped for
  cmd, so spaces, &, parentheses, a %NAME% and a trailing backslash in
  the repo path arrive intact. An .exe or .com needs no shell.
- Otherwise codegraph by bare name, exactly as before. On macOS and
  Linux without CODEGRAPH_BIN, the call and the output are unchanged.

bench.sh takes CODEGRAPH_BIN too. It makes a path absolute (indexing
runs from inside the repo), exports it so verify-extraction.mjs reads
the index with the same build, and runs its own --version and init calls
with it. Without it, bench.sh behaves exactly as before. The A/B's MCP
server stays run-all.sh's own CG_BIN.

Validation (Windows 11, Node 22.22.0):
- New __tests__/add-lang-verify-extraction.test.ts covers CODEGRAPH_BIN
  with a .js stand-in (all platforms), a .cmd shim on PATH with an
  awkward repo path (Windows only), and the bare-name call (macOS and
  Linux only). With main's script both tests that run here fail ("could
  not read codegraph status"); with the fix, 2 pass and 1 is skipped.
- The fixture from PR 1246's review (a codegraph.cmd on PATH): main
  exits 2 with ENOENT; the fix reads the status.
- 13 scenarios with npm's own generated shims, and a real index in a
  folder named "lua repo & co (x86) 100%": main fails every one that
  needs the shim or CODEGRAPH_BIN, and the fix passes them with the
  exact argv reaching codegraph. With no codegraph anywhere, both exit 2
  the same way.
- bench.sh with the A/B stubbed: with npm link's shims on PATH, main's
  copy skips the A/B (verify exit 2) and the fix reaches it (PASS).
  CODEGRAPH_BIN as an absolute or a relative path reaches it too, and a
  missing one stops with a clear message.
- With process.platform reported as linux or darwin, main's script and
  the new one make the same execFileSync call and print the same output.

Raised by PR 1246 (@bslatner).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-30 07:27:10 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4e7a221089
commit 1941caf7bc
3 changed files with 167 additions and 4 deletions
+21 -2
View File
@@ -10,6 +10,10 @@
#
# Usage: bench.sh <lang> <repo-name> <repo-url> "<question>" [headless|tmux|all]
# Env: CORPUS corpus dir (default /tmp/codegraph-corpus, shared with agent-eval)
# CODEGRAPH_BIN index and verify with this codegraph instead of the one on
# PATH: a binary, or a build's dist/bin/codegraph.js (run with
# node). Exported, so verify-extraction.mjs uses it too. The A/B's
# MCP server is run-all.sh's own CG_BIN (default: codegraph on PATH).
set -uo pipefail
LANG_TOKEN="${1:?usage: bench.sh <lang> <repo-name> <repo-url> \"<question>\" [mode]}"
@@ -23,10 +27,25 @@ AGENT_EVAL="$(cd "$HARNESS/../agent-eval" && pwd)"
CORPUS="${CORPUS:-/tmp/codegraph-corpus}"
REPO="$CORPUS/$NAME"
command -v codegraph >/dev/null || { echo "no codegraph on PATH (build + ./scripts/local-install.sh first)"; exit 1; }
if [ -n "${CODEGRAPH_BIN:-}" ]; then
# A path is made absolute, since indexing runs from inside the repo.
if [ -e "$CODEGRAPH_BIN" ]; then
CODEGRAPH_BIN="$(cd "$(dirname "$CODEGRAPH_BIN")" && pwd)/$(basename "$CODEGRAPH_BIN")"
else
command -v "$CODEGRAPH_BIN" >/dev/null || { echo "CODEGRAPH_BIN not found: $CODEGRAPH_BIN"; exit 1; }
fi
export CODEGRAPH_BIN
# Stands in for the PATH codegraph in this script's own calls.
case "$CODEGRAPH_BIN" in
*.js|*.mjs|*.cjs) codegraph() { node "$CODEGRAPH_BIN" "$@"; } ;;
*) codegraph() { "$CODEGRAPH_BIN" "$@"; } ;;
esac
else
command -v codegraph >/dev/null || { echo "no codegraph on PATH (build + ./scripts/local-install.sh first)"; exit 1; }
fi
echo "==================== add-lang bench: $NAME ($LANG_TOKEN) ===================="
echo "codegraph: $(command -v codegraph) -> $(codegraph --version 2>/dev/null || echo '?')"
echo "codegraph: ${CODEGRAPH_BIN:-$(command -v codegraph)} -> $(codegraph --version 2>/dev/null || echo '?')"
# 1. Ensure the repo (shallow clone, reuse if present).
mkdir -p "$CORPUS"
+49 -2
View File
@@ -5,11 +5,15 @@
//
// Usage: node scripts/add-lang/verify-extraction.mjs <repo-path> <lang>
// Reads `codegraph status <repo> --json` using whatever codegraph is on PATH,
// so it reflects the binary that built the index.
// so it reflects the binary that built the index. Set CODEGRAPH_BIN to read it
// with another one: a binary, or a build's dist/bin/codegraph.js, which runs
// with this node. bench.sh exports CODEGRAPH_BIN when it is given one.
//
// Exit codes: 0 = pass or soft-warn, 1 = critical fail, 2 = could not run.
import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import path from 'node:path';
const [repo, lang] = process.argv.slice(2);
if (!repo || !lang) {
@@ -17,9 +21,52 @@ if (!repo || !lang) {
process.exit(2);
}
// On Windows, npm installs codegraph as a .cmd shim (`npm link` too), and
// execFileSync can neither find a .cmd by bare name nor run one without a
// shell. So look where Windows would: the first PATH folder holding codegraph
// under one of PATHEXT's runnable extensions. Only a .cmd or .bat needs the
// shell; an .exe or .com is found by the plain call below on its own.
function findWindowsShim() {
const runnable = ['.com', '.exe', '.bat', '.cmd'];
const exts = (process.env.PATHEXT || runnable.join(';')).toLowerCase().split(';').filter((e) => runnable.includes(e));
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
if (!dir) continue;
for (const ext of exts) {
const file = path.join(dir, `codegraph${ext}`);
if (existsSync(file)) return ext === '.bat' || ext === '.cmd' ? file : null;
}
}
return null;
}
// One token of a command line for cmd.exe, which hands it on to node.exe
// through the shim's %*. Double quotes keep it one argument for the C runtime
// that splits node.exe's command line, so a backslash run before the closing
// quote is doubled (or it would escape that quote). Then every cmd
// metacharacter, the quotes included, gets a caret, so cmd neither splits the
// token nor expands a %NAME% inside it. A Windows path can't contain a double
// quote, so a token with one is refused rather than mangled.
function quoteForCmd(token) {
if (/["\r\n]/.test(token)) throw new Error(`cannot pass ${JSON.stringify(token)} through cmd.exe`);
return `"${token.replace(/(\\+)$/, '$1$1')}"`.replace(/[()[\]%!^"`<>&|;, *?]/g, '^$&');
}
function runCodegraph(args) {
const options = { encoding: 'utf8' };
const bin = process.env.CODEGRAPH_BIN || (process.platform === 'win32' ? findWindowsShim() : null);
if (!bin) return execFileSync('codegraph', args, options);
if (/\.[cm]?js$/i.test(bin)) return execFileSync(process.execPath, [bin, ...args], options);
if (process.platform === 'win32' && /\.(bat|cmd)$/i.test(bin)) {
// A single command string with no args array: Node hands it to cmd.exe
// unchanged, so the quoting above is all there is.
return execFileSync([bin, ...args].map(quoteForCmd).join(' '), { ...options, shell: true });
}
return execFileSync(bin, args, options);
}
let status;
try {
const out = execFileSync('codegraph', ['status', repo, '--json'], { encoding: 'utf8' });
const out = runCodegraph(['status', repo, '--json']);
status = JSON.parse(out);
} catch (e) {
console.error(`[verify] could not read codegraph status for ${repo}: ${e.message}`);