fix: deliver the static -portable linux binary on all install/update paths

The standard linux release binary dynamically links glibc 2.38+ and
GLIBCXX_3.4.32, so it fails to start on Debian 11, Ubuntu 20.04/22.04,
RHEL/Rocky 8/9, Amazon Linux 2, etc. — yet install.sh, the npm and PyPI
wrappers, and the binary's own self-update all fetched it by default,
contradicting the "single static binary" promise.

Point every linux install + self-update path at the fully-static
"-portable" asset (gcc -static), which has no glibc floor. macOS/Windows
are unaffected and unchanged.

- install.sh, pkg/npm/install.js, pkg/pypi _cli.py: select -portable on linux
- src/cli/cli.c: self-update download URL AND checksum archive name both use
  -portable on linux (they must match or the update fails checksum verify)
- scripts/smoke-test.sh: assert linux self-update targets the -portable asset
- scripts/ci/check-glibc-compat.sh: new guard — runs the binary inside
  debian:bullseye (glibc 2.31) and asserts it starts
- _smoke.yml: run the guard on the portable binary in smoke-linux-portable

Reproduced: standard binary -> "GLIBC_2.38 not found" on glibc 2.31;
portable binary runs cleanly.
This commit is contained in:
Martin Vogel committed 2026-06-09 11:36:00 +02:00
1 parent 16103d9e39
commit 69c16cada4
7 files changed
+69 -8

No files matched your search

