From 5e87c649c13b1d3903b9f11b29be846b578afbcd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 14:33:27 +0000 Subject: [PATCH 1/8] Add a Linux release binary compatibility guard Reads the ELF version-need table of a built artifact and fails when a *-gnu binary requires a glibc newer than GLIBC_FLOOR, or when a *-musl binary is not fully static. Also checks the ELF machine matches the target triple. Co-authored-by: Ibrahim Rahhal --- scripts/check-linux-binary.sh | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 scripts/check-linux-binary.sh diff --git a/scripts/check-linux-binary.sh b/scripts/check-linux-binary.sh new file mode 100755 index 0000000..6572a1c --- /dev/null +++ b/scripts/check-linux-binary.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Gate on the runtime requirements of a released Linux binary. +# +# Linking against the build runner's glibc stamps that version into the ELF +# version-need table, and the loader refuses the binary on any host with an +# older glibc. Runner images move forward on their own schedule, so without +# this check the supported floor silently follows them. +# +# scripts/check-linux-binary.sh +# +# GLIBC_FLOOR (default 2.17) is the highest glibc version a *-gnu binary may +# require. *-musl binaries must be fully static instead. + +set -euo pipefail + +binary=${1:-} +triple=${2:-} +floor=${GLIBC_FLOOR:-2.17} + +if [ -z "$binary" ] || [ -z "$triple" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +if [ ! -f "$binary" ]; then + echo "FAIL: $binary does not exist" >&2 + exit 1 +fi + +case "$triple" in + aarch64-*) want_machine="AArch64" ;; + x86_64-*) want_machine="X86-64" ;; + *) + echo "FAIL: unsupported target triple: $triple" >&2 + exit 1 + ;; +esac + +machine=$(readelf -h "$binary" | sed -n 's/^ *Machine: *//p') +case "$machine" in + *"$want_machine"*) ;; + *) + echo "FAIL: $binary is '$machine', expected $want_machine for $triple" >&2 + exit 1 + ;; +esac + +case "$triple" in + *-musl) + if readelf -lW "$binary" | grep -q INTERP; then + echo "FAIL: $binary is dynamically linked; musl builds must be static" >&2 + exit 1 + fi + echo "OK: $binary ($triple) is statically linked" + ;; + + *-gnu) + needed=$(readelf -V "$binary" | grep -o 'GLIBC_[0-9.]*' | sed 's/^GLIBC_//' | sort -uV) + if [ -z "$needed" ]; then + echo "OK: $binary ($triple) requires no versioned glibc symbols" + exit 0 + fi + + highest=$(echo "$needed" | tail -1) + if [ "$(printf '%s\n%s\n' "$highest" "$floor" | sort -V | tail -1)" != "$floor" ]; then + echo "FAIL: $binary requires glibc $highest, above the $floor floor" >&2 + echo " versions required: $(echo "$needed" | tr '\n' ' ')" >&2 + exit 1 + fi + + echo "OK: $binary ($triple) requires at most glibc $highest (floor $floor)" + ;; + + *) + echo "FAIL: unsupported libc in target triple: $triple" >&2 + exit 1 + ;; +esac From ca6117d16de8e8ac8bf5368dfe2393d02c02ac53 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 14:33:34 +0000 Subject: [PATCH 2/8] Pin the Linux release binaries to a glibc 2.17 floor Building the *-gnu targets natively stamped the runner's glibc into the ELF version-need table, so every release since v1.7.2 required GLIBC_2.39 and failed to load on Ubuntu 22.04 (glibc 2.35) and older: corgea: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found corgea: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found Cross-build all Linux targets on ubuntu-latest with cargo-zigbuild, pinning the gnu targets to GLIBC_FLOOR, and add an aarch64 musl target so ARM64 hosts have a libc-independent option. Each artifact is verified before it is archived. Co-authored-by: Ibrahim Rahhal --- .github/workflows/release-binaries.yml | 49 ++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index d740ab7..41954e6 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -12,6 +12,13 @@ on: permissions: contents: write +env: + # Highest glibc a *-gnu binary may require. Linux builds are cross-linked + # against this floor so the artifacts do not inherit the runner's glibc. + GLIBC_FLOOR: "2.17" + ZIG_VERSION: "0.13.0" + CARGO_ZIGBUILD_VERSION: "0.23.0" + jobs: native-binaries: name: Build and Upload Native Binaries @@ -24,9 +31,12 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-musl binary_name: corgea - - os: ubuntu-24.04-arm + - os: ubuntu-latest target: aarch64-unknown-linux-gnu binary_name: corgea + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + binary_name: corgea - os: macos-latest target: x86_64-apple-darwin binary_name: corgea @@ -44,16 +54,43 @@ jobs: with: ref: ${{ inputs.tag || github.ref }} - - name: Install musl tools - if: matrix.target == 'x86_64-unknown-linux-musl' - run: sudo apt-get install -y musl-tools - - name: Install Rust Target run: rustup target add ${{ matrix.target }} - - name: Build Binary + - name: Install Zig + if: runner.os == 'Linux' + uses: mlugg/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + + - name: Install cargo-zigbuild + if: runner.os == 'Linux' + uses: taiki-e/install-action@v2 + with: + tool: cargo-zigbuild@${{ env.CARGO_ZIGBUILD_VERSION }} + + - name: Build Binary (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + build_target="${{ matrix.target }}" + if [[ "$build_target" == *-gnu ]]; then + build_target="${build_target}.${GLIBC_FLOOR}" + fi + cargo zigbuild --release --target "$build_target" + + - name: Build Binary (macOS and Windows) + if: runner.os != 'Linux' run: cargo build --release --target ${{ matrix.target }} + - name: Verify Linux Runtime Compatibility + if: runner.os == 'Linux' + shell: bash + run: | + scripts/check-linux-binary.sh \ + "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" \ + "${{ matrix.target }}" + - name: Archive Binary (Unix) if: runner.os != 'Windows' shell: bash From c19430cc374e7ef2913a6f1335338eed01d4e13f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 14:33:41 +0000 Subject: [PATCH 3/8] Ship the static musl binary to npm on Linux arm64 Linux x64 already resolves to the musl build; arm64 pointed at the gnu build and inherited its glibc requirement. Also ignore the generated vendor/ tree so bundled binaries cannot be committed by accident. Co-authored-by: Ibrahim Rahhal --- .gitignore | 1 + bin/corgea.js | 2 +- scripts/npm/bundle-binaries.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 25c3317..a2fca7f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.zip *.profraw node_modules/ +/vendor/ /packages/*/vendor/ .claude/ diff --git a/bin/corgea.js b/bin/corgea.js index f68895d..be9dc62 100755 --- a/bin/corgea.js +++ b/bin/corgea.js @@ -10,7 +10,7 @@ function resolveTargetTriple() { case "linux": case "android": if (process.arch === "x64") return "x86_64-unknown-linux-musl"; - if (process.arch === "arm64") return "aarch64-unknown-linux-gnu"; + if (process.arch === "arm64") return "aarch64-unknown-linux-musl"; return null; case "darwin": if (process.arch === "x64") return "x86_64-apple-darwin"; diff --git a/scripts/npm/bundle-binaries.js b/scripts/npm/bundle-binaries.js index 905cd22..c6a13d6 100644 --- a/scripts/npm/bundle-binaries.js +++ b/scripts/npm/bundle-binaries.js @@ -7,7 +7,7 @@ const { unzipSync } = require("fflate"); const TARGETS = [ { triple: "x86_64-unknown-linux-musl", binary: "corgea" }, - { triple: "aarch64-unknown-linux-gnu", binary: "corgea" }, + { triple: "aarch64-unknown-linux-musl", binary: "corgea" }, { triple: "x86_64-apple-darwin", binary: "corgea" }, { triple: "aarch64-apple-darwin", binary: "corgea" }, { triple: "x86_64-pc-windows-msvc", binary: "corgea.exe" }, From 30ada9e822161b821bac4321c97a8e7bf718d96c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 14:33:41 +0000 Subject: [PATCH 4/8] Document the glibc floor and match it in build_release.sh The local release script built the gnu targets natively, which produces exactly the binaries the pipeline now rejects. Route Linux through cargo-zigbuild with the same floor and verify the result. Drops the x86_64-pc-windows-gnu target, which has not been part of a release since v1.7.2 and was the script's only remaining use of cross. Co-authored-by: Ibrahim Rahhal --- README.md | 5 +++++ RELEASING.md | 25 +++++++++++++++++++++++-- build_release.sh | 32 +++++++++++++++++++++++--------- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ccecc68..44ee09d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,11 @@ pip install corgea-cli ### Manual Installation You can get the latest binaries for your OS from https://github.com/Corgea/cli/releases. +On Linux, both a `-gnu` and a `-musl` binary are published for x86_64 and +aarch64. The `-gnu` builds run on glibc 2.17 and newer; the `-musl` builds are +fully static and have no libc requirement at all, which makes them the safer +pick for minimal containers and CI runners. + ### Setup Once the binary is installed, login with your token from the Corgea app. ``` diff --git a/RELEASING.md b/RELEASING.md index 986bd07..7bdc917 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -69,8 +69,29 @@ Pushing an annotated `vX.Y.Z` tag to `main` fans out to three GitHub Actions wor ## What gets built - **PyPI wheels** (`release.yml`): Linux (x86_64, x86), Windows (x64, x86), macOS (x86_64, aarch64), plus an sdist. Asserts `manylinux2014` tags for broad Linux compatibility. -- **Native binaries** (`release-binaries.yml`): `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-gnu`, `x86_64-apple-darwin`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`. -- **npm binary bundling** (`npm-publish.yml` → `scripts/npm/bundle-binaries.js`): downloads the GitHub Release zips and lays them out as `vendor//corgea/corgea`. At runtime `bin/corgea.js` selects the binary for the host OS/arch. npm ships 5 of the 6 targets (Linux x64 uses the musl build). +- **Native binaries** (`release-binaries.yml`): `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-unknown-linux-gnu`, `aarch64-unknown-linux-musl`, `x86_64-apple-darwin`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`. +- **npm binary bundling** (`npm-publish.yml` → `scripts/npm/bundle-binaries.js`): downloads the GitHub Release zips and lays them out as `vendor//corgea/corgea`. At runtime `bin/corgea.js` selects the binary for the host OS/arch. npm ships 5 of the 7 targets (both Linux arches use the musl build). + +### Linux glibc floor + +Building a `*-gnu` target natively stamps the *runner's* glibc into the binary's +ELF version-need table, and the loader then refuses to start it on any host with +an older glibc. Every release from `v1.7.2` through `v1.9.2` shipped Linux gnu +binaries that required `GLIBC_2.39`, so they failed on Ubuntu 22.04 (glibc 2.35) +and older with `version 'GLIBC_2.38' not found`. + +All four Linux targets are therefore cross-built on `ubuntu-latest` with +[`cargo-zigbuild`](https://github.com/rust-cross/cargo-zigbuild), pinning the gnu +targets to the `GLIBC_FLOOR` declared in `release-binaries.yml` (currently +`2.17`, i.e. RHEL 7 / Ubuntu 14.04 and newer). `scripts/check-linux-binary.sh` +then re-reads each artifact and fails the build if a gnu binary exceeds the +floor, or if a musl binary is not fully static. Run it locally with: + +```bash +scripts/check-linux-binary.sh target/aarch64-unknown-linux-gnu/release/corgea aarch64-unknown-linux-gnu +``` + +Raising `GLIBC_FLOOR` drops support for older distros — change it deliberately. ## Release notes diff --git a/build_release.sh b/build_release.sh index 4f0173c..8f92a03 100755 --- a/build_release.sh +++ b/build_release.sh @@ -2,10 +2,21 @@ BUILD_OS="$(uname)" +# Keep in sync with GLIBC_FLOOR in .github/workflows/release-binaries.yml. Linux +# binaries are cross-linked against this floor so they do not pick up whatever +# glibc the build host happens to ship. +GLIBC_FLOOR="${GLIBC_FLOOR:-2.17}" + if [ $BUILD_OS == "Darwin" ]; then TARGETS=("aarch64-apple-darwin" "x86_64-apple-darwin") elif [ $BUILD_OS == "Linux" ]; then - TARGETS=("aarch64-unknown-linux-gnu" "x86_64-unknown-linux-gnu" "x86_64-pc-windows-gnu") + TARGETS=("aarch64-unknown-linux-gnu" "aarch64-unknown-linux-musl" "x86_64-unknown-linux-gnu" "x86_64-unknown-linux-musl") + + if ! command -v cargo-zigbuild >/dev/null 2>&1; then + echo "cargo-zigbuild is required to build the Linux targets." + echo "Install it with: cargo install cargo-zigbuild && pip install ziglang" + exit 1 + fi else echo "Are you building from a supported OS? (Darwin/Linux)" exit 1 @@ -14,9 +25,16 @@ fi for target in "${TARGETS[@]}" do if [ $BUILD_OS == "Darwin" ]; then - cargo build -r --target $target + cargo build -r --target $target || exit 1 else - cross build -r --target $target + build_target="$target" + if [[ "$target" == *-gnu ]]; then + build_target="${target}.${GLIBC_FLOOR}" + fi + + cargo zigbuild -r --target "$build_target" || exit 1 + GLIBC_FLOOR="$GLIBC_FLOOR" \ + ./scripts/check-linux-binary.sh "target/$target/release/corgea" "$target" || exit 1 fi zip_file_name="corgea-$target.zip" @@ -26,9 +44,5 @@ do rm $zip_file_name fi - if [ $target == "x86_64-pc-windows-gnu" ]; then - zip -j $zip_file_name "target/$target/release/corgea.exe" - else - zip -j $zip_file_name "target/$target/release/corgea" - fi -done \ No newline at end of file + zip -j $zip_file_name "target/$target/release/corgea" +done From 3489fbced572ee56b775f32e823682d166bda399 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 14:55:12 +0000 Subject: [PATCH 5/8] Smoke test the Linux release binaries under qemu The aarch64 artifacts are now cross-built, so no job in CI would otherwise execute them. Running --version catches a binary that passes the ELF header checks but fails to start. Co-authored-by: Ibrahim Rahhal --- .github/workflows/release-binaries.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 41954e6..5312a1c 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -91,6 +91,31 @@ jobs: "target/${{ matrix.target }}/release/${{ matrix.binary_name }}" \ "${{ matrix.target }}" + # The aarch64 artifacts are cross-built, so nothing else in CI would ever + # execute them. Run them under qemu to catch a binary that passes the + # header checks but cannot actually start. + - name: Smoke Test Linux Binary + if: runner.os == 'Linux' + shell: bash + run: | + binary="target/${{ matrix.target }}/release/${{ matrix.binary_name }}" + + case "${{ matrix.target }}" in + aarch64-*-gnu) + sudo apt-get update -qq + sudo apt-get install -y -qq qemu-user-static libc6-arm64-cross + qemu-aarch64-static -L /usr/aarch64-linux-gnu "$binary" --version + ;; + aarch64-*-musl) + sudo apt-get update -qq + sudo apt-get install -y -qq qemu-user-static + qemu-aarch64-static "$binary" --version + ;; + *) + "$binary" --version + ;; + esac + - name: Archive Binary (Unix) if: runner.os != 'Windows' shell: bash From fb294962c1bcc8b5e7249c2b7ee8e4ee2a7e8869 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 15:08:33 +0000 Subject: [PATCH 6/8] Bump version to 1.9.3 Ships the Linux glibc floor fix. No tag on the 1.9.x line has a usable aarch64-unknown-linux-gnu binary, so a release is the only way to unblock ARM64 users. Co-authored-by: Ibrahim Rahhal --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9cfb89..e35738f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,7 +369,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "corgea" -version = "1.9.2" +version = "1.9.3" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index b75af24..2543dc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "corgea" -version = "1.9.2" +version = "1.9.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 05b9baf0f6d6e1065fd20ead34231ef80c32460d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 16:02:41 +0000 Subject: [PATCH 7/8] Fix the glibc guard aborting on a binary with no glibc references set -euo pipefail made grep's no-match exit status abort the assignment, so the branch that accepts a binary with no versioned glibc symbols was unreachable and the script exited 1 with no diagnostic. Run grep outside the pipeline that feeds the assignment, and require a digit so GLIBC_PRIVATE and GLIBC_ABI are not read as versions. Co-authored-by: Ibrahim Rahhal --- scripts/check-linux-binary.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check-linux-binary.sh b/scripts/check-linux-binary.sh index 6572a1c..67bc0a1 100755 --- a/scripts/check-linux-binary.sh +++ b/scripts/check-linux-binary.sh @@ -56,7 +56,12 @@ case "$triple" in ;; *-gnu) - needed=$(readelf -V "$binary" | grep -o 'GLIBC_[0-9.]*' | sed 's/^GLIBC_//' | sort -uV) + # A binary with no versioned glibc references is fine, but grep exits 1 on + # no match, so run it outside the pipeline that feeds the assignment. + # Requiring a digit keeps unversioned tags such as GLIBC_PRIVATE out. + version_info=$(readelf -V "$binary") + needed=$(grep -oE 'GLIBC_[0-9]+(\.[0-9]+)*' <<<"$version_info" | sed 's/^GLIBC_//' | sort -uV || true) + if [ -z "$needed" ]; then echo "OK: $binary ($triple) requires no versioned glibc symbols" exit 0 From 8f2583fd3855b5a9f37425e26304d9d9c6b7816d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 16:02:41 +0000 Subject: [PATCH 8/8] Pin the new release-workflow actions to commit SHAs This workflow holds contents: write, so a moved tag on either action would get a write-capable GITHUB_TOKEN. Co-authored-by: Ibrahim Rahhal --- .github/workflows/release-binaries.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 5312a1c..74a02b0 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -59,13 +59,13 @@ jobs: - name: Install Zig if: runner.os == 'Linux' - uses: mlugg/setup-zig@v2 + uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 with: version: ${{ env.ZIG_VERSION }} - name: Install cargo-zigbuild if: runner.os == 'Linux' - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3 with: tool: cargo-zigbuild@${{ env.CARGO_ZIGBUILD_VERSION }}