feat(npm): glibc preflight check on Linux postinstall (#560) (#565)

Linux installs currently succeed even when the host glibc is older than
what the prebuilt binary requires, leaving the user with a cryptic
`GLIBC_2.XX not found` runtime error.

Add a Linux-only preflight in `scripts/preflight-glibc.js` that runs
right after checksum verification:

- Read the highest required `GLIBC_X.Y` symbol from the downloaded
  binary by scanning its bytes (no readelf dependency).
- Detect host glibc via `getconf GNU_LIBC_VERSION`, falling back to
  `ldd --version`.
- If host < required, throw with a clear message pointing at the
  build-from-source path (cargo install / git clone instructions).
- If glibc cannot be detected at all (musl/Alpine), surface the same
  guidance instead of installing an incompatible binary.
- Skipped on macOS/Windows. `DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1` (or the
  legacy `DEEPSEEK_SKIP_GLIBC_CHECK=1`) bypasses the check.

The downloaded file is unlinked on failure, so a failed preflight
leaves nothing behind and npm exits non-zero.

Closes #560
This commit is contained in:
Vishnu
2026-05-04 12:48:26 +05:30
committed by GitHub
parent 0047b3225b
commit 3179b552d4
2 changed files with 137 additions and 0 deletions
+2
View File
@@ -13,6 +13,7 @@ const {
releaseAssetUrl,
releaseBinaryDirectory,
} = require("./artifacts");
const { preflightGlibc } = require("./preflight-glibc");
const pkg = require("../package.json");
function resolvePackageVersion() {
@@ -154,6 +155,7 @@ async function ensureBinary(targetPath, assetName, version, repo, getChecksums)
await download(url, destination);
try {
await verifyChecksum(destination, assetName, checksums);
preflightGlibc(destination);
} catch (error) {
await unlink(destination).catch(() => {});
throw error;
+135
View File
@@ -0,0 +1,135 @@
const fs = require("fs");
const { execFileSync } = require("child_process");
const GLIBC_VERSION_RE = /GLIBC_(\d+)\.(\d+)(?:\.(\d+))?/g;
function isLinux() {
return process.platform === "linux";
}
function parseVersion(text) {
const match = String(text || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/);
if (!match) return null;
return [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
}
function compareVersion(a, b) {
for (let i = 0; i < 3; i += 1) {
if (a[i] !== b[i]) return a[i] - b[i];
}
return 0;
}
function formatVersion(version) {
return version[2] ? `${version[0]}.${version[1]}.${version[2]}` : `${version[0]}.${version[1]}`;
}
function detectHostGlibc() {
try {
const out = execFileSync("getconf", ["GNU_LIBC_VERSION"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
const version = parseVersion(out);
if (version) return version;
} catch {
// fall through to ldd
}
try {
const out = execFileSync("ldd", ["--version"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
const firstLine = out.split("\n", 1)[0];
const version = parseVersion(firstLine);
if (version) return version;
} catch {
// glibc not present (e.g. musl / Alpine)
}
return null;
}
function detectBinaryRequiredGlibc(filePath) {
const buf = fs.readFileSync(filePath);
const text = buf.toString("latin1");
let highest = null;
GLIBC_VERSION_RE.lastIndex = 0;
let match;
while ((match = GLIBC_VERSION_RE.exec(text)) !== null) {
const version = [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
if (!highest || compareVersion(version, highest) > 0) {
highest = version;
}
}
return highest;
}
function buildFromSourceHint() {
return [
"You can still run DeepSeek TUI by building from source with Cargo:",
"",
" # Requires Rust 1.85+ (https://rustup.rs)",
" cargo install deepseek-tui-cli --locked # provides `deepseek`",
" cargo install deepseek-tui --locked # provides `deepseek-tui`",
"",
"Or build from a checkout:",
"",
" git clone https://github.com/Hmbown/DeepSeek-TUI.git",
" cd DeepSeek-TUI",
" cargo install --path crates/cli --locked",
" cargo install --path crates/tui --locked",
"",
"See https://github.com/Hmbown/DeepSeek-TUI/blob/main/docs/INSTALL.md",
].join("\n");
}
function preflightGlibc(filePath) {
if (!isLinux()) return;
if (
process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK === "1" ||
process.env.DEEPSEEK_SKIP_GLIBC_CHECK === "1"
) {
return;
}
const required = detectBinaryRequiredGlibc(filePath);
if (!required) {
// Statically linked / musl binary, or no GLIBC_* version dependencies present.
return;
}
const host = detectHostGlibc();
if (!host) {
throw new Error(
[
`The prebuilt binary requires GLIBC_${formatVersion(required)}, but no GNU libc was detected on this host.`,
"This usually means you're on a musl-based distro such as Alpine.",
"",
buildFromSourceHint(),
"",
"Set DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.",
].join("\n"),
);
}
if (compareVersion(host, required) < 0) {
throw new Error(
[
`Prebuilt DeepSeek TUI binary requires GLIBC_${formatVersion(required)} but this system has glibc ${formatVersion(host)}.`,
"Older distros (CentOS 7/8, RHEL 7/8, Debian 10, etc.) ship an older glibc that is not compatible with the prebuilt artifact.",
"",
buildFromSourceHint(),
"",
"Set DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.",
].join("\n"),
);
}
}
module.exports = {
preflightGlibc,
detectHostGlibc,
detectBinaryRequiredGlibc,
// exported for tests
_internal: { parseVersion, compareVersion, formatVersion },
};