diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index dc13b38e14..219a1e5b7b 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -35,16 +35,24 @@ buildifier: # As a regression test for #225, check that wheel targets still build when # their package path is qualified with the repo name. - "@rules_python//examples/wheel/..." + # Tasks can merge these into their own build_flags or test_flags with `<<:`. + # To change a merged flag, such as a tag filter, list it again: Bazel uses the + # last value. build_flags: &reusable_config_build_flags ? "--experimental_repository_cache_hardlinks=false" ? "--keep_going" ? "--build_tag_filters=-integration-test" ? "--verbose_failures" + # Among other things, makes missing Visual Studio ARM64 build tools an + # error on Windows; see .bazelrc. test_flags repeat it so that `bazel test` + # reuses the build's analysis cache. + ? "--config=ci" test_targets: - "--" - "..." - test_flags: + test_flags: &reusable_config_test_flags ? "--test_tag_filters=-integration-test" + ? "--config=ci" .common_workspace_flags_min_bazel: &common_workspace_flags_min_bazel build_flags: ? "--experimental_repository_cache_hardlinks=false" @@ -245,8 +253,8 @@ tasks: - "--" - "//tests/..." test_flags: + <<: *reusable_config_test_flags ? "--keep_going" - ? "--test_tag_filters=-integration-test" ubuntu_workspace: <<: *reusable_config <<: *common_workspace_flags @@ -299,6 +307,7 @@ tasks: platform: windows bazel: ${{ bazel }} test_flags: + <<: *reusable_config_test_flags ? "--test_tag_filters=-integration-test,-fix-windows" rbe_min: <<: *minimum_supported_version @@ -306,7 +315,7 @@ tasks: name: "RBE: Ubuntu, minimum Bazel" platform: rbe_ubuntu2404 build_flags: - ? "--experimental_repository_cache_hardlinks=false" + <<: *reusable_config_build_flags # BazelCI sets --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1, # which prevents cc toolchain autodetection from working correctly # on Bazel 5.4 and earlier. To workaround this, manually specify the @@ -314,6 +323,7 @@ tasks: ? "--extra_toolchains=@buildkite_config//config:cc-toolchain" ? "--build_tag_filters=-docs" test_flags: + <<: *reusable_config_test_flags ? "--test_tag_filters=-integration-test,-acceptance-test,-docs" # BazelCI sets --action_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1, # which prevents cc toolchain autodetection from working correctly @@ -328,6 +338,7 @@ tasks: # See https://github.com/bazelbuild/rules_python/issues/2499 bazel: 8.x test_flags: + <<: *reusable_config_test_flags ? "--test_tag_filters=-integration-test,-acceptance-test" ? "--extra_toolchains=@buildkite_config//config:cc-toolchain" diff --git a/.bazelrc b/.bazelrc index 12d1b33865..24aba939a6 100644 --- a/.bazelrc +++ b/.bazelrc @@ -48,13 +48,19 @@ common --disk_cache=~/.cache/bazel/bazel-disk-cache common --repository_cache=~/.cache/bazel/bazel-repo-cache # Hardlinks save disk space and copy time when unpacking repositories common --experimental_repository_cache_hardlinks=true -# To work around bug on bazel 7 -common:ci --experimental_repository_cache_hardlinks=false # Drop `experimental_` prefix once Bazel 7 is no longer supported common --experimental_downloader_config=downloader_config.cfg common --http_timeout_scaling=10.0 common --experimental_repository_downloader_retries=10 +# BazelCI's default and RBE bzlmod jobs pass --config=ci; see +# .bazelci/presubmit.yml. +# To work around bug on bazel 7 +common:ci --experimental_repository_cache_hardlinks=false +# Make missing Visual Studio ARM64 build tools an error on Windows x86_64; see +# dev/release_artifacts/BUILD.bazel. +common:ci --//dev/release_artifacts:require_msvc_arm64=true + # Additional config to use for readthedocs builds. # See .readthedocs.yml for additional flags that can only be determined from # the runtime environment. diff --git a/MODULE.bazel b/MODULE.bazel index 532c0c1832..989aa89965 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -230,10 +230,11 @@ register_toolchains( dev_dependency = True, ) -# A Rust toolchain to cross-compile the release artifacts for 32-bit Windows -# from Windows x86_64, with MSVC's x86 tools. Unlike the Linux ones, it isn't -# registered here: no default toolchain targets 32-bit Windows, and rules_rust -# registers it. +# Rust toolchains to cross-compile the release artifacts for 32-bit and Arm +# Windows from Windows x86_64, with MSVC's cross tools; the Arm one needs +# Visual Studio's optional ARM64 build tools. Unlike the Linux ones, they +# aren't registered here: no default toolchain targets these platforms from +# Windows x86_64, and rules_rust registers them. rust.repository_set( # As above, its x86_64 toolchain's name must sort after the default one's. # On Windows, rules_rust names toolchains after a hash of the full name: @@ -250,6 +251,16 @@ rust.repository_set( versions = ["1.97.1"], ) +# Another target for the same set; the first tag sets the shared attributes. +rust.repository_set( + name = "rust_release_windows", + target_compatible_with = [ + "@platforms//cpu:aarch64", + "@platforms//os:windows", + ], + target_triple = "aarch64-pc-windows-msvc", +) + # Use the Rust-based build tools when developing rules_python itself. register_toolchains( "//dev/dev_only_toolchains:exe_zip_maker_toolchain", diff --git a/RELEASING.md b/RELEASING.md index cbd9bc1b0a..2bf5e59fd0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -73,6 +73,9 @@ 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, so a failed "Build tools" job doesn't block it. +Presubmit builds it for every platform too: natively on each CI host, and +through `//dev/release_artifacts:cross_compiled_artifacts` for the others. + ### Manually publishing to PyPI If PyPI publishing failed or was skipped during the main release, the PyPI diff --git a/dev/platforms/BUILD.bazel b/dev/platforms/BUILD.bazel index 90657fb1a3..17f19cc357 100644 --- a/dev/platforms/BUILD.bazel +++ b/dev/platforms/BUILD.bazel @@ -5,9 +5,13 @@ # aarch64 macOS file. s390x isn't covered yet: Rust ships no musl std for it, # and a static glibc build needs a C cross toolchain. # -# Linux x86_64 hosts cross-compile for the musl ones, and Windows x86_64 hosts -# for i686-pc-windows-msvc, with the Rust toolchains in MODULE.bazel. The -# others are built on hosts that match them. +# Linux x86_64 hosts can cross-compile for the musl ones, Windows x86_64 hosts +# for the other Windows ones (Arm needs Visual Studio's ARM64 build tools), and +# macOS arm64 hosts for x86_64-apple-darwin, with the Rust toolchains in +# MODULE.bazel and rules_rust's defaults. +# //dev/release_artifacts:cross_compiled_artifacts cross-compiles them all; the +# release workflow builds aarch64-pc-windows-msvc and x86_64-apple-darwin on +# hosts that match them instead. package(default_visibility = ["//:__subpackages__"]) diff --git a/dev/release_artifacts/BUILD.bazel b/dev/release_artifacts/BUILD.bazel index 4c7f904b8c..d81b5294b1 100644 --- a/dev/release_artifacts/BUILD.bazel +++ b/dev/release_artifacts/BUILD.bazel @@ -1,6 +1,11 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility -load(":release_files.bzl", "release_files") +load( + ":cc_toolchain_available_config_setting.bzl", + "cc_toolchain_available_config_setting", +) +load(":release_files.bzl", "cross_compiled_files", "release_files") package(default_visibility = ["//:__subpackages__"]) @@ -14,8 +19,99 @@ release_files( tags = [] if BZLMOD_ENABLED else ["manual"], ) +# artifacts_for_release for the other platforms in dev/platforms that the host +# can cross-compile for, so that builds of //... on each host cover them. The +# select()s match the target platform, which is the host's unless --platforms +# is set. Arm Windows needs its own select(): Windows x86_64 hosts can match +# both is_windows_x86_64 and is_windows_x86_64_with_msvc_arm64, and in a single +# select() that ambiguous match would be an error. +# +# To reproduce a failure, build artifacts_for_release with +# --platforms=//dev/platforms:; see build.sh. If a platform's build +# breaks presubmit without a related change, tag this target "manual" until +# it's fixed. +cross_compiled_files( + name = "cross_compiled_artifacts", + src = ":artifacts_for_release", + platforms = select({ + ":is_linux_x86_64": [ + "//dev/platforms:aarch64-unknown-linux-musl", + "//dev/platforms:armv7-unknown-linux-musleabihf", + "//dev/platforms:i686-unknown-linux-musl", + "//dev/platforms:powerpc64le-unknown-linux-musl", + "//dev/platforms:riscv64gc-unknown-linux-musl", + ], + ":is_macos_aarch64": ["//dev/platforms:x86_64-apple-darwin"], + ":is_windows_x86_64": ["//dev/platforms:i686-pc-windows-msvc"], + "//conditions:default": [], + }) + select({ + ":is_windows_x86_64_with_msvc_arm64": [ + "//dev/platforms:aarch64-pc-windows-msvc", + ], + "//conditions:default": [], + }), + # See artifacts_for_release. + tags = [] if BZLMOD_ENABLED else ["manual"], +) + +# Matches on Windows x86_64 if Visual Studio's optional ARM64 build tools +# (Microsoft.VisualStudio.Component.VC.Tools.ARM64) are installed, so that the +# C++ toolchain for Windows arm64 works. Without them, aarch64-pc-windows-msvc +# is skipped, or the build fails if require_msvc_arm64 is set. After installing +# them, run `bazel fetch --configure --force` so that rules_cc detects them. +cc_toolchain_available_config_setting( + name = "is_windows_x86_64_with_msvc_arm64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + ], + platform = "//dev/platforms:aarch64-pc-windows-msvc", + required_flag = ":require_msvc_arm64", +) + +# BazelCI sets this through --config=ci (see .bazelrc). Its Windows images have +# the ARM64 build tools, so losing them fails the build instead of going +# unnoticed. It has no effect unless the target platform is Windows x86_64. +bool_flag( + name = "require_msvc_arm64", + build_setting_default = False, +) + +config_setting( + name = "is_linux_x86_64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], +) + +config_setting( + name = "is_macos_aarch64", + constraint_values = [ + "@platforms//cpu:aarch64", + "@platforms//os:macos", + ], +) + +config_setting( + name = "is_windows_x86_64", + constraint_values = [ + "@platforms//cpu:x86_64", + "@platforms//os:windows", + ], +) + bzl_library( name = "release_files", srcs = ["release_files.bzl"], deps = ["//python/private:common_labels"], ) + +bzl_library( + name = "cc_toolchain_available_config_setting", + srcs = ["cc_toolchain_available_config_setting.bzl"], + deps = [ + "@bazel_skylib//rules:common_settings", + "@rules_cc//cc:find_cc_toolchain_bzl", + ], +) diff --git a/dev/release_artifacts/cc_toolchain_available_config_setting.bzl b/dev/release_artifacts/cc_toolchain_available_config_setting.bzl new file mode 100644 index 0000000000..44122da195 --- /dev/null +++ b/dev/release_artifacts/cc_toolchain_available_config_setting.bzl @@ -0,0 +1,122 @@ +"""Config setting for whether a C++ toolchain can build for a platform.""" + +load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") +load( + "@rules_cc//cc:find_cc_toolchain.bzl", + "find_cc_toolchain", + "use_cc_toolchain", +) + +_ProbeInfo = provider( + doc = "The result of looking for a C++ toolchain.", + fields = {"available": "bool: whether a usable C++ toolchain was found."}, +) + +def _probe_impl(ctx): + cc_toolchain = find_cc_toolchain(ctx, mandatory = False) + + # rules_cc registers its Windows toolchains even if their tools are + # missing, and then runs an error script instead of them, e.g. + # vc_installation_error_arm64.bat or clang_installation_error.bat. + available = cc_toolchain != None and ( + "installation_error" not in cc_toolchain.compiler_executable + ) + return [_ProbeInfo(available = available)] + +_probe = rule( + implementation = _probe_impl, + toolchains = use_cc_toolchain(mandatory = False), +) + +def _platform_transition_impl(_settings, attr): + return {"//command_line_option:platforms": str(attr.platform)} + +_platform_transition = transition( + implementation = _platform_transition_impl, + inputs = [], + outputs = ["//command_line_option:platforms"], +) + +def _cc_toolchain_available_impl(ctx): + for constraint in ctx.attr.constraint_values: + if not ctx.target_platform_has_constraint( + constraint[platform_common.ConstraintValueInfo], + ): + return [config_common.FeatureFlagInfo(value = "no")] + + # With a transition, ctx.attr.probe is a list. + available = ctx.attr.probe[0][_ProbeInfo].available + required = ( + ctx.attr.required_flag != None and + ctx.attr.required_flag[BuildSettingInfo].value + ) + if required and not available: + fail(( + "{} is set, but no usable C++ toolchain can build for {}. " + + "Install one, or unset the flag." + ).format(ctx.attr.required_flag.label, ctx.attr.platform.label)) + return [config_common.FeatureFlagInfo(value = "yes" if available else "no")] + +_cc_toolchain_available = rule( + implementation = _cc_toolchain_available_impl, + attrs = { + "constraint_values": attr.label_list( + providers = [platform_common.ConstraintValueInfo], + ), + "platform": attr.label( + mandatory = True, + providers = [platform_common.PlatformInfo], + ), + "probe": attr.label( + cfg = _platform_transition, + mandatory = True, + providers = [_ProbeInfo], + ), + "required_flag": attr.label(providers = [BuildSettingInfo]), + }, +) + +def cc_toolchain_available_config_setting( + name, + platform, + constraint_values = None, + required_flag = None, + visibility = None): + """Matches if a usable C++ toolchain can build for `platform`. + + Toolchain resolution for `platform` finds the toolchain, so the result + reflects the registered toolchains and execution platforms. A toolchain + that runs one of rules_cc's error scripts instead of a compiler isn't + usable. + + Args: + name: The name of the `config_setting`. + platform: The platform to look for a C++ toolchain for. + constraint_values: Constraint values that the target platform must + also have, as in `config_setting`. + required_flag: The label of a `bool_flag`. When it's true and the + target platform has `constraint_values`, a missing or unusable + toolchain fails the build instead of not matching. + visibility: The visibility of the `config_setting`. + """ + constraint_values = constraint_values or [] + + _probe( + name = name + "_probe", + tags = ["manual"], + visibility = ["//visibility:private"], + ) + _cc_toolchain_available( + name = name + "_flag", + constraint_values = constraint_values, + platform = platform, + probe = ":" + name + "_probe", + required_flag = required_flag, + tags = ["manual"], + visibility = ["//visibility:private"], + ) + native.config_setting( + name = name, + flag_values = {":" + name + "_flag": "yes"}, + visibility = visibility, + ) diff --git a/dev/release_artifacts/release_files.bzl b/dev/release_artifacts/release_files.bzl index b5bde5f65f..ae6563c6c4 100644 --- a/dev/release_artifacts/release_files.bzl +++ b/dev/release_artifacts/release_files.bzl @@ -1,4 +1,4 @@ -"""Macro to build the files for a release and list where they are.""" +"""Macro and rule to build the files for a release and list where they are.""" load("//python/private:common_labels.bzl", "labels") # buildifier: disable=bzl-visibility @@ -102,3 +102,47 @@ def release_files(name, srcs, **kwargs): }), **kwargs ) + +def _platforms_transition_impl(_settings, attr): + # A split transition: one configuration per platform, keyed by its label. + return { + str(platform): {"//command_line_option:platforms": str(platform)} + for platform in attr.platforms + } + +_platforms_transition = transition( + implementation = _platforms_transition_impl, + inputs = [], + outputs = ["//command_line_option:platforms"], +) + +def _cross_compiled_files_impl(ctx): + # Without platforms, the transition leaves src in this target's + # configuration, so skip it. + if not ctx.attr.platforms: + return [DefaultInfo()] + + # With a transition, ctx.attr.src is a list: one target per platform. + files = [src[DefaultInfo].files for src in ctx.attr.src] + return [DefaultInfo(files = depset(transitive = files))] + +cross_compiled_files = rule( + implementation = _cross_compiled_files_impl, + doc = """ +Builds `src` for each platform in `platforms`, in one build. + +`--platforms` selects only one target platform per build. With no platforms, +this builds nothing. +""", + attrs = { + "platforms": attr.label_list( + doc = "The platforms to build `src` for.", + providers = [platform_common.PlatformInfo], + ), + "src": attr.label( + cfg = _platforms_transition, + doc = "The target to build.", + mandatory = True, + ), + }, +) diff --git a/tests/dev/BUILD.bazel b/tests/dev/BUILD.bazel new file mode 100644 index 0000000000..5ed87cd1df --- /dev/null +++ b/tests/dev/BUILD.bazel @@ -0,0 +1,12 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility + +# Builds the cross-compiled release files in test runs too, e.g. with +# --build_tests_only. +build_test( + name = "cross_compiled_artifacts_build_test", + # Under WORKSPACE, rules_rust is a stub, so the target can't build; see + # //dev/release_artifacts:artifacts_for_release. + tags = [] if BZLMOD_ENABLED else ["manual"], + targets = ["//dev/release_artifacts:cross_compiled_artifacts"], +)