Skip to content

Build the Linux release binary statically against musl - #465

Merged
tinder-maxwellelliott merged 2 commits into
masterfrom
claude/musl-binaries-release-30c8f3
Aug 17, 2026
Merged

Build the Linux release binary statically against musl#465
tinder-maxwellelliott merged 2 commits into
masterfrom
claude/musl-binaries-release-30c8f3

Conversation

@tinder-maxwellelliott

Copy link
Copy Markdown
Collaborator

What

The published bazel-diff-rust-linux-amd64 is now built for a musl platform, so it is statically linked. macOS and Windows assets are untouched, and the asset name is unchanged (it is derived from the target platform).

Why

The Linux asset was linked against the release runner's glibc, which made that glibc version a floor on the systems it could run on, and left it unable to run on musl distributions such as Alpine at all.

How

Bazel has no vocabulary for which libc a platform uses, and neither does rules_rust: abi_to_constraints maps x86_64-unknown-linux-gnu and x86_64-unknown-linux-musl to the same {os:linux, cpu:x86_64}. So the two toolchains would both match a musl build and resolution would silently take whichever was registered first.

  • platforms/BUILD (new) — a libc constraint setting with values system_libc (the default, so @platforms//host and every stock platform satisfy it without declaring anything) and musl, the config_settings that gate toolchains, and the linux_x86_64_musl platform.
  • MODULE.bazel — musl Rust std via rust.repository_set, hosted from Linux and from Apple Silicon; stock toolchains gated to //platforms:is_system_libc; toolchains_musl (dev dependency) scoped to //platforms:musl for the C toolchain that links the binary and builds the C in ring. Exactly one toolchain matches any given build. rustc itself stays the ordinary glibc build — only the target changes — which is what keeps proc-macro and build-script actions, compiled for the exec platform, loadable.
  • .bazelrc--config=release-musl is --config=release plus the musl platform. It cannot fold into --config=release: .bazelrc expands platform-specific config for the bare build config name only.
  • ci.yaml / release.yaml — a release_config matrix key (musl on Linux, plain elsewhere), plus a new assert_static_binary.sh gate run on Linux in both the per-PR release-artifacts job and the release job. Dropping the flag would not break the build, it would quietly produce a glibc binary again — hence an assertion rather than an assumption. It checks the asset is static, requests no dynamic loader (PT_INTERP), has no NEEDED entries, references no versioned GLIBC_ symbols, and runs.
  • Makefile / README.md / tools/readme_template.mdmake release_rust_binary_linux, and docs noting the Linux asset has no glibc requirement.

Verification

Built locally, cross-compiled from an Apple Silicon Mac:

ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped

No INTERP, no NEEDED, no GLIBC_ strings. The generated @rust_toolchains hub shows the intended mutual exclusion, and each set's incidental exec-triple toolchain is unsatisfiable. Normal host build, bazel build //..., and the full test suite are unchanged; buildifier reports no formatting changes.

Notes for reviewers

  • The Linux-hosted leg (exec_triple = x86_64-unknown-linux-gnu) was not exercised locally — same declarations as the macOS-hosted one, and the required rust artifacts were confirmed to exist upstream. The release-artifacts job exercises it on this PR.
  • toolchains_musl pulls rules_cc from 0.2.17 to 0.2.18 in MODULE.bazel.lock. It is a dev dependency, so consumers of the module never resolve it.
  • The README was hand-edited to match tools/readme_template.md rather than regenerated: //tools:generate-readme hit a GitHub API 504 mid-fetch and degraded the contributors table.
  • //cli:E2ETest fails on this machine both with and without the change (nested Bazel cannot resolve local_jdk locally); it is unrelated.

🤖 Generated with Claude Code

The published bazel-diff-rust-linux-amd64 was linked against the release
runner's glibc, which made the runner's glibc version a floor on the
systems that binary could run on -- and left it unable to run on musl
distributions such as Alpine at all. Build it for a musl platform instead,
so it is statically linked and runs anywhere.

Bazel has no vocabulary for which libc a platform uses, and neither does
rules_rust: abi_to_constraints maps x86_64-unknown-linux-gnu and
x86_64-unknown-linux-musl to the same {os:linux, cpu:x86_64}. //platforms
supplies that missing constraint, gates the stock Rust toolchains to
system-libc platforms and the new musl ones to //platforms:musl, so
exactly one toolchain matches any given build rather than resolution
silently taking whichever was registered first. The C toolchain
(@toolchains_musl, a dev dependency) is scoped the same way, so ordinary
builds keep using the host cc toolchain.

rustc itself stays the ordinary glibc build -- only the target changes --
which is what keeps proc-macro and build-script actions, compiled for the
exec platform, loadable. The build is a cross-compile from either a glibc
Linux host or an Apple Silicon Mac, so a maintainer can reproduce the
published binary locally with `make release_rust_binary_linux`.

Dropping --config=release-musl would not break the build; it would quietly
produce a glibc binary again. assert_static_binary.sh is therefore run on
Linux in both the per-PR release-artifacts job and the release job: it
checks that the asset is static, requests no dynamic loader, has no shared
library dependencies, references no versioned glibc symbols, and runs.

The asset name is derived from the target platform, so it is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tinder-maxwellelliott
tinder-maxwellelliott marked this pull request as ready for review August 17, 2026 16:44
//tools:perf_gate_test is flaky on CI: it failed the Bazel 8.x leg of
this PR and the Bazel 9.x leg on master a day earlier, both in
test_passes_when_rust_is_consistently_faster.

The gate passes only when the faster implementation wins *every* round,
and the tests asserting that path decided the winner with a 0.2s `sleep`
in the stub binary. On a runner already busy with the rest of the build,
process start-up noise dwarfs 0.2s -- a failing run measured 2.041s for a
stub that only execs bash and exits -- so a single round flips and the
gate reports FAIL. Reproduced locally at --runs_per_test=40, where ~20
runs failed.

These tests are about the gate's decision, not about measurement, so
supply the elapsed time instead of sleeping for it: fixed_timings() keys
a fixed number on the binary that ran, while still executing the stub, so
parity checking, output files and exit codes stay real. TimeCommandTest
continues to cover the measurement itself. The two failure-direction
tests move over too, so all four gate-decision tests read the same way.

60/60 runs pass under the load that previously failed ~20 of 40.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tinder-maxwellelliott
tinder-maxwellelliott merged commit aba38e1 into master Aug 17, 2026
63 of 65 checks passed
@tinder-maxwellelliott
tinder-maxwellelliott deleted the claude/musl-binaries-release-30c8f3 branch August 17, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant