From 3179b552d48be190e940c6edece5151ff2170b31 Mon Sep 17 00:00:00 2001 From: Vishnu <104626273+Vishnu1837@users.noreply.github.com> Date: Mon, 4 May 2026 12:48:26 +0530 Subject: [PATCH] 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 --- npm/deepseek-tui/scripts/install.js | 2 + npm/deepseek-tui/scripts/preflight-glibc.js | 135 ++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 npm/deepseek-tui/scripts/preflight-glibc.js diff --git a/npm/deepseek-tui/scripts/install.js b/npm/deepseek-tui/scripts/install.js index b1310094..3f23703b 100644 --- a/npm/deepseek-tui/scripts/install.js +++ b/npm/deepseek-tui/scripts/install.js @@ -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; diff --git a/npm/deepseek-tui/scripts/preflight-glibc.js b/npm/deepseek-tui/scripts/preflight-glibc.js new file mode 100644 index 00000000..b18a03b5 --- /dev/null +++ b/npm/deepseek-tui/scripts/preflight-glibc.js @@ -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 }, +};