diff --git a/.github/workflows/_smoke.yml b/.github/workflows/_smoke.yml index ac4ca595d..4c26391a0 100644 --- a/.github/workflows/_smoke.yml +++ b/.github/workflows/_smoke.yml @@ -41,12 +41,13 @@ jobs: # Broad legs are REQUIRED gates (no optional / continue-on-error): # every smoke leg must pass. No `optional` flags anywhere. # - # NOTE: the *dynamic* linux binary links glibc 2.38+ and cannot run on - # older distros by design — older-glibc coverage is the -portable (static) - # binary's job, exercised green by the smoke-linux-portable broad legs - # (ubuntu-22.04 / 22.04-arm). So the dynamic broad legs stay on - # forward-compatible OSes only (macOS); running the dynamic binary on - # ubuntu-22.04 would fail Phase 1 (glibc too old), not a real regression. + # NOTE: the *dynamic* linux binary is built in a pinned Ubuntu 22.04 + # container (_build.yml build-linux) and links glibc 2.35+; the + # composition gate (A6) asserts that floor on the built bytes, so no + # broad leg re-runs it on a 22.04 runner. Older-glibc coverage stays + # the -portable (static) binary's job, exercised green by the + # smoke-linux-portable broad legs (ubuntu-22.04 / 22.04-arm) and the + # glibc 2.31 check below. # Empty since the move off macos-14 (retired 2026-11-02): the darwin-arm64 # artifact is now built AND smoked on macos-15, the core leg, which was # this broad leg. @@ -329,8 +330,8 @@ jobs: # 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`. + # in debian:bullseye (glibc 2.31) — the standard dynamic binary (glibc + # 2.35 floor) would not start here. - name: Old-glibc compatibility run: scripts/ci/check-glibc-compat.sh "$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp" diff --git a/install.sh b/install.sh index 9f4b4ec99..928e6f446 100755 --- a/install.sh +++ b/install.sh @@ -164,9 +164,9 @@ 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. +# Linux ships a fully-static "-portable" build; the standard linux binary is +# built on Ubuntu 22.04, dynamically links glibc 2.35+ and fails on older +# distros (Debian 11, RHEL 8, Ubuntu 20.04). macOS/Windows have no such variant. PORTABLE="" [ "$OS" = "linux" ] && PORTABLE="-portable" diff --git a/pkg/homebrew/Formula/codebase-memory-mcp.rb b/pkg/homebrew/Formula/codebase-memory-mcp.rb index 0ab8bf015..dfcdb74e9 100644 --- a/pkg/homebrew/Formula/codebase-memory-mcp.rb +++ b/pkg/homebrew/Formula/codebase-memory-mcp.rb @@ -15,8 +15,8 @@ class CodebaseMemoryMcp < Formula end end - # The static -portable build: the dynamic one needs glibc 2.38, which - # Ubuntu 22.04, Debian 12 and RHEL 9 do not have. + # The static -portable build: the dynamic one is built on Ubuntu 22.04 and + # needs glibc 2.35, which Debian 11, RHEL 8/9 and Ubuntu 20.04 do not have. on_linux do on_arm do url "https://github.com/DeusData/codebase-memory-mcp/releases/download/v#{version}/codebase-memory-mcp-linux-arm64-portable.tar.gz" diff --git a/pkg/npm/install.js b/pkg/npm/install.js index f3ec0e9ef..ab977d802 100644 --- a/pkg/npm/install.js +++ b/pkg/npm/install.js @@ -742,9 +742,10 @@ async function main() { const source = resolveDownloadBase(); fs.mkdirSync(BIN_DIR, { recursive: true }); - // 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. + // Linux ships a fully-static "-portable" build; the standard linux binary is + // built on Ubuntu 22.04, dynamically links glibc 2.35+ 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' : ''; // No UI/standard split since v0.10.0: one archive per platform, graph UI // always embedded. The old CBM_VARIANT=ui opt-in pointed at ui-* archives diff --git a/pkg/pypi/src/codebase_memory_mcp/_cli.py b/pkg/pypi/src/codebase_memory_mcp/_cli.py index e6e870b34..889acf75c 100644 --- a/pkg/pypi/src/codebase_memory_mcp/_cli.py +++ b/pkg/pypi/src/codebase_memory_mcp/_cli.py @@ -1290,9 +1290,10 @@ def _download(version: str) -> Path: os_name = _os_name() arch = _arch() ext = "zip" if os_name == "windows" else "tar.gz" - # 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. + # Linux ships a fully-static "-portable" build; the standard linux binary is + # built on Ubuntu 22.04, dynamically links glibc 2.35+ and fails on older + # distros. macOS/Windows have no such variant. Keep in sync with install.sh / + # install.js / cli.c. portable = "-portable" if os_name == "linux" else "" archive = f"codebase-memory-mcp-{os_name}-{arch}{portable}.{ext}" base, loopback = _download_base(version) diff --git a/scripts/ci/check-glibc-compat.sh b/scripts/ci/check-glibc-compat.sh index ed0b8ac50..89634fa69 100755 --- a/scripts/ci/check-glibc-compat.sh +++ b/scripts/ci/check-glibc-compat.sh @@ -1,12 +1,14 @@ #!/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) +# The standard linux release binary is built in a pinned Ubuntu 22.04 container +# and dynamically links glibc / libstdc++ with a 2.35 / GLIBCXX_3.4.30 floor +# (asserted on the built bytes by check-binary-composition.sh A6), so it fails +# to start on Debian 11, RHEL/Rocky 8, Ubuntu 20.04, Amazon Linux 2, etc. All +# linux install + self-update paths therefore use 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.3x not found) # - GREEN for the static -portable binary (runs anywhere) # # Usage: check-glibc-compat.sh diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 993870217..e724843a9 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -1256,8 +1256,9 @@ fi echo "OK: update hands off to $UPDATE_SCRIPT without downloading" # The glibc constraint did NOT disappear with in-process update -- it moved. The -# standard linux asset dynamically links glibc 2.38+ and breaks on Debian 11, -# RHEL 8 and Ubuntu 20.04, so the installer must fetch the static "-portable" +# standard linux asset is built on Ubuntu 22.04, dynamically links glibc 2.35+ +# and breaks on Debian 11, RHEL 8 and Ubuntu 20.04, so the installer must fetch +# the static "-portable" # build. Guard it where the behaviour now lives instead of retiring the # protection along with the code that used to implement it. if [ -f "$REPO_ROOT/install.sh" ]; then diff --git a/src/cli/cli.c b/src/cli/cli.c index 587e25918..366cc4cff 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -14358,9 +14358,9 @@ static void build_update_url(char *url, int url_sz, const char *os, const char * base_url = "https://github.com/DeusData/codebase-memory-mcp/releases/latest/download"; } /* 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. */ + * is built on Ubuntu 22.04, dynamically links glibc 2.35+ 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", base_url, os, arch, portable, ext); } diff --git a/test-infrastructure/Dockerfile.glibc22 b/test-infrastructure/Dockerfile.glibc22 index 094645323..cb65a6e14 100644 --- a/test-infrastructure/Dockerfile.glibc22 +++ b/test-infrastructure/Dockerfile.glibc22 @@ -3,9 +3,11 @@ # Mirrors CI's broad-matrix ubuntu-22.04 legs: the OLDEST userland we claim # the portable (musl-static) binary runs on. Two assertions live here: # 1. the portable binary passes the full canonical smoke on glibc 2.35; -# 2. the DYNAMIC binary refuses to start (its glibc 2.38+ floor is by -# design — old-glibc coverage is the portable binary's job, and a -# dynamic binary that silently ran here would mask a floor regression). +# 2. the LOCAL dynamic binary (built in the noble test image, glibc 2.39) +# refuses to start — proof that this venue really is an older userland +# than the build image. The RELEASE dynamic binary is built in a pinned +# ubuntu:22.04 container instead and does start on 2.35; its floor is +# asserted on the built bytes by check-binary-composition.sh (A6). # Pinned by digest like every other venue image: a floating tag means the # glibc-FLOOR assertion silently drifts to whatever ubuntu:22.04 points at, and # a floor that moves is not a floor. (Scorecard PinnedDependenciesID #76 — this diff --git a/test-infrastructure/docker-compose.yml b/test-infrastructure/docker-compose.yml index f58c78c82..f6e3950c6 100644 --- a/test-infrastructure/docker-compose.yml +++ b/test-infrastructure/docker-compose.yml @@ -286,7 +286,10 @@ services: # ── glibc floor (ubuntu-22.04, glibc 2.35) ───────────────── # Mirrors CI's broad-matrix 22.04 legs: the portable (musl-static) binary # must pass the canonical smoke on the oldest supported userland, and the - # dynamic binary must REFUSE to start (glibc 2.38+ floor is by design). + # LOCAL dynamic binary (built in the noble test image) must REFUSE to start, + # proving this venue is older than the build image. The release dynamic + # binary builds in a pinned ubuntu:22.04 container instead (2.35 floor, + # asserted by check-binary-composition.sh A6). smoke-glibc-floor: build: context: .. @@ -301,9 +304,9 @@ services: file build/alpine/codebase-memory-mcp | grep -q "statically linked" echo "=== glibc-floor: portable binary smoke on glibc 2.35 ===" scripts/smoke-local.sh ./build/alpine/codebase-memory-mcp - echo "=== glibc-floor: dynamic binary must refuse (glibc 2.38+ floor) ===" + echo "=== glibc-floor: local noble-built dynamic binary must refuse ===" if build/linux-arm64/codebase-memory-mcp --version 2>/tmp/dynamic-floor.err; then - echo "ERROR: dynamic binary ran on glibc 2.35 — floor contract broken" + echo "ERROR: a noble-built dynamic binary ran on this venue — it is not older than the build image" exit 1 fi grep -qi 'GLIBC' /tmp/dynamic-floor.err