| 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 codewhale by building from source with Cargo:", |
| 70 | "", |
| 71 | " # Requires Rust 1.88+ (https://rustup.rs)", |
| 72 | " cargo install codewhale-cli --locked # provides `codewhale` and `codew`", |
| 73 | " cargo install codewhale-tui --locked # provides `codewhale-tui`", |
| 74 | "", |
| 75 | "Or build from a checkout:", |
| 76 | "", |
| 77 | " git clone https://github.com/Hmbown/CodeWhale.git", |
| 78 | " cd CodeWhale", |
| 79 | " cargo install --path crates/cli --locked", |
| 80 | " cargo install --path crates/tui --locked", |
| 81 | "", |
| 82 | "See https://github.com/Hmbown/CodeWhale/blob/main/docs/INSTALL.md", |
| 83 | ].join("\n"); |
| 84 | } |
| 85 | |
| 86 | function skipGlibcCheck() { |
| 87 | return ( |
| 88 | process.env.CODEWHALE_SKIP_GLIBC_CHECK === "1" || |
| 89 | process.env.DEEPSEEK_TUI_SKIP_GLIBC_CHECK === "1" || |
| 90 | process.env.DEEPSEEK_SKIP_GLIBC_CHECK === "1" |
| 91 | ); |
| 92 | } |
| 93 | |
| 94 | function glibcCompatibilityMessage(required, host) { |
| 95 | const hostLine = host |
| 96 | ? `this system has glibc ${formatVersion(host)}, which is too old for that asset.` |
| 97 | : "this system does not appear to provide GNU libc."; |
| 98 | return [ |
| 99 | `Prebuilt Codewhale Linux binaries require GLIBC_${formatVersion(required)}, but ${hostLine}`, |
| 100 | "", |
| 101 | "The Linux x64 release asset is a static (musl) build that runs on any glibc,", |
| 102 | "but the Linux arm64 asset is a GNU libc build linked against", |
| 103 | "Ubuntu 24.04/glibc 2.39, which Ubuntu 22.04 (glibc 2.35) cannot run.", |
| 104 | "", |
| 105 | buildFromSourceHint(), |
| 106 | "", |
| 107 | "Set CODEWHALE_SKIP_GLIBC_CHECK=1 to bypass this check at your own risk.", |
| 108 | ].join("\n"); |
| 109 | } |
| 110 | |
| 111 | function preflightGlibc(filePath) { |
| 112 | if (!isLinux()) return; |
| 113 | if (skipGlibcCheck()) { |
| 114 | return; |
| 115 | } |
| 116 | |
| 117 | const required = detectBinaryRequiredGlibc(filePath); |
| 118 | if (!required) { |
| 119 | // Statically linked / musl binary, or no GLIBC_* version dependencies present. |
| 120 | return; |
| 121 | } |
| 122 | |
| 123 | const host = detectHostGlibc(); |
| 124 | if (!host) { |
| 125 | throw new Error(glibcCompatibilityMessage(required, null)); |
| 126 | } |
| 127 | |
| 128 | if (compareVersion(host, required) < 0) { |
| 129 | throw new Error(glibcCompatibilityMessage(required, host)); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | module.exports = { |
| 134 | preflightGlibc, |
| 135 | detectHostGlibc, |
| 136 | detectBinaryRequiredGlibc, |
| 137 | // exported for tests |
| 138 | _internal: { |
| 139 | parseVersion, |
| 140 | compareVersion, |
| 141 | formatVersion, |
| 142 | glibcCompatibilityMessage, |
| 143 | skipGlibcCheck, |
| 144 | }, |
| 145 | }; |
| 146 |