diff --git a/cc/toolchains/args.bzl b/cc/toolchains/args.bzl index 3409d00..a19c16a 100644 --- a/cc/toolchains/args.bzl +++ b/cc/toolchains/args.bzl @@ -56,7 +56,7 @@ def _cc_args_impl(ctx): args = tuple([args]), files = files, by_action = tuple([ - struct(action = action, args = [args], files = files) + struct(action = action, args = tuple([args]), files = files) for action in actions.to_list() ]), ), diff --git a/cc/toolchains/cc_toolchain_info.bzl b/cc/toolchains/cc_toolchain_info.bzl index ed2bdb6..c4b2344 100644 --- a/cc/toolchains/cc_toolchain_info.bzl +++ b/cc/toolchains/cc_toolchain_info.bzl @@ -89,7 +89,8 @@ FeatureInfo = provider( "implies": "(depset[FeatureInfo]) Set of features implied by this feature", "requires_any_of": "(Sequence[FeatureSetInfo]) A list of feature sets, at least one of which is required to enable this feature. This is semantically equivalent to the requires attribute of rules_cc's FeatureInfo", "mutually_exclusive": "(Sequence[MutuallyExclusiveCategoryInfo]) Indicates that this feature is one of several mutually exclusive alternate features.", - "known": "(bool) Whether the feature is a known feature. Known features are assumed to be defined elsewhere.", + "external": "(bool) Whether a feature is defined elsewhere.", + "overridable": "(bool) Whether the feature is an overridable feature.", "overrides": "(Optional[FeatureInfo]) The feature that this overrides. Must be a known feature", }, ) diff --git a/cc/toolchains/feature.bzl b/cc/toolchains/feature.bzl new file mode 100644 index 0000000..c81a756 --- /dev/null +++ b/cc/toolchains/feature.bzl @@ -0,0 +1,243 @@ +# Copyright 2024 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. +"""Implementation of the cc_feature rule.""" + +load( + "//cc/toolchains/impl:collect.bzl", + "collect_args_lists", + "collect_features", + "collect_provider", +) +load( + ":cc_toolchain_info.bzl", + "ArgsListInfo", + "FeatureConstraintInfo", + "FeatureInfo", + "FeatureSetInfo", + "MutuallyExclusiveCategoryInfo", +) + +def _cc_feature_impl(ctx): + if bool(ctx.attr.feature_name) == (ctx.attr.overrides != None): + fail("Exactly one of 'feature_name' and 'overrides' are required") + + if ctx.attr.overrides == None: + overrides = None + + # In the future, we may consider making feature_name optional, + # defaulting to ctx.label.name. However, starting that way would make it + # very difficult if we did want to later change that. + name = ctx.attr.feature_name + else: + overrides = ctx.attr.overrides[FeatureInfo] + if not overrides.overridable: + fail("Attempting to override %s, which is not overridable" % overrides.label) + name = overrides.name + + # In the following scenario: + # cc_args(name = "foo", env = {"FOO": "BAR"}, args = ["--foo"]) + # cc_action_config(name = "ac", args=[":foo"]) + + # We will translate this into providers roughly equivalent to the following: + # cc_args(name = "implied_by_ac_env", env = {"FOO": "BAR"}, args = ["--foo"]) + # cc_feature(name = "implied_by_ac", args = [":implied_by_ac_env"]) + # cc_action_config( + # name = "c_compile", + # implies = [":implied_by_c_compile"] + # ) + + # The reason for this is because although the legacy providers support + # flag_sets in action_config, they don't support env sets. + if name.startswith("implied_by_"): + fail("Feature names starting with 'implied_by' are reserved") + + feature = FeatureInfo( + label = ctx.label, + name = name, + enabled = ctx.attr.enabled, + args = collect_args_lists(ctx.attr.args, ctx.label), + implies = collect_features(ctx.attr.implies), + requires_any_of = tuple(collect_provider( + ctx.attr.requires_any_of, + FeatureSetInfo, + )), + mutually_exclusive = tuple(collect_provider( + ctx.attr.mutually_exclusive, + MutuallyExclusiveCategoryInfo, + )), + external = False, + overridable = False, + overrides = overrides, + ) + + return [ + feature, + FeatureSetInfo(label = ctx.label, features = depset([feature])), + FeatureConstraintInfo( + label = ctx.label, + all_of = depset([feature]), + none_of = depset([]), + ), + MutuallyExclusiveCategoryInfo(label = ctx.label, name = name), + ] + +cc_feature = rule( + implementation = _cc_feature_impl, + # @unsorted-dict-items + attrs = { + "feature_name": attr.string( + doc = """The name of the feature that this rule implements. + +The feature name is a string that will be used in the `features` attribute of +rules to enable them (eg. `cc_binary(..., features = ["opt"])`. + +While two features with the same `feature_name` may not be bound to the same +toolchain, they can happily live alongside each other in the same BUILD file. + +Example: + + cc_feature( + name = "sysroot_macos", + feature_name = "sysroot", + ... + ) + + cc_feature( + name = "sysroot_linux", + feature_name = "sysroot", + ... + ) +""", + ), + "enabled": attr.bool( + mandatory = True, + doc = """Whether or not this feature is enabled by default.""", + ), + "args": attr.label_list( + mandatory = True, + doc = """Args that, when expanded, implement this feature.""", + providers = [ArgsListInfo], + ), + "requires_any_of": attr.label_list( + doc = """A list of feature sets that define toolchain compatibility. + +If *at least one* of the listed `cc_feature_set`s are fully satisfied (all +features exist in the toolchain AND are currently enabled), this feature is +deemed compatible and may be enabled. + +Note: Even if `cc_feature.requires_any_of` is satisfied, a feature is not +enabled unless another mechanism (e.g. command-line flags, `cc_feature.implies`, +`cc_feature.enabled`) signals that the feature should actually be enabled. +""", + providers = [FeatureSetInfo], + ), + "implies": attr.label_list( + providers = [FeatureSetInfo], + doc = """List of features enabled along with this feature. + +Warning: If any of the features cannot be enabled, this feature is +silently disabled. +""", + ), + "mutually_exclusive": attr.label_list( + providers = [MutuallyExclusiveCategoryInfo], + doc = """A list of things that this is mutually exclusive with. + +It can be either: +* A feature, in which case the two features are mutually exclusive. +* A `cc_mutually_exclusive_category`, in which case all features that write + `mutually_exclusive = [":category"]` are mutually exclusive with each other. + +If this feature has a side-effect of implementing another feature, it can be +useful to list that feature here to ensure they aren't enabled at the +same time. +""", + ), + "overrides": attr.label( + providers = [FeatureInfo], + doc = """A declaration that this feature overrides a known feature. + +In the example below, if you missed the "overrides" attribute, it would complain +that the feature "opt" was defined twice. + +Example: + + cc_feature( + name = "opt", + feature_name = "opt", + ... + overrides = "@toolchain//features/well_known:opt", + ) + +""", + ), + }, + provides = [ + FeatureInfo, + FeatureSetInfo, + FeatureConstraintInfo, + MutuallyExclusiveCategoryInfo, + ], + doc = """Defines the implemented behavior of a C/C++ toolchain feature. + +A feature is basically a toggleable list of args. There are a variety of +dependencies and compatibility requirements that must be satisfied for the +listed args to be applied. + +A feature may be enabled or disabled through the following mechanisms: +* Via command-line flags, or a `.bazelrc`. +* Through inter-feature relationships (enabling one feature may implicitly + enable another). +* Individual rules may elect to manually enable or disable features through the + builtin `features` attribute. + +Because of the toggleable nature of toolchain features, it's generally best to +avoid defining features as part of your toolchain with the following exceptions: +* You want build files to be able to configure compiler flags. For example, a + binary might specify `features = ["optimize_for_size"]` to create a small + binary instead of optimizing for performance. +* You need to carry forward Starlark toolchain behaviors. If you're migrating a + complex Starlark-based toolchain definition to these rules, many of the + workflows and flags were likely based on features. This rule exists to support + those existing structures. + +If you want to be able to configure flags via the bazel command-line, instead +consider making a bool_flag, and then making your `cc_args` `select` on those +flags. + +For more details about how Bazel handles features, see the official Bazel +documentation at +https://bazel.build/docs/cc-toolchain-config-reference#features. + +Examples: + + # A feature that can be easily toggled to optimize for size + cc_feature( + name = "optimize_for_size", + enabled = False, + feature_name = "optimize_for_size", + args = [":optimize_for_size_args"], + ) + + # This feature signals a capability, and doesn't have associated flags. + # + # For a list of well-known features, see: + # https://bazel.build/docs/cc-toolchain-config-reference#wellknown-features + cc_feature( + name = "supports_pic", + enabled = True, + overrides = "//cc/toolchains/features:supports_pic + ) +""", +) diff --git a/cc/toolchains/impl/collect.bzl b/cc/toolchains/impl/collect.bzl index 5865d27..3fab3e6 100644 --- a/cc/toolchains/impl/collect.bzl +++ b/cc/toolchains/impl/collect.bzl @@ -144,7 +144,11 @@ def collect_args_lists(targets, label): args = tuple(args), files = depset(transitive = transitive_files), by_action = tuple([ - struct(action = k, args = v.args, files = depset(transitive = v.transitive_files)) + struct( + action = k, + args = tuple(v.args), + files = depset(transitive = v.transitive_files), + ) for k, v in by_action.items() ]), ) diff --git a/tests/rule_based_toolchain/features/BUILD b/tests/rule_based_toolchain/features/BUILD new file mode 100644 index 0000000..0d9f5cd --- /dev/null +++ b/tests/rule_based_toolchain/features/BUILD @@ -0,0 +1,55 @@ +load("@rules_testing//lib:util.bzl", "util") +load("//cc/toolchains:args.bzl", "cc_args") +load("//cc/toolchains:feature.bzl", "cc_feature") +load("//tests/rule_based_toolchain:analysis_test_suite.bzl", "analysis_test_suite") +load(":features_test.bzl", "TARGETS", "TESTS") + +util.helper_target( + cc_args, + name = "c_compile", + actions = ["//tests/rule_based_toolchain/actions:c_compile"], + additional_files = ["//tests/rule_based_toolchain/testdata:file1"], + args = ["c"], +) + +util.helper_target( + cc_feature, + name = "simple", + args = [":c_compile"], + enabled = False, + feature_name = "feature_name", + visibility = ["//tests/rule_based_toolchain:__subpackages__"], +) + +util.helper_target( + cc_feature, + name = "requires", + args = [":c_compile"], + enabled = True, + feature_name = "requires", + requires_any_of = [":simple"], +) + +util.helper_target( + cc_feature, + name = "implies", + args = [":c_compile"], + enabled = True, + feature_name = "implies", + implies = [":simple"], +) + +util.helper_target( + cc_feature, + name = "mutual_exclusion_feature", + args = [":c_compile"], + enabled = True, + feature_name = "mutual_exclusion", + mutually_exclusive = [":simple"], +) + +analysis_test_suite( + name = "test_suite", + targets = TARGETS, + tests = TESTS, +) diff --git a/tests/rule_based_toolchain/features/features_test.bzl b/tests/rule_based_toolchain/features/features_test.bzl new file mode 100644 index 0000000..75ae7ee --- /dev/null +++ b/tests/rule_based_toolchain/features/features_test.bzl @@ -0,0 +1,80 @@ +# Copyright 2024 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. +"""Tests for actions for the rule based toolchain.""" + +load( + "//cc/toolchains:cc_toolchain_info.bzl", + "ArgsInfo", + "FeatureInfo", + "MutuallyExclusiveCategoryInfo", +) + +visibility("private") + +_C_COMPILE_FILE = "tests/rule_based_toolchain/testdata/file1" + +def _simple_feature_test(env, targets): + simple = env.expect.that_target(targets.simple).provider(FeatureInfo) + simple.name().equals("feature_name") + simple.args().args().contains_exactly([targets.c_compile.label]) + simple.enabled().equals(False) + simple.overrides().is_none() + simple.overridable().equals(False) + + simple.args().files().contains_exactly([_C_COMPILE_FILE]) + c_compile_action = simple.args().by_action().get( + targets.c_compile[ArgsInfo].actions.to_list()[0], + ) + c_compile_action.files().contains_exactly([_C_COMPILE_FILE]) + c_compile_action.args().contains_exactly([targets.c_compile[ArgsInfo]]) + +def _feature_collects_requirements_test(env, targets): + env.expect.that_target(targets.requires).provider( + FeatureInfo, + ).requires_any_of().contains_exactly([ + targets.simple.label, + ]) + +def _feature_collects_implies_test(env, targets): + env.expect.that_target(targets.implies).provider( + FeatureInfo, + ).implies().contains_exactly([ + targets.simple.label, + ]) + +def _feature_collects_mutual_exclusion_test(env, targets): + env.expect.that_target(targets.simple).provider( + MutuallyExclusiveCategoryInfo, + ).name().equals("feature_name") + env.expect.that_target(targets.mutual_exclusion_feature).provider( + FeatureInfo, + ).mutually_exclusive().contains_exactly([ + targets.simple.label, + ]) + +TARGETS = [ + ":c_compile", + ":simple", + ":requires", + ":implies", + ":mutual_exclusion_feature", +] + +# @unsorted-dict-items +TESTS = { + "simple_feature_test": _simple_feature_test, + "feature_collects_requirements_test": _feature_collects_requirements_test, + "feature_collects_implies_test": _feature_collects_implies_test, + "feature_collects_mutual_exclusion_test": _feature_collects_mutual_exclusion_test, +} diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl index 5fcb8a1..23f5ec7 100644 --- a/tests/rule_based_toolchain/subjects.bzl +++ b/tests/rule_based_toolchain/subjects.bzl @@ -71,7 +71,8 @@ _FEATURE_FLAGS = dict( implies = None, requires_any_of = None, mutually_exclusive = ProviderSequence(_MutuallyExclusiveCategoryFactory), - known = _subjects.bool, + overridable = _subjects.bool, + external = _subjects.bool, overrides = None, ) @@ -146,7 +147,7 @@ _FeatureFactory = generate_factory( args = _ArgsListFactory.factory, implies = ProviderDepset(_FakeFeatureFactory), requires_any_of = ProviderSequence(_FeatureSetFactory), - overrides = optional_subject(_FakeFeatureFactory), + overrides = optional_subject(_FakeFeatureFactory.factory), ), )