| 1 | const fs = require("fs"); |
| 2 | const { execFileSync } = require("child_process"); |
| 3 | |
| 4 | const GLIBC_VERSION_RE = /GLIBC_(\d+)\.(\d+)(?:\.(\d+))?/g; |
| 5 | |
| 6 | function isLinux() { |
| 7 | return process.platform === "linux"; |
| 8 | } |
| 9 | |
| 10 | function parseVersion(text) { |
| 11 | const match = String(text || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/); |
| 12 | if (!match) return null; |
| 13 | return [Number(match[1]), Number(match[2]), Number(match[3] || 0)]; |
| 14 | } |
| 15 | |
| 16 | function compareVersion(a, b) { |
| 17 | for (let i = 0; i < 3; i += 1) { |
| 18 | if (a[i] !== b[i]) return a[i] - b[i]; |
| 19 | } |
| 20 | return 0; |
| 21 | } |
| 22 | |
| 23 | function formatVersion(version) { |
| 24 | return version[2] ? `${version[0]}.${version[1]}.${version[2]}` : `${version[0]}.${version[1]}`; |
| 25 | } |
| 26 | |
| 27 | function detectHostGlibc() { |
| 28 | try { |
| 29 | const out = execFileSync("getconf", ["GNU_LIBC_VERSION"], { |
| 30 | encoding: "utf8", |
| 31 | stdio: ["ignore", "pipe", "ignore"], |
| 32 | }); |
| 33 | const version = parseVersion(out); |
| 34 | if (version) return version; |
| 35 | } catch { |
| 36 | // fall through to ldd |
| 37 | } |
| 38 | try { |
| 39 | const out = execFileSync("ldd", ["--version"], { |
| 40 | encoding: "utf8", |
| 41 | stdio: ["ignore", "pipe", "ignore"], |
| 42 | }); |
| 43 | const firstLine = out.split("\n", 1)[0]; |
| 44 | const version = parseVersion(firstLine); |
| 45 | if (version) return version; |
| 46 | } catch { |
| 47 | // glibc not present (e.g. musl / Alpine) |
| 48 | } |
| 49 | return null; |
| 50 | } |
| 51 | |
| 52 | function detectBinaryRequiredGlibc(filePath) { |
| 53 | const buf = fs.readFileSync(filePath); |
| 54 | const text = buf.toString("latin1"); |
| 55 | let highest = null; |
| 56 | GLIBC_VERSION_RE.lastIndex = 0; |
| 57 | let match; |
| 58 | while ((match = GLIBC_VERSION_RE.exec(text)) !== null) { |
| 59 | const version = [Number(match[1]), Number(match[2]), Number(match[3] || 0)]; |
| 60 | if (!highest || compareVersion(version, highest) > 0) { |
| 61 | highest = version; |
| 62 | } |
| 63 | } |
| 64 | return highest; |
| 65 | } |
| 66 | |
| 67 | function buildFromSourceHint() { |
| 68 | return [ |
| 69 | "You can still run DeepSeek TUI by building from source with Cargo:", |
| 70 | "", |
| 71 | " # Requires Rust 1.88+ (https://rustup.rs)", |
| 72 | " cargo install deepseek-tui-cli --locked # provides `deepseek`", |
| 73 | " cargo install deepseek-tui --locked # provides `deepseek-tui`", |
| 74 | "", |
| 75 | "Or build from a checkout:", |
| 76 | "", |
| 77 | " git clone https://github.com/Hmbown/DeepSeek-TUI.git", |
| 78 | " cd DeepSeek-TUI", |
| 79 | " cargo install --path crates/cli --locked", |
| 80 | " cargo install --path crates/tui --locked", |
| 81 | "", |
| 82 | "See https://github.com/Hmbown/DeepSeek-TUI/blob/main/docs/INSTALL.md", |
| 83 | ].join("\n"); |
| 84 | } |
| 85 | |
| 86 | function preflightGlibc(filePath) { |
| 87 | if (!isLinux()) return; |
| 88 | if ( |
| 89 | process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK === "1" || |
| 90 | process.env.DEEPSEEK_SKIP_GLIBC_CHECK === "1" |
| 91 | ) { |
| 92 | return; |
| 93 | } |
| 94 | |
| 95 | const required = detectBinaryRequiredGlibc(filePath); |
| 96 | if (!required) { |
| 97 | // Statically linked / musl binary, or no GLIBC_* version dependencies present. |
| 98 | return; |
| 99 | } |
| 100 | |
| 101 | const host = detectHostGlibc(); |
| 102 | if (!host) { |
| 103 | throw new Error( |
| 104 | [ |
| 105 | `The prebuilt binary requires GLIBC_${formatVersion(required)}, but no GNU libc was detected on this host.`, |
| 106 | "This usually means you're on a musl-based distro such as Alpine.", |
| 107 | "", |
| 108 | buildFromSourceHint(), |
| 109 | "", |
| 110 | "Set DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.", |
| 111 | ].join("\n"), |
| 112 | ); |
| 113 | } |
| 114 | |
| 115 | if (compareVersion(host, required) < 0) { |
| 116 | throw new Error( |
| 117 | [ |
| 118 | `Prebuilt DeepSeek TUI binary requires GLIBC_${formatVersion(required)} but this system has glibc ${formatVersion(host)}.`, |
| 119 | "Older distros (CentOS 7/8, RHEL 7/8, Debian 10, etc.) ship an older glibc that is not compatible with the prebuilt artifact.", |
| 120 | "", |
| 121 | buildFromSourceHint(), |
| 122 | "", |
| 123 | "Set DEEPSEEK_TUI_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.", |
| 124 | ].join("\n"), |
| 125 | ); |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | module.exports = { |
| 130 | preflightGlibc, |
| 131 | detectHostGlibc, |
| 132 | detectBinaryRequiredGlibc, |
| 133 | // exported for tests |
| 134 | _internal: { parseVersion, compareVersion, formatVersion }, |
| 135 | }; |
| 136 |