Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ build:release --@rules_rust//rust/settings:lto=thin
# property of the toolchain, which would change every build, not just releases.
build:release --@rules_rust//rust/settings:extra_rustc_flags=-Cstrip=symbols

# Linux release artifacts: `bazel build //release:bazel-diff-rust --config=release-musl`.
# Everything --config=release does, but built for a musl platform, which makes
# the published Linux binary statically linked: it runs on any distribution
# instead of requiring the glibc of whatever runner built it. //platforms
# explains the constraint; MODULE.bazel wires up the Rust and C toolchains it
# selects. Cross-compiles, so this config works on a glibc Linux host and on an
# Apple Silicon Mac -- the asset name is derived from the target platform, so it
# is bazel-diff-rust-linux-amd64 either way.
build:release-musl --config=release
build:release-musl --platforms=//platforms:linux_x86_64_musl

# Avoid cache thrashing, but allow integration tests to find "bazel" on the PATH.
common --incompatible_strict_action_env
common --test_env=PATH
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/assert_static_binary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash

# Asserts that a published Linux binary is statically linked, i.e. that it
# carries no dependency on the glibc of the runner that built it. Shared by the
# `release-artifacts` job in ci.yaml (per-PR check) and the `rust-binaries` job
# in release.yaml (before the asset is uploaded), so a regression cannot reach a
# release without failing a PR first.
#
# The build flag that makes this true is --config=release-musl (see .bazelrc).
# Losing it does not break the build -- it silently produces a glibc-linked
# binary -- which is exactly why this is asserted rather than assumed.

set -o errexit -o nounset -o pipefail

BINARY=${1:?usage: assert_static_binary.sh <binary>}

echo "Checking $BINARY"
# -L because bazel-bin/release/<asset> is a symlink into the output base, and
# `file` would otherwise describe the link rather than the binary.
file -L "$BINARY"

# Both "statically linked" and "static-pie linked" are acceptable; a dynamically
# linked binary says "dynamically linked (uses shared libs)" instead.
if ! file -L "$BINARY" | grep -q "static"; then
echo "FAIL: $BINARY is not statically linked" >&2
exit 1
fi

# The definitive check: a dynamic loader (PT_INTERP) is what ties a binary to
# the host's libc, and NEEDED entries are the libraries it would load.
if readelf --program-headers "$BINARY" | grep -q "INTERP"; then
echo "FAIL: $BINARY requests a dynamic loader" >&2
readelf --program-headers "$BINARY" | grep -A 2 "INTERP" >&2
exit 1
fi

if readelf --dynamic "$BINARY" 2>/dev/null | grep -q "NEEDED"; then
echo "FAIL: $BINARY has shared library dependencies" >&2
readelf --dynamic "$BINARY" | grep "NEEDED" >&2
exit 1
fi

# Versioned glibc symbols (GLIBC_2.34 and friends) are the failure this whole
# exercise is about: they make a binary refuse to start on older distributions.
if strings -a "$BINARY" | grep -q "GLIBC_"; then
echo "FAIL: $BINARY references versioned glibc symbols" >&2
exit 1
fi

# Nothing above proves the binary runs, only that it is self-contained.
"$BINARY" --version

