BEGIN_PUBLIC

Implement cc_feature for the rule based toolchain.
END_PUBLIC

PiperOrigin-RevId: 610712498
Change-Id: I2539825f0f4cf7f234a2310de6af0662aeb0ea2c
This commit is contained in:
Googler 2024-02-27 04:44:55 -08:00 committed by Copybara-Service
parent c5493f9b2c
commit 2b6cdcfe88
7 changed files with 389 additions and 5 deletions

View File

@ -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()
]),
),

View File

@ -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",
},
)

243
cc/toolchains/feature.bzl Normal file
View File

@ -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
)
""",
)

View File

@ -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()
]),
)

View File

@ -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,
)

View File

@ -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,
}

View File

@ -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),
),
)