From 453983866982007e539c008a5be12a36e21e9b29 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Mon, 5 Oct 2026 01:31:44 -0700 Subject: [PATCH 1/2] build(release): build prebuilt tools in the release workflow rules_rust is only a dev dependency, so users can't build Rust tools like `exe_zip_maker` themselves; the tools must ship prebuilt. Before attaching them to releases, the release workflow should show that they build on each target platform. `//dev/release_artifacts:artifacts_for_release` builds its files with release settings through a transition: optimized, with glibc (Linux) or the C runtime (Windows) linked statically, and macOS 11 as the minimum. Presubmit's bzlmod `//...` builds now cover these settings too; under WORKSPACE, where rules_rust is a stub, the target is manual. The target also lists the files' paths, so scripts don't need `bazel cquery`. The release workflow now calls a reusable workflow that natively builds the target on Linux, macOS, and Windows (x86_64 and aarch64), with `--stamp` and the release tag as `--embed_label`, checks the Linux and macOS binaries' OS requirements, and uploads the binaries as workflow artifacts. Nothing is attached to the release yet, so a failed `bazel build` only adds a warning to the run. The workflow can also be run manually to try it before a release. --- .github/workflows/release_build_tools.yaml | 89 ++++++++++++++++++++ .github/workflows/release_publish.yaml | 9 ++ RELEASING.md | 6 ++ dev/release_artifacts/BUILD.bazel | 20 +++++ dev/release_artifacts/build.sh | 32 ++++++++ dev/release_artifacts/release_files.bzl | 96 ++++++++++++++++++++++ dev/release_artifacts/verify.sh | 36 ++++++++ 7 files changed, 288 insertions(+) create mode 100644 .github/workflows/release_build_tools.yaml create mode 100644 dev/release_artifacts/BUILD.bazel create mode 100755 dev/release_artifacts/build.sh create mode 100644 dev/release_artifacts/release_files.bzl create mode 100755 dev/release_artifacts/verify.sh diff --git a/.github/workflows/release_build_tools.yaml b/.github/workflows/release_build_tools.yaml new file mode 100644 index 0000000000..0801df0f8d --- /dev/null +++ b/.github/workflows/release_build_tools.yaml @@ -0,0 +1,89 @@ +# Builds `//dev/release_artifacts:artifacts_for_release` natively for each +# platform; see dev/release_artifacts/build.sh. +# +# For now, this only verifies that the artifacts build; they aren't attached to +# releases yet. +name: "Release: Build Tools" + +on: + workflow_call: + inputs: + ref: + description: "The git ref (e.g. release tag) to build" + required: true + type: string + workflow_dispatch: + inputs: + ref: + description: "The git ref (e.g. release tag) to build. Defaults to the selected ref." + required: false + type: string + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + build: + name: Build ${{ matrix.triple }} + runs-on: ${{ matrix.runner }} + strategy: + # Report the result of every platform instead of stopping at the first + # failure. + fail-fast: false + matrix: + include: + - triple: aarch64-apple-darwin + runner: macos-15 + - triple: aarch64-pc-windows-msvc + runner: windows-11-arm + - triple: aarch64-unknown-linux-gnu + runner: ubuntu-24.04-arm + - triple: x86_64-apple-darwin + runner: macos-15-intel + - triple: x86_64-pc-windows-msvc + runner: windows-2022 + - triple: x86_64-unknown-linux-gnu + runner: ubuntu-24.04 + env: + REF: ${{ inputs.ref || github.ref_name }} + TRIPLE: ${{ matrix.triple }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref }} + persist-credentials: false + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.19.0 + with: + # Windows images also have a fixed-version `bazel` on PATH. 1.20.0, + # used by the other workflows, has no windows-arm64 build. + 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. + - name: Build artifacts + id: build + if: hashFiles('dev/release_artifacts/build.sh') != '' + run: dev/release_artifacts/build.sh "$REF" + + # 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. + - name: Verify artifacts + if: steps.build.outputs.files != '' + run: dev/release_artifacts/verify.sh "$TRIPLE" + + # Stores the files with this workflow run so they can be downloaded from + # the run's page. Nothing is added to the GitHub release. + - name: Upload artifacts + if: steps.build.outputs.files != '' + uses: actions/upload-artifact@v7 + with: + name: release-tools-${{ matrix.triple }} + path: ${{ steps.build.outputs.files }} + if-no-files-found: error diff --git a/.github/workflows/release_publish.yaml b/.github/workflows/release_publish.yaml index eedf653fc1..cfa34cca7e 100644 --- a/.github/workflows/release_publish.yaml +++ b/.github/workflows/release_publish.yaml @@ -46,6 +46,15 @@ on: default: true jobs: + build_tools: + # For now, this only verifies that the tools build. They aren't attached + # to the release, so no other job waits on this one. A failed build only + # adds a warning to this run; a failed check of the built files fails it. + name: Build tools + uses: ./.github/workflows/release_build_tools.yaml + with: + ref: ${{ inputs.tag_name || github.ref_name }} + release: name: Release runs-on: ubuntu-latest diff --git a/RELEASING.md b/RELEASING.md index 2f94fde3b6..29251e0215 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -67,6 +67,12 @@ you can set the `publish_to_pypi` input to `false`: gh workflow run release_publish.yaml --ref -f publish_to_pypi=false ``` +### Prebuilt tools + +The release workflow also builds `//dev/release_artifacts:artifacts_for_release` +for each platform to verify it builds. For now, the files aren't attached to +the release. + ### Manually publishing to PyPI If PyPI publishing failed or was skipped during the main release, the PyPI diff --git a/dev/release_artifacts/BUILD.bazel b/dev/release_artifacts/BUILD.bazel new file mode 100644 index 0000000000..d44628da4a --- /dev/null +++ b/dev/release_artifacts/BUILD.bazel @@ -0,0 +1,20 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +load(":release_files.bzl", "release_files") + +package(default_visibility = ["//:__subpackages__"]) + +# Files that the release workflow builds for each platform; see build.sh. +release_files( + name = "artifacts_for_release", + srcs = ["//crates/exe_zip_maker"], + # Under WORKSPACE, rules_rust is a stub without the rustc flags setting + # that release_files sets, so building this would fail. + tags = [] if BZLMOD_ENABLED else ["manual"], +) + +bzl_library( + name = "release_files", + srcs = ["release_files.bzl"], + deps = ["//python/private:common_labels"], +) diff --git a/dev/release_artifacts/build.sh b/dev/release_artifacts/build.sh new file mode 100755 index 0000000000..79fbb6f914 --- /dev/null +++ b/dev/release_artifacts/build.sh @@ -0,0 +1,32 @@ +#!/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. +# +# Usage: dev/release_artifacts/build.sh EMBED_LABEL +set -euo pipefail + +embed_label="$1" + +# The paths below are relative to the workspace root. +cd "$(dirname "$0")/../.." + +# Keep Git Bash on Windows from rewriting `//foo` labels into paths. +export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL="*" + +if ! bazel build --verbose_failures --stamp --embed_label="$embed_label" \ + //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 +fi + +# The target lists the paths of its files in a manifest; see release_files.bzl. +if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "files<> "$GITHUB_OUTPUT" +fi diff --git a/dev/release_artifacts/release_files.bzl b/dev/release_artifacts/release_files.bzl new file mode 100644 index 0000000000..7bd3662306 --- /dev/null +++ b/dev/release_artifacts/release_files.bzl @@ -0,0 +1,96 @@ +"""Macro to build the files for a release and list where they are.""" + +load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility + +_FEATURES = "//command_line_option:features" +_RUSTC_FLAGS = "@rules_rust//rust/settings:extra_rustc_flags" + +def _release_transition_impl(settings, attr): + features = [] + rustc_flags = [] + if attr.target_os == "linux": + # 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. + rustc_flags = [ + "-Ctarget-feature=+crt-static", + "-Clink-arg=-fuse-ld=bfd", + ] + elif attr.target_os == "windows": + # Statically link the C runtime so that the VC++ redistributable isn't + # required. + features = ["static_link_msvcrt"] + rustc_flags = ["-Ctarget-feature=+crt-static"] + return { + "//command_line_option:compilation_mode": "opt", + # Only affects macOS. Without it, the minimum OS version is the build + # machine's SDK version. + "//command_line_option:macos_minimum_os": "11.0", + _FEATURES: settings[_FEATURES] + features, + _RUSTC_FLAGS: settings[_RUSTC_FLAGS] + rustc_flags, + } + +_release_transition = transition( + implementation = _release_transition_impl, + inputs = [_FEATURES, _RUSTC_FLAGS], + outputs = [ + "//command_line_option:compilation_mode", + "//command_line_option:macos_minimum_os", + _FEATURES, + _RUSTC_FLAGS, + ], +) + +def _release_files_impl(ctx): + manifest = ctx.actions.declare_file(ctx.label.name + ".txt") + ctx.actions.write( + output = manifest, + # End every line with "\n", including the last: `while read` loops skip + # an unterminated last line, and build.sh adds a line after the paths. + content = "".join([file.path + "\n" for file in ctx.files.srcs]), + ) + return [DefaultInfo(files = depset([manifest] + ctx.files.srcs))] + +_release_files = rule( + implementation = _release_files_impl, + attrs = { + "srcs": attr.label_list( + allow_files = True, + cfg = _release_transition, + doc = "The files to build and list.", + ), + "target_os": attr.string( + doc = "The OS that the files are built for. Set by the macro.", + ), + }, +) + +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 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 + resolve from the workspace root through the `bazel-out` convenience + symlink. This lets scripts find the files without running `bazel cquery`, + which re-analyzes the build. + + Args: + name: The target name. + srcs: The files to build and list. + **kwargs: Additional attributes for the rule. + """ + _release_files( + name = name, + srcs = srcs, + target_os = select({ + Label("@platforms//os:linux"): "linux", + labels.PLATFORMS_OS_WINDOWS: "windows", + "//conditions:default": "", + }), + **kwargs + ) diff --git a/dev/release_artifacts/verify.sh b/dev/release_artifacts/verify.sh new file mode 100755 index 0000000000..8982e6f937 --- /dev/null +++ b/dev/release_artifacts/verify.sh @@ -0,0 +1,36 @@ +#!/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. +# +# Usage: dev/release_artifacts/verify.sh TRIPLE +set -euo pipefail + +triple="$1" + +# The paths below are relative to the workspace root. +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" + exit 1 + fi + ;; + *-apple-darwin) + minos="$(otool -l "$bin" | awk ' + $2 == "LC_BUILD_VERSION" || $2 == "LC_VERSION_MIN_MACOSX" { found = 1 } + found && minos == "" && ($1 == "minos" || $1 == "version") { minos = $2 } + END { print minos } + ')" + if [[ -z "$minos" || "${minos%%.*}" -gt 11 ]]; then + echo "::error::$bin requires macOS ${minos:-}; expected 11 or lower" + exit 1 + fi + ;; + esac +done < bazel-bin/dev/release_artifacts/artifacts_for_release.txt From 3fc2b21c4cc5841a840b7697f52def543e85490b Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Wed, 7 Oct 2026 23:36:38 -0700 Subject: [PATCH 2/2] build(release): build release artifacts with --compilation_mode=opt Make it explicit in build.sh that release artifacts are optimized. The release transition already sets opt for the files, so the binaries don't change; the flag makes the rest of the build match. --- dev/release_artifacts/build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/release_artifacts/build.sh b/dev/release_artifacts/build.sh index 79fbb6f914..1460a34722 100755 --- a/dev/release_artifacts/build.sh +++ b/dev/release_artifacts/build.sh @@ -15,8 +15,8 @@ cd "$(dirname "$0")/../.." # Keep Git Bash on Windows from rewriting `//foo` labels into paths. export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL="*" -if ! bazel build --verbose_failures --stamp --embed_label="$embed_label" \ - //dev/release_artifacts:artifacts_for_release; then +if ! bazel build --verbose_failures --compilation_mode=opt --stamp \ + --embed_label="$embed_label" //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