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..1460a34722 --- /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 --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 +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