fix: replace postinstall script with binary resolution in ast-grep (#2595)

* fix: replace postinstall script with binary resolution in ast-grep

* fix: restrict package name detection
This commit is contained in:
Mohamad Mohebifar
2026-05-02 13:41:45 -07:00
committed by GitHub
parent 673b6c0fbf
commit 333e195155
4 changed files with 152 additions and 59 deletions
Regular → Executable
+149 -3
View File
@@ -1,3 +1,149 @@
echo "ast-grep shim file was executed. This file exists so that npm tries to create the binary. It should be replaced after the 'postinstall' script is executed."
echo "Did your package manager execute the post-install script? (N.B. not all of them do)"
exit 1
#!/usr/bin/env node
const { spawn } = require("node:child_process");
const fs = require("fs");
const path = require("path");
const binaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
function detectPackageName() {
const { platform, arch } = process;
switch (platform) {
case "darwin":
if (arch === "arm64") return "@ast-grep/cli-darwin-arm64";
if (arch === "x64") return "@ast-grep/cli-darwin-x64";
break;
case "linux": {
const { MUSL, familySync } = require("detect-libc");
if (familySync() === MUSL) return null;
if (arch === "arm64") return "@ast-grep/cli-linux-arm64-gnu";
if (arch === "x64") return "@ast-grep/cli-linux-x64-gnu";
break;
}
case "win32":
if (arch === "arm64") return "@ast-grep/cli-win32-arm64-msvc";
if (arch === "ia32") return "@ast-grep/cli-win32-ia32-msvc";
if (arch === "x64") return "@ast-grep/cli-win32-x64-msvc";
break;
default:
break;
}
return null;
}
function failToLocateBinary(pkgName, originalError) {
const messages = ["Failed to locate the ast-grep native binary."];
if (pkgName) {
messages.push(`Expected package: ${pkgName}`);
} else {
messages.push(
"This platform does not have a published prebuilt ast-grep binary.",
);
}
if (originalError) {
messages.push(`Underlying error: ${originalError.message}`);
}
messages.push(
"If you are installing from npm, ensure optional dependencies are enabled (e.g. do not use --no-optional).",
);
messages.push(
"For local development, run `cargo build --release` before invoking this script.",
);
console.error(messages.join("\n"));
process.exit(1);
}
function resolveBinaryPath() {
const pkgName = detectPackageName();
let moduleError = null;
if (pkgName) {
try {
const pkgDir = path.dirname(
require.resolve(`${pkgName}/package.json`, { paths: [__dirname] }),
);
const candidate = path.join(pkgDir, binaryName);
if (fs.existsSync(candidate)) {
return { binaryPath: candidate, pkgName };
}
} catch (err) {
moduleError = err;
}
}
const releaseCandidate = path.join(
__dirname,
"..",
"target",
"release",
binaryName,
);
if (fs.existsSync(releaseCandidate)) {
return { binaryPath: releaseCandidate, pkgName: null };
}
const debugCandidate = path.join(
__dirname,
"..",
"target",
"debug",
binaryName,
);
if (fs.existsSync(debugCandidate)) {
return { binaryPath: debugCandidate, pkgName: null };
}
failToLocateBinary(pkgName, moduleError);
}
function attachChildLifecycle(child) {
child.on("error", (error) => {
console.error("Failed to execute ast-grep binary.");
console.error(error);
process.exit(1);
});
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
const forwarders = signals.map((signal) => {
const handler = () => {
if (child.killed) {
return;
}
try {
child.kill(signal);
} catch (_) {
// Ignore
}
};
process.on(signal, handler);
return { signal, handler };
});
child.on("exit", (code, signal) => {
forwarders.forEach(({ signal, handler }) => {
process.removeListener(signal, handler);
});
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code == null ? 1 : code);
}
});
}
function main() {
const { binaryPath } = resolveBinaryPath();
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: "inherit",
});
attachChildLifecycle(child);
}
main();
+1 -5
View File
@@ -20,15 +20,11 @@
],
"files": [
"sg",
"ast-grep",
"postinstall.js"
"ast-grep"
],
"dependencies": {
"detect-libc": "2.1.2"
},
"scripts": {
"postinstall": "node postinstall.js"
},
"optionalDependencies": {
"@ast-grep/cli-win32-arm64-msvc": "0.42.1",
"@ast-grep/cli-win32-ia32-msvc": "0.42.1",
-49
View File
@@ -1,49 +0,0 @@
let fs = require('fs');
let path = require('path');
let parts = [process.platform, process.arch];
if (process.platform === 'linux') {
const {MUSL, familySync} = require('detect-libc');
if (familySync() === MUSL) {
parts.push('musl');
} else if (process.arch === 'arm') {
parts.push('gnueabihf');
} else {
parts.push('gnu');
}
} else if (process.platform === 'win32') {
parts.push('msvc');
}
let binary = process.platform === 'win32' ? 'ast-grep.exe' : 'ast-grep';
let alternative = process.platform === 'win32' ? 'sg.exe' : 'sg';
let pkgPath;
try {
pkgPath = path.dirname(require.resolve(`@ast-grep/cli-${parts.join('-')}/package.json`));
} catch (err) {
pkgPath = path.join(__dirname, '..', 'target', 'release');
if (!fs.existsSync(path.join(pkgPath, binary))) {
pkgPath = path.join(__dirname, '..', 'target', 'debug');
}
}
try {
fs.linkSync(path.join(pkgPath, binary), path.join(__dirname, binary));
fs.linkSync(path.join(pkgPath, binary), path.join(__dirname, alternative));
} catch (err) {
try {
fs.copyFileSync(path.join(pkgPath, binary), path.join(__dirname, binary));
fs.copyFileSync(path.join(pkgPath, binary), path.join(__dirname, alternative));
} catch (err) {
console.error('Failed to move @ast-grep/cli binary into place.');
process.exit(1);
}
}
if (process.platform === 'win32') {
try {
fs.unlinkSync(path.join(__dirname, 'sg'));
fs.unlinkSync(path.join(__dirname, 'ast-grep'));
} catch (err) {}
}
Regular → Executable
+2 -2
View File
@@ -1,2 +1,2 @@
This file is required so that dumb npm creates the sg binary.
N.B. pnpm does not have this issue.
#!/usr/bin/env node
require("./ast-grep");