diff --git a/README.md b/README.md index ce70baba..12ef7dce 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ s = shell.quote(p) ## List of modules (in lib/) * [collections](docs/collections_doc.md) +* [compatibility](docs/compatibility_doc.md) * [dicts](docs/dicts_doc.md) * [partial](docs/partial_doc.md) * [paths](docs/paths_doc.md) diff --git a/docs/BUILD b/docs/BUILD index 4d31cd10..ddff1b27 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -26,6 +26,11 @@ stardoc_with_diff_test( out_label = "//docs:common_settings_doc.md", ) +stardoc_with_diff_test( + bzl_library_target = "//lib:compatibility", + out_label = "//docs:compatibility_doc.md", +) + stardoc_with_diff_test( name = "copy_directory", bzl_library_target = "//rules:copy_directory", diff --git a/docs/compatibility_doc.md b/docs/compatibility_doc.md new file mode 100644 index 00000000..1e939029 --- /dev/null +++ b/docs/compatibility_doc.md @@ -0,0 +1,145 @@ + + +Skylib module of convenience functions for `target_compatible_with`. + +Load the macros as follows in your `BUILD` files: +```python +load("@bazel_skylib//lib:compatibility.bzl", "compatibility") +``` + +See the [Platform docs](https://bazel.build/docs/platforms#skipping-incompatible-targets) for +more information. + + + + +## compatibility.all_of + +
+compatibility.all_of(settings)
+
+ +Create a `select()` for `target_compatible_with` which matches all of the given settings. + +All of the settings must be true to get an empty list. Failure to match will result +in an incompatible `constraint_value` for the purpose of target skipping. + +In other words, use this function to make a target incompatible unless all of the settings are +true. + +Example: + +```python +config_setting( + name = "dbg", + values = {"compilation_mode": "dbg"}, +) + +cc_binary( + name = "bin", + srcs = ["bin.cc"], + # This target can only be built for Linux in debug mode. + target_compatible_with = compatibility.all_of( + ":dbg", + "@platforms//os:linux", + ), +) +``` + +See also: `selects.config_setting_group(match_all)` + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| settings | The config_setting or constraint_value targets. | none | + +**RETURNS** + +A native series of `select()`s. The result is "incompatible" unless all settings are true. + + + + +## compatibility.any_of + +
+compatibility.any_of(settings)
+
+ +Create a `select()` for `target_compatible_with` which matches any of the given settings. + +Any of the settings will resolve to an empty list, while the default condition will map to +an incompatible `constraint_value` for the purpose of target skipping. + +In other words, use this function to make target incompatible unless one or more of the +settings are true. + +```python +cc_binary( + name = "bin", + srcs = ["bin.cc"], + # This target can only be built for Linux or Mac. + target_compatible_with = compatibility.any_of( + "@platforms//os:linux", + "@platforms//os:macos", + ), +) +``` + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| settings | The config_settings or constraint_value targets. | none | + +**RETURNS** + +A native `select()` which maps any of the settings an empty list. + + + + +## compatibility.none_of + +
+compatibility.none_of(settings)
+
+ +Create a `select()` for `target_compatible_with` which matches none of the given settings. + +Any of the settings will resolve to an incompatible `constraint_value` for the +purpose of target skipping. + +In other words, use this function to make target incompatible if any of the settings are true. + +```python +cc_binary( + name = "bin", + srcs = ["bin.cc"], + # This target cannot be built for Linux or Mac, but can be built for + # everything else. + target_compatible_with = compatibility.none_of( + "@platforms//os:linux", + "@platforms//os:macos", + ), +) +``` + + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| settings | The config_setting or constraint_value targets. | none | + +**RETURNS** + +A native `select()` which maps any of the settings to the incompatible target. + + diff --git a/lib/BUILD b/lib/BUILD index 23280816..c44ee12f 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -15,6 +15,14 @@ bzl_library( srcs = ["collections.bzl"], ) +bzl_library( + name = "compatibility", + srcs = ["compatibility.bzl"], + deps = [ + ":selects", + ], +) + bzl_library( name = "dicts", srcs = ["dicts.bzl"], diff --git a/lib/compatibility.bzl b/lib/compatibility.bzl new file mode 100644 index 00000000..b75baab4 --- /dev/null +++ b/lib/compatibility.bzl @@ -0,0 +1,126 @@ +"""Skylib module of convenience functions for `target_compatible_with`. + +Load the macros as follows in your `BUILD` files: +```python +load("@bazel_skylib//lib:compatibility.bzl", "compatibility") +``` + +See the [Platform docs](https://bazel.build/docs/platforms#skipping-incompatible-targets) for +more information. +""" + +load(":selects.bzl", "selects") + +def _none_of(*settings): + """Create a `select()` for `target_compatible_with` which matches none of the given settings. + + Any of the settings will resolve to an incompatible `constraint_value` for the + purpose of target skipping. + + In other words, use this function to make target incompatible if any of the settings are true. + + ```python + cc_binary( + name = "bin", + srcs = ["bin.cc"], + # This target cannot be built for Linux or Mac, but can be built for + # everything else. + target_compatible_with = compatibility.none_of( + "@platforms//os:linux", + "@platforms//os:macos", + ), + ) + ``` + + Args: + *settings: The `config_setting` or `constraint_value` targets. + + Returns: + A native `select()` which maps any of the settings to the incompatible target. + """ + return selects.with_or({ + tuple(settings): ["@platforms//:incompatible"], + "//conditions:default": [], + }) + +def _any_of(*settings): + """Create a `select()` for `target_compatible_with` which matches any of the given settings. + + Any of the settings will resolve to an empty list, while the default condition will map to + an incompatible `constraint_value` for the purpose of target skipping. + + In other words, use this function to make target incompatible unless one or more of the + settings are true. + + ```python + cc_binary( + name = "bin", + srcs = ["bin.cc"], + # This target can only be built for Linux or Mac. + target_compatible_with = compatibility.any_of( + "@platforms//os:linux", + "@platforms//os:macos", + ), + ) + ``` + + Args: + *settings: The `config_settings` or `constraint_value` targets. + + Returns: + A native `select()` which maps any of the settings an empty list. + """ + return selects.with_or({ + tuple(settings): [], + "//conditions:default": ["@platforms//:incompatible"], + }) + +def _all_of(*settings): + """Create a `select()` for `target_compatible_with` which matches all of the given settings. + + All of the settings must be true to get an empty list. Failure to match will result + in an incompatible `constraint_value` for the purpose of target skipping. + + In other words, use this function to make a target incompatible unless all of the settings are + true. + + Example: + + ```python + config_setting( + name = "dbg", + values = {"compilation_mode": "dbg"}, + ) + + cc_binary( + name = "bin", + srcs = ["bin.cc"], + # This target can only be built for Linux in debug mode. + target_compatible_with = compatibility.all_of( + ":dbg", + "@platforms//os:linux", + ), + ) + ``` + + See also: `selects.config_setting_group(match_all)` + + Args: + *settings: The `config_setting` or `constraint_value` targets. + + Returns: + A native series of `select()`s. The result is "incompatible" unless all settings are true. + """ + result = [] + for setting in settings: + result += select({ + setting: [], + "//conditions:default": ["@platforms//:incompatible"], + }) + return result + +compatibility = struct( + all_of = _all_of, + any_of = _any_of, + none_of = _none_of, +) diff --git a/tests/BUILD b/tests/BUILD index bbab077a..babb4b95 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -92,6 +92,19 @@ sh_test( tags = ["local"], ) +sh_test( + name = "compatibility_test", + srcs = ["compatibility_test.sh"], + data = [ + ":unittest.bash", + "//lib:compatibility", + ], + tags = ["local"], + deps = [ + "@bazel_tools//tools/bash/runfiles", + ], +) + shell_args_test_gen( name = "shell_spawn_e2e_test_src", ) diff --git a/tests/compatibility_test.sh b/tests/compatibility_test.sh new file mode 100755 index 00000000..80007111 --- /dev/null +++ b/tests/compatibility_test.sh @@ -0,0 +1,320 @@ +#!/bin/bash +# +# Copyright 2022 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Test building targets that are declared as compatible only with certain +# platforms (see the "target_compatible_with" common build rule attribute). + +# --- begin runfiles.bash initialization v2 --- +# Copy-pasted from the Bazel Bash runfiles library v2. +set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash +source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \ + source "$0.runfiles/$f" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \ + { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e +# --- end runfiles.bash initialization v2 --- + +source "$(rlocation bazel_skylib/tests/unittest.bash)" \ + || { echo "Could not source bazel_skylib/tests/unittest.bash" >&2; exit 1; } + +# `uname` returns the current platform, e.g "MSYS_NT-10.0" or "Linux". +# `tr` converts all upper case letters to lower case. +# `case` matches the result if the `uname | tr` expression to string prefixes +# that use the same wildcards as names do in Bash, i.e. "msys*" matches strings +# starting with "msys", and "*" matches everything (it's the default case). +case "$(uname -s | tr [:upper:] [:lower:])" in +msys*) + # As of 2019-01-15, Bazel on Windows only supports MSYS Bash. + declare -r is_windows=true + ;; +*) + declare -r is_windows=false + ;; +esac + +if "$is_windows"; then + export MSYS_NO_PATHCONV=1 + export MSYS2_ARG_CONV_EXCL="*" +fi + +function set_up() { + mkdir -p target_skipping || fail "couldn't create directory" + + cat > target_skipping/pass.sh < WORKSPACE < BUILD < bzl_library.bzl < lib/BUILD < target_skipping/BUILD < "${TEST_log}" \ + || fail "Bazel failed unexpectedly." + + expect_log "INFO: Build completed successfully" + done +} + +# Builds the specified target against various platforms and expects the builds +# to fail. +function ensure_that_target_doesnt_build_for_platforms() { + local target="$1" + local error_string="$2" + local platform + + for platform in "${@:3}"; do + echo "Building ${target} for ${platform}. Expecting failure." + bazel build \ + --show_result=10 \ + --host_platform="${platform}" \ + --platforms="${platform}" \ + --nocache_test_results \ + "${target}" &> "${TEST_log}" \ + && fail "Bazel passed unexpectedly." + + expect_log "ERROR: Target ${target} is incompatible and cannot be built, but was explicitly requested" + expect_log " <-- target platform (${platform}) ${error_string}" + expect_log 'FAILED: Build did NOT complete successfully' + done +} + +# Validates that we can express targets being compatible with A _or_ B. +function test_any_of_logic() { + cat >> target_skipping/BUILD <> target_skipping/BUILD <> target_skipping/BUILD <> target_skipping/BUILD <