From f3ae77fe50fa527c218470138a5f358ce4022b54 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Fri, 9 Oct 2026 22:46:50 -0700 Subject: [PATCH] build(release): cross-compile the Linux aarch64 tools on x86_64 Building each release tool natively needs a GitHub runner per platform. Cross-compiling on a Linux x86_64 runner instead lets us add platforms that lack GitHub runners, e.g. other Linux CPUs, as suggested in #4217's review. The Linux aarch64 job now runs on an x86_64 runner and cross-compiles for musl (aarch64-unknown-linux-musl) instead of glibc. Rust links musl statically with its bundled rust-lld, so the build needs just a dev-only Rust toolchain, not a C cross toolchain. macOS and Windows files need those OSes' SDKs, so their jobs still build natively, and Linux x86_64 keeps its static glibc build. build.sh now takes the job's target triple and builds for the matching platform in dev/platforms, so the workflow skips older refs without it. release_files still picks the release flags, now from the platform's OS and libc. verify.sh also checks the CPU of Linux files, since one is now cross-compiled. Work towards #4216 --- .github/workflows/release_build_tools.yaml | 18 +++--- MODULE.bazel | 38 ++++++++++++ dev/platforms/BUILD.bazel | 67 ++++++++++++++++++++++ dev/release_artifacts/BUILD.bazel | 3 +- dev/release_artifacts/build.sh | 15 +++-- dev/release_artifacts/release_files.bzl | 18 ++++-- dev/release_artifacts/verify.sh | 34 ++++++++--- 7 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 dev/platforms/BUILD.bazel diff --git a/.github/workflows/release_build_tools.yaml b/.github/workflows/release_build_tools.yaml index 0801df0f8d..998c20316d 100644 --- a/.github/workflows/release_build_tools.yaml +++ b/.github/workflows/release_build_tools.yaml @@ -1,5 +1,7 @@ -# Builds `//dev/release_artifacts:artifacts_for_release` natively for each -# platform; see dev/release_artifacts/build.sh. +# Builds `//dev/release_artifacts:artifacts_for_release` for each platform; see +# dev/release_artifacts/build.sh. Linux x86_64 runners cross-compile the files +# for other Linux CPUs. macOS and Windows files need those OSes' SDKs, so +# they're built natively. # # For now, this only verifies that the artifacts build; they aren't attached to # releases yet. @@ -40,8 +42,9 @@ jobs: runner: macos-15 - triple: aarch64-pc-windows-msvc runner: windows-11-arm - - triple: aarch64-unknown-linux-gnu - runner: ubuntu-24.04-arm + # Cross-compiled; see dev/platforms/BUILD.bazel. + - triple: aarch64-unknown-linux-musl + runner: ubuntu-24.04 - triple: x86_64-apple-darwin runner: macos-15-intel - triple: x86_64-pc-windows-msvc @@ -66,11 +69,12 @@ jobs: bazelisk-version: 1.29.0 # Release commands run main's workflows, even for patch releases of older - # release branches, which don't have the script; skip those. + # release branches. Skip refs without the platforms that build.sh takes, + # since their scripts don't build for the job's platform, or don't exist. - name: Build artifacts id: build - if: hashFiles('dev/release_artifacts/build.sh') != '' - run: dev/release_artifacts/build.sh "$REF" + if: hashFiles('dev/platforms/BUILD.bazel') != '' + run: dev/release_artifacts/build.sh "$REF" "$TRIPLE" # build.sh only sets `files` if the build succeeded. Otherwise, it adds a # warning instead of failing, since releases don't use the files yet. diff --git a/MODULE.bazel b/MODULE.bazel index 18a5196952..8b140ccdf7 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -112,6 +112,44 @@ rust_crates.spec( rust_crates.from_specs() use_repo(rust_crates, "crates") +# A Rust toolchain to cross-compile the release artifacts for Linux aarch64 +# from Linux x86_64; see dev/release_artifacts. It targets musl, which Rust can +# link statically with its bundled rust-lld, so no C cross toolchain is needed. +rust = use_extension( + "@rules_rust//rust:extensions.bzl", + "rust", + dev_dependency = True, +) +rust.repository_set( + # rules_rust also registers an x86_64 to x86_64 toolchain from this set, in + # name order with its default ones, so the name must sort after + # rust_linux_x86_64 for the default x86_64 toolchain to take precedence. + name = "rust_release_linux_x86_64", + # The default allocator library needs a C toolchain for the target platform, + # but there isn't one. This one is what rules_rust uses for e.g. Windows. + allocator_library = "@rules_rust//rust/private/cc:empty", + edition = "2021", + exec_triple = "x86_64-unknown-linux-gnu", + target_compatible_with = [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + "//dev/platforms:musl", + ], + target_triple = "aarch64-unknown-linux-musl", + # rules_rust 0.73.0's default version; update it with rules_rust. The + # default toolchain builds proc-macros, which rustc only loads if they were + # built by the same version. + versions = ["1.97.1"], +) +use_repo(rust, "rust_release_linux_x86_64__aarch64-unknown-linux-musl__stable") + +# Registered here so that it takes precedence over rules_rust's default +# toolchains, whose Linux x86_64 to aarch64 one also matches the musl platform. +register_toolchains( + "@rust_release_linux_x86_64__aarch64-unknown-linux-musl__stable//:toolchain", + dev_dependency = True, +) + # Use the Rust-based build tools when developing rules_python itself. register_toolchains( "//dev/dev_only_toolchains:exe_zip_maker_toolchain", diff --git a/dev/platforms/BUILD.bazel b/dev/platforms/BUILD.bazel new file mode 100644 index 0000000000..d381862c74 --- /dev/null +++ b/dev/platforms/BUILD.bazel @@ -0,0 +1,67 @@ +# Platforms that rules_python's release artifacts are built for, named after +# their Rust target triples; see dev/release_artifacts/build.sh. Linux x86_64 +# hosts cross-compile for the musl one with the musl Rust toolchain in +# MODULE.bazel. The others are built on hosts that match them. + +package(default_visibility = ["//:__subpackages__"]) + +platform( + name = "aarch64-apple-darwin", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:macos", + ], +) + +platform( + name = "aarch64-pc-windows-msvc", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:windows", + ], +) + +platform( + name = "aarch64-unknown-linux-musl", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:linux", + ":musl", + ], +) + +platform( + name = "x86_64-apple-darwin", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:macos", + ], +) + +platform( + name = "x86_64-pc-windows-msvc", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + ], +) + +platform( + name = "x86_64-unknown-linux-gnu", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], +) + +# The libc of Linux platforms. Only musl is a constraint value: platforms +# without it, like the host, use glibc. It also keeps the musl Rust toolchain +# in MODULE.bazel from being used for glibc platforms. +constraint_setting(name = "libc") + +constraint_value( + name = "musl", + constraint_setting = ":libc", + # The musl Rust toolchain's repo, generated by rules_rust, refers to it. + visibility = ["//visibility:public"], +) diff --git a/dev/release_artifacts/BUILD.bazel b/dev/release_artifacts/BUILD.bazel index d44628da4a..4c7f904b8c 100644 --- a/dev/release_artifacts/BUILD.bazel +++ b/dev/release_artifacts/BUILD.bazel @@ -4,7 +4,8 @@ load(":release_files.bzl", "release_files") package(default_visibility = ["//:__subpackages__"]) -# Files that the release workflow builds for each platform; see build.sh. +# Files that the release workflow builds for each platform in dev/platforms; +# see build.sh. release_files( name = "artifacts_for_release", srcs = ["//crates/exe_zip_maker"], diff --git a/dev/release_artifacts/build.sh b/dev/release_artifacts/build.sh index 1460a34722..00a81fa284 100755 --- a/dev/release_artifacts/build.sh +++ b/dev/release_artifacts/build.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash -# Builds //dev/release_artifacts:artifacts_for_release for the host platform. -# The target applies the release settings; see release_files.bzl. In GitHub -# Actions, the paths of the built files, relative to the workspace root, are -# set as the step's `files` output, one per line. +# Builds //dev/release_artifacts:artifacts_for_release for the platform with +# the Rust target triple TRIPLE; see dev/platforms. The target applies the +# release settings for the platform's OS and libc; see release_files.bzl. In +# GitHub Actions, the paths of the built files, relative to the workspace root, +# are set as the step's `files` output, one per line. # -# Usage: dev/release_artifacts/build.sh EMBED_LABEL +# Usage: dev/release_artifacts/build.sh EMBED_LABEL TRIPLE set -euo pipefail embed_label="$1" +triple="$2" # The paths below are relative to the workspace root. cd "$(dirname "$0")/../.." @@ -16,7 +18,8 @@ cd "$(dirname "$0")/../.." export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL="*" if ! bazel build --verbose_failures --compilation_mode=opt --stamp \ - --embed_label="$embed_label" //dev/release_artifacts:artifacts_for_release; then + --embed_label="$embed_label" --platforms="//dev/platforms:$triple" \ + //dev/release_artifacts:artifacts_for_release; then # Releases don't use the files yet, so don't fail the release over them. echo "::warning::Building the release artifacts failed. Releases don't use them yet." exit 0 diff --git a/dev/release_artifacts/release_files.bzl b/dev/release_artifacts/release_files.bzl index 7bd3662306..857f049e26 100644 --- a/dev/release_artifacts/release_files.bzl +++ b/dev/release_artifacts/release_files.bzl @@ -8,11 +8,11 @@ _RUSTC_FLAGS = "@rules_rust//rust/settings:extra_rustc_flags" def _release_transition_impl(settings, attr): features = [] rustc_flags = [] - if attr.target_os == "linux": + if attr.target_os == "linux" and attr.target_libc != "musl": # Rust links glibc dynamically, so the files would require at least # the build machine's glibc version. Link it statically so they run on # older distros too. The gold linker can't link static glibc, so use - # bfd. + # bfd. Rust already links musl statically. rustc_flags = [ "-Ctarget-feature=+crt-static", "-Clink-arg=-fuse-ld=bfd", @@ -60,6 +60,9 @@ _release_files = rule( cfg = _release_transition, doc = "The files to build and list.", ), + "target_libc": attr.string( + doc = "The libc that the files are built for. Set by the macro.", + ), "target_os": attr.string( doc = "The OS that the files are built for. Set by the macro.", ), @@ -69,9 +72,10 @@ _release_files = rule( def release_files(name, srcs, **kwargs): """Builds files for a release and writes their paths to `.txt`. - The files are built with release settings: optimized, and linked so that - they run on older OS versions than the build machine's, without extra - runtime libraries. + The files are built for the target platform (`--platforms`), with release + settings for its OS and libc: optimized, and linked so that they run on + older OS versions than the build machine's, without extra runtime + libraries. The paths, one per line, are relative to the execroot, e.g. `bazel-out/k8-opt-ST-1234/bin/foo/foo`. Paths of generated files also @@ -87,6 +91,10 @@ def release_files(name, srcs, **kwargs): _release_files( name = name, srcs = srcs, + target_libc = select({ + "//dev/platforms:musl": "musl", + "//conditions:default": "", + }), target_os = select({ Label("@platforms//os:linux"): "linux", labels.PLATFORMS_OS_WINDOWS: "windows", diff --git a/dev/release_artifacts/verify.sh b/dev/release_artifacts/verify.sh index 8982e6f937..8e86d10ce5 100755 --- a/dev/release_artifacts/verify.sh +++ b/dev/release_artifacts/verify.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -# Checks that the files built by build.sh don't require a newer OS than users -# may have. Only Linux and macOS files are checked. +# Checks the files built by build.sh for the platform with the Rust target +# triple TRIPLE: that Linux files are statically linked for the right CPU, and +# that macOS files don't require a newer macOS than users may have. Windows +# files aren't checked. # # Usage: dev/release_artifacts/verify.sh TRIPLE set -euo pipefail @@ -13,11 +15,24 @@ cd "$(dirname "$0")/../.." # The target lists the paths of its files in a manifest; see release_files.bzl. while IFS= read -r bin; do case "$triple" in - *-linux-gnu) - linkage="$(file "$bin")" - if [[ "$linkage" != *"statically linked"* && - "$linkage" != *"static-pie linked"* ]]; then - echo "::error::$bin isn't statically linked: $linkage" + *-linux-*) + info="$(file -L -b "$bin")" + if [[ "$info" != *"statically linked"* && "$info" != *"static-pie linked"* ]]; then + echo "::error::$bin isn't statically linked: $info" + exit 1 + fi + # Some Linux files are cross-compiled, so check that they're for the + # right CPU. + case "$triple" in + aarch64-*) cpu="ARM aarch64" ;; + x86_64-*) cpu="x86-64" ;; + *) + echo "::error::$bin: no CPU check for $triple" + exit 1 + ;; + esac + if [[ "$info" != *", $cpu,"* ]]; then + echo "::error::$bin isn't for $cpu: $info" exit 1 fi ;; @@ -32,5 +47,10 @@ while IFS= read -r bin; do exit 1 fi ;; + *-windows-*) ;; # Not checked. + *) + echo "::error::$bin: no checks for $triple" + exit 1 + ;; esac done < bazel-bin/dev/release_artifacts/artifacts_for_release.txt