+7
View File
@@ -189,6 +189,13 @@ jobs:
chmod +x codebase-memory-mcp
scripts/smoke-test.sh ./codebase-memory-mcp
# The -portable binary is what all linux install/update paths now deliver;
# it MUST start on old glibc (Debian 11 / RHEL 8 / Ubuntu 20.04). Runs it
# in debian:bullseye (glibc 2.31) — the standard dynamic binary would fail
# here with `GLIBC_2.38 not found`.
- name: Old-glibc compatibility
run: scripts/ci/check-glibc-compat.sh ./codebase-memory-mcp
- name: Security audits
run: |
scripts/security-strings.sh ./codebase-memory-mcp
+8 -2
View File
@@ -97,10 +97,16 @@ else
EXT="tar.gz"
fi
# Linux ships a fully-static "-portable" build; the standard linux binary
# dynamically links glibc 2.38+ and fails on older distros (Debian 11, RHEL 8,
# Ubuntu 20.04). macOS/Windows have no such variant.
PORTABLE=""
[ "$OS" = "linux" ] && PORTABLE="-portable"
if [ "$VARIANT" = "ui" ]; then
ARCHIVE="codebase-memory-mcp-ui-${OS}-${ARCH}.${EXT}"
ARCHIVE="codebase-memory-mcp-ui-${OS}-${ARCH}${PORTABLE}.${EXT}"
else
ARCHIVE="codebase-memory-mcp-${OS}-${ARCH}.${EXT}"
ARCHIVE="codebase-memory-mcp-${OS}-${ARCH}${PORTABLE}.${EXT}"
fi
URL="${CBM_DOWNLOAD_URL}/${ARCHIVE}"
+5 -1
View File
@@ -105,7 +105,11 @@ async function main() {
fs.mkdirSync(BIN_DIR, { recursive: true });
const archive = `codebase-memory-mcp-${platform}-${arch}.${ext}`;
// Linux ships a fully-static "-portable" build; the standard linux binary
// dynamically links glibc 2.38+ and fails on older distros. macOS/Windows
// have no such variant. Keep in sync with install.sh / pypi _cli.py / cli.c.
const variant = platform === 'linux' ? '-portable' : '';
const archive = `codebase-memory-mcp-${platform}-${arch}${variant}.${ext}`;
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archive}`;
process.stdout.write(`codebase-memory-mcp: downloading v${VERSION} for ${platform}/${arch}...\n`);
+5 -1
View File
@@ -153,7 +153,11 @@ def _download(version: str) -> Path:
os_name = _os_name()
arch = _arch()
ext = "zip" if os_name == "windows" else "tar.gz"
archive = f"codebase-memory-mcp-{os_name}-{arch}.{ext}"
# Linux ships a fully-static "-portable" build; the standard linux binary
# dynamically links glibc 2.38+ and fails on older distros. macOS/Windows
# have no such variant. Keep in sync with install.sh / install.js / cli.c.
variant = "-portable" if os_name == "linux" else ""
archive = f"codebase-memory-mcp-{os_name}-{arch}{variant}.{ext}"
url = f"https://github.com/{REPO}/releases/download/v{version}/{archive}"
_validate_url_scheme(url)
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Regression guard: the linux binary we ship must START on OLD glibc.
#
# The standard linux release binary dynamically links glibc 2.38+ / GLIBCXX
# 3.4.32 and fails to start on Debian 11, RHEL/Rocky 8, Ubuntu 20.04, Amazon
# Linux 2, etc. The fix points all linux install + self-update paths at the
# fully-static "-portable" asset. This runs a given linux binary inside an
# old-glibc container (debian:bullseye, glibc 2.31) and asserts it starts:
# - RED for the dynamic standard binary (GLIBC_2.38 not found)
# - GREEN for the static -portable binary (runs anywhere)
#
# Usage: check-glibc-compat.sh <path-to-linux-binary>
# Env: GLIBC_TEST_IMAGE (default: debian:bullseye-slim, glibc 2.31)
set -euo pipefail
BIN="${1:?usage: check-glibc-compat.sh <path-to-linux-binary>}"
IMAGE="${GLIBC_TEST_IMAGE:-debian:bullseye-slim}"
BIN_ABS="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")"
echo "==> running $(basename "$BIN") --version inside ${IMAGE} (glibc 2.31)"
docker run --rm -v "${BIN_ABS}:/cbm:ro" "${IMAGE}" /cbm --version
echo "PASS: binary starts on old glibc (${IMAGE})"
+11
View File
@@ -584,6 +584,17 @@ if ! echo "$UPDATE_OUT" | grep -qi 'standard'; then
echo "FAIL: update --dry-run did not respect --standard flag"
exit 1
fi
# On Linux the binary must self-update from the static "-portable" asset: the
# standard linux asset dynamically links glibc 2.38+ and breaks on older distros
# (Debian 11, RHEL 8, Ubuntu 20.04). Guards build_update_url in src/cli/cli.c.
if [ "$(uname -s)" = "Linux" ]; then
if ! echo "$UPDATE_OUT" | grep -q -- '-portable'; then
echo "FAIL: linux update --dry-run does not target the -portable asset"
echo "$UPDATE_OUT"
exit 1
fi
echo "OK: linux update targets the -portable (static) asset"
fi
echo "OK: update --dry-run --standard completed"
# 6d: config set/get/reset round-trip
+11 -4
View File
@@ -3761,8 +3761,13 @@ static void build_update_url(char *url, int url_sz, const char *os, const char *
if (!base_url || !base_url[0]) {
base_url = "https://github.com/DeusData/codebase-memory-mcp/releases/latest/download";
}
snprintf(url, url_sz, "%s/codebase-memory-mcp-%s%s-%s.%s", base_url, want_ui ? "ui-" : "", os,
arch, ext);
/* Linux ships a fully-static "-portable" build; the standard linux binary
* dynamically links glibc 2.38+ and fails on older distros. macOS/Windows
* have no such variant. Keep in sync with install.sh / install.js / pypi
* _cli.py. */
const char *portable = (strcmp(os, "linux") == 0) ? "-portable" : "";
snprintf(url, url_sz, "%s/codebase-memory-mcp-%s%s-%s%s.%s", base_url, want_ui ? "ui-" : "", os,
arch, portable, ext);
}
/* Prompt to delete existing indexes. Returns 0 to continue, 1 to abort. */
@@ -3801,8 +3806,10 @@ static int download_verify_install(const char *url, const char *ext, const char
}
char archive_name[CLI_BUF_256];
snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s%s-%s.%s",
want_ui ? "ui-" : "", os, arch, ext);
/* Must match build_update_url: linux uses the static "-portable" asset. */
const char *portable = (strcmp(os, "linux") == 0) ? "-portable" : "";
snprintf(archive_name, sizeof(archive_name), "codebase-memory-mcp-%s%s-%s%s.%s",
want_ui ? "ui-" : "", os, arch, portable, ext);
int crc = verify_download_checksum(tmp_archive, archive_name);
if (crc == CLI_TRUE) {
cbm_unlink(tmp_archive);