echo "OK: $BINARY is statically linked"
21 changes: 17 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,22 +290,31 @@ jobs:
# ...rust_toolchain\lib\rustlib\x86_64-pc-windows-msvc\lib\
# librustc_std_workspace_alloc-<hash>.rlib comes to 263 characters and
# the link fails with LNK1181. C:/b buys back 35.
#
# release_config is `release-musl` on Linux: the published Linux binary
# is statically linked against musl so it does not inherit the runner's
# glibc as a floor on the systems it runs on. It is a cross-compile, not
# a different runner, so it stays in this row. macOS and Windows link
# their platform's own libc and use the plain `release` config.
include:
- os: ubuntu-latest
java: '11'
rust_asset: bazel-diff-rust-linux-amd64
release_config: release-musl
bazel_startup_flags: ''
bazel_extra_flags: ''
upload_jar_and_archive: true
- os: macos-latest
java: '11'
rust_asset: bazel-diff-rust-macos-arm64
release_config: release
bazel_startup_flags: ''
bazel_extra_flags: ''
upload_jar_and_archive: false
- os: windows-latest
java: '11'
rust_asset: bazel-diff-rust-windows-amd64.exe
release_config: release
bazel_startup_flags: '--output_user_root=C:/b'
bazel_extra_flags: '--legacy_external_runfiles'
upload_jar_and_archive: false
Expand Down Expand Up @@ -357,9 +366,10 @@ jobs:
if-no-files-found: error
# //release:bazel-diff-rust names the binary for the platform Bazel built
# it for, so this step neither renames nor relocates anything: whatever
# lands in bazel-bin/release/ is the published asset. --config=release
# carries the flags (see .bazelrc). `if-no-files-found: error` below is
# the assertion that Bazel's name still matches matrix.rust_asset.
# lands in bazel-bin/release/ is the published asset. matrix.release_config
# carries the flags (see .bazelrc) -- including, on Linux, the musl target
# platform. `if-no-files-found: error` below is the assertion that Bazel's
# name still matches matrix.rust_asset.
- name: Build Rust binary
shell: bash
env:
Expand All @@ -368,7 +378,10 @@ jobs:
# Bazel then rejects "invalid package name '/release'". (`//:target`
# survived only because it has no path-like segment to convert.)
MSYS2_ARG_CONV_EXCL: '//'
run: bazelisk ${{ matrix.bazel_startup_flags }} build //release:bazel-diff-rust --config=release ${{ matrix.bazel_extra_flags }}
run: bazelisk ${{ matrix.bazel_startup_flags }} build //release:bazel-diff-rust --config=${{ matrix.release_config }} ${{ matrix.bazel_extra_flags }}
- name: Assert Linux binary is statically linked
if: runner.os == 'Linux'
run: .github/workflows/assert_static_binary.sh "bazel-bin/release/${{ matrix.rust_asset }}"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.rust_asset }}
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,24 @@ jobs:
# link.exe is MAX_PATH-bound (260): under the default root the Rust
# stdlib rlib ...\librustc_std_workspace_alloc-<hash>.rlib comes to 263
# characters and the link fails with LNK1181. Keep in sync with the
# `deploy` job in ci.yaml, which is where this gets exercised per-PR.
# `release-artifacts` job in ci.yaml, which is where this gets exercised
# per-PR -- including release_config, which builds the Linux asset
# statically against musl so it runs on any distribution rather than
# requiring the runner's glibc or newer.
include:
- os: ubuntu-latest
asset: bazel-diff-rust-linux-amd64
release_config: release-musl
bazel_startup_flags: ""
bazel_extra_flags: ""
- os: macos-latest
asset: bazel-diff-rust-macos-arm64
release_config: release
bazel_startup_flags: ""
bazel_extra_flags: ""
- os: windows-latest
asset: bazel-diff-rust-windows-amd64.exe
release_config: release
bazel_startup_flags: "--output_user_root=C:/b"
bazel_extra_flags: "--legacy_external_runfiles"
steps:
Expand Down Expand Up @@ -90,7 +96,7 @@ jobs:
# //release:bazel-diff-rust names the binary for the platform Bazel built
# it for, so nothing here renames or relocates it: bazel-bin/release/ holds
# the published asset, and `gh release upload` keeps that file name. The
# build flags live in .bazelrc under --config=release.
# build flags live in .bazelrc under matrix.release_config.
- name: Build Rust binary
shell: bash
env:
Expand All @@ -99,7 +105,12 @@ jobs:
# Bazel then rejects "invalid package name '/release'". (`//:target`
# survived only because it has no path-like segment to convert.)
MSYS2_ARG_CONV_EXCL: '//'
run: bazelisk ${{ matrix.bazel_startup_flags }} build //release:bazel-diff-rust --config=release ${{ matrix.bazel_extra_flags }}
run: bazelisk ${{ matrix.bazel_startup_flags }} build //release:bazel-diff-rust --config=${{ matrix.release_config }} ${{ matrix.bazel_extra_flags }}
# Last gate before the asset is published: a glibc-linked binary here
# would strand every user on an older distribution than the runner.
- name: Assert Linux binary is statically linked
if: runner.os == 'Linux'
run: .github/workflows/assert_static_binary.sh "bazel-bin/release/${{ matrix.asset }}"
- name: Upload release asset
shell: bash
env:
Expand Down
73 changes: 73 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,85 @@ bazel_dep(name = "rules_rust", version = "0.73.0")
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(
edition = "2021",
# Confine the stock toolchains to platforms that use the system libc. They
# are only constrained by os and cpu -- rules_rust has no musl constraint --
# so on //platforms:linux_x86_64_musl they would match alongside the musl
# toolchain below and resolution would silently take whichever of the two
# was registered first.
target_settings = ["//platforms:is_system_libc"],
versions = ["1.90.0"],
)
use_repo(rust, "rust_toolchains")

register_toolchains("@rust_toolchains//:all")

# Rust std for the statically linked musl release binary, cross-compiled from a
# glibc Linux host (the release runner) or from an Apple Silicon Mac (so a
# maintainer can reproduce the published binary locally). rustc itself is the
# ordinary glibc build: only the target changes, which is what keeps proc-macro
# and build-script actions -- compiled for the exec platform -- loadable.
# These toolchains join the same @rust_toolchains hub registered above.
rust.repository_set(
name = "rust_musl_linux_x86_64",
edition = "2021",
exec_triple = "x86_64-unknown-linux-gnu",
target_compatible_with = [
"@platforms//cpu:x86_64",
"@platforms//os:linux",
"//platforms:musl",
],
target_settings = ["//platforms:is_musl"],
target_triple = "x86_64-unknown-linux-musl",
versions = ["1.90.0"],
)

# rules_rust also emits a toolchain for a set's own exec triple, so the set
# above comes with a glibc x86_64 Linux toolchain whether or not it is wanted.
# Declaring its constraints keeps it from matching musl builds; the set-level
# //platforms:is_musl gate keeps it out of ordinary builds, where the stock
# toolchain serves and this one would otherwise be a second, identical
# candidate whose repository is a redundant ~200MB download.
rust.repository_set(
name = "rust_musl_linux_x86_64",
target_compatible_with = [
"@platforms//cpu:x86_64",
"@platforms//os:linux",
"//platforms:system_libc",
],
target_triple = "x86_64-unknown-linux-gnu",
)

# The macOS-hosted half of the same story. No companion tag is needed here: this
# set's exec-triple toolchain targets macOS, and a macOS platform can never
# satisfy the set's //platforms:is_musl gate.
rust.repository_set(
name = "rust_musl_macos_aarch64",
edition = "2021",
exec_triple = "aarch64-apple-darwin",
target_compatible_with = [
"@platforms//cpu:x86_64",
"@platforms//os:linux",
"//platforms:musl",
],
target_settings = ["//platforms:is_musl"],
target_triple = "x86_64-unknown-linux-musl",
versions = ["1.90.0"],
)

# C toolchain for the musl target: a gcc cross-compiler bundled with musl libc,
# used to link the Rust binary and to build the C in `ring` (rustls). Scoped to
# //platforms:musl so it never displaces the host cc toolchain on ordinary
# builds. dev_dependency because only bazel-diff's own release build needs it --
# consumers of the module never resolve it.
bazel_dep(name = "toolchains_musl", version = "0.1.27.bcr.1", dev_dependency = True)

toolchains_musl = use_extension(
"@toolchains_musl//:toolchains_musl.bzl",
"toolchains_musl",
dev_dependency = True,
)
toolchains_musl.config(extra_target_compatible_with = ["//platforms:musl"])

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
name = "bazel_diff_maven",
Expand Down
7 changes: 6 additions & 1 deletion MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ release_rust_binary:
//release:bazel-diff-rust \
--config=release

# The published Linux binary, which is not host-native: it is statically linked
# against musl so it runs on any distribution, and cross-compiles from a glibc
# Linux host or an Apple Silicon Mac. Same output path and asset name.
.PHONY: release_rust_binary_linux
release_rust_binary_linux:
bazel \
build \
//release:bazel-diff-rust \
--config=release-musl

.PHONY: build_rust
build_rust:
bazel build //:bazel-diff-rust -c opt
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,10 +913,11 @@ implementation remains the default `//:bazel-diff` target and the released JAR.
bazel run //:bazel-diff-rust -- --help
```

GitHub Releases also ship prebuilt host-native binaries:
GitHub Releases also ship prebuilt binaries:

```terminal
# Linux amd64
# Linux amd64 (statically linked against musl -- no glibc requirement, so it
# runs on any distribution, including Alpine and images older than the runner)
curl -Lo bazel-diff-rust https://github.com/Tinder/bazel-diff/releases/latest/download/bazel-diff-rust-linux-amd64
chmod +x bazel-diff-rust

Expand All @@ -929,14 +930,20 @@ Windows amd64: download
`bazel-diff-rust-windows-amd64.exe` from the
[latest release](https://github.com/Tinder/bazel-diff/releases/latest).

Those assets are produced by Bazel alone -- CI runs nothing but the command below and uploads
Those assets are produced by Bazel alone -- CI runs nothing but the commands below and uploads
whatever lands in `bazel-bin/release/`, so `//release:bazel-diff-rust` names the binary for the
platform it was built on (`bazel-diff-rust-<os>-<arch>`, plus `.exe` on Windows):
platform it was built for (`bazel-diff-rust-<os>-<arch>`, plus `.exe` on Windows):

```terminal
make release_rust_binary # bazel build //release:bazel-diff-rust --config=release
make release_rust_binary # bazel build //release:bazel-diff-rust --config=release
make release_rust_binary_linux # ... --config=release-musl
```

`--config=release-musl` targets `//platforms:linux_x86_64_musl`, which selects a musl Rust std
and a musl C toolchain, so the Linux asset is statically linked instead of inheriting the build
runner's glibc as a version floor. It is a cross-compile: the same command produces the same
`bazel-diff-rust-linux-amd64` on a glibc Linux host and on an Apple Silicon Mac.

### Performance gate

The Rust candidate is expected to be faster than Kotlin, and CI enforces it. `make perf-gate`
Expand Down
Loading
Loading