BEGIN_PUBLIC
Implement cc_feature_set and cc_feature_constraint.
END_PUBLIC

PiperOrigin-RevId: 610713183
Change-Id: Ia009ac536b71cd9aa44578f823f13361c1580e37
diff --git a/cc/toolchains/feature_constraint.bzl b/cc/toolchains/feature_constraint.bzl
new file mode 100644
index 0000000..b02d420
--- /dev/null
+++ b/cc/toolchains/feature_constraint.bzl
@@ -0,0 +1,54 @@
+# 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_constraint rule."""
+
+load(
+    "//cc/toolchains/impl:collect.bzl",
+    "collect_features",
+    "collect_provider",
+)
+load("//cc/toolchains/impl:features_attr.bzl", "disallow_features_attr")
+load(
+    ":cc_toolchain_info.bzl",
+    "FeatureConstraintInfo",
+    "FeatureSetInfo",
+)
+
+def _cc_feature_constraint_impl(ctx):
+    all_of = collect_provider(ctx.attr.all_of, FeatureConstraintInfo)
+    none_of = [collect_features(ctx.attr.none_of)]
+    none_of.extend([fc.none_of for fc in all_of])
+    return [FeatureConstraintInfo(
+        label = ctx.label,
+        all_of = depset(transitive = [fc.all_of for fc in all_of]),
+        none_of = depset(transitive = none_of),
+    )]
+
+_cc_feature_constraint = rule(
+    implementation = _cc_feature_constraint_impl,
+    attrs = {
+        "all_of": attr.label_list(
+            providers = [FeatureConstraintInfo],
+        ),
+        "none_of": attr.label_list(
+            providers = [FeatureSetInfo],
+        ),
+    },
+    provides = [FeatureConstraintInfo],
+    doc = """Defines a constraint on features.
+
+Can be used with require_any_of to specify that something is only enabled when
+a constraint is met.""",
+)
+cc_feature_constraint = disallow_features_attr(_cc_feature_constraint)
diff --git a/cc/toolchains/feature_set.bzl b/cc/toolchains/feature_set.bzl
new file mode 100644
index 0000000..369de9c
--- /dev/null
+++ b/cc/toolchains/feature_set.bzl
@@ -0,0 +1,57 @@
+# 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_set rule."""
+
+load("//cc/toolchains/impl:collect.bzl", "collect_features")
+load("//cc/toolchains/impl:features_attr.bzl", "require_features_attr")
+load(
+    ":cc_toolchain_info.bzl",
+    "FeatureConstraintInfo",
+    "FeatureSetInfo",
+)
+
+def _cc_feature_set_impl(ctx):
+    features = collect_features(ctx.attr.features_)
+    return [
+        FeatureSetInfo(label = ctx.label, features = features),
+        FeatureConstraintInfo(
+            label = ctx.label,
+            all_of = features,
+            none_of = depset([]),
+        ),
+    ]
+
+_cc_feature_set = rule(
+    implementation = _cc_feature_set_impl,
+    attrs = {
+        "features_": attr.label_list(
+            providers = [FeatureSetInfo],
+            doc = "A set of features",
+        ),
+    },
+    provides = [FeatureSetInfo],
+    doc = """Defines a set of features.
+
+Example:
+
+    cc_feature_set(
+        name = "thin_lto_requirements",
+        all_of = [
+            ":thin_lto",
+            ":opt",
+        ],
+    )
+""",
+)
+cc_feature_set = require_features_attr(_cc_feature_set)
diff --git a/cc/toolchains/impl/features_attr.bzl b/cc/toolchains/impl/features_attr.bzl
new file mode 100644
index 0000000..ab8d83a
--- /dev/null
+++ b/cc/toolchains/impl/features_attr.bzl
@@ -0,0 +1,29 @@
+# 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.
+"""Helpers for dealing with the fact that features is a reserved attribute."""
+
+# buildifier: disable=unnamed-macro
+def disallow_features_attr(rule):
+    def rule_wrapper(*, name, **kwargs):
+        if "features" in kwargs:
+            fail("Cannot use features in %s" % native.package_relative_label(name))
+        rule(name = name, **kwargs)
+
+    return rule_wrapper
+
+def require_features_attr(rule):
+    def rule_wrapper(*, name, features, **kwargs):
+        rule(name = name, features_ = features, **kwargs)
+
+    return rule_wrapper
diff --git a/tests/rule_based_toolchain/features/BUILD b/tests/rule_based_toolchain/features/BUILD
index 0d9f5cd..701d478 100644
--- a/tests/rule_based_toolchain/features/BUILD
+++ b/tests/rule_based_toolchain/features/BUILD
@@ -1,6 +1,8 @@
 load("@rules_testing//lib:util.bzl", "util")
 load("//cc/toolchains:args.bzl", "cc_args")
 load("//cc/toolchains:feature.bzl", "cc_feature")
+load("//cc/toolchains:feature_constraint.bzl", "cc_feature_constraint")
+load("//cc/toolchains:feature_set.bzl", "cc_feature_set")
 load("//tests/rule_based_toolchain:analysis_test_suite.bzl", "analysis_test_suite")
 load(":features_test.bzl", "TARGETS", "TESTS")
 
@@ -23,11 +25,28 @@
 
 util.helper_target(
     cc_feature,
+    name = "simple2",
+    args = [":c_compile"],
+    enabled = False,
+    feature_name = "simple2",
+)
+
+util.helper_target(
+    cc_feature_set,
+    name = "feature_set",
+    features = [
+        ":simple",
+        ":simple2",
+    ],
+)
+
+util.helper_target(
+    cc_feature,
     name = "requires",
     args = [":c_compile"],
     enabled = True,
     feature_name = "requires",
-    requires_any_of = [":simple"],
+    requires_any_of = [":feature_set"],
 )
 
 util.helper_target(
@@ -48,6 +67,23 @@
     mutually_exclusive = [":simple"],
 )
 
+util.helper_target(
+    cc_feature_constraint,
+    name = "direct_constraint",
+    all_of = [":simple"],
+    none_of = [":simple2"],
+)
+
+util.helper_target(
+    cc_feature_constraint,
+    name = "transitive_constraint",
+    all_of = [
+        ":direct_constraint",
+        ":requires",
+    ],
+    none_of = [":implies"],
+)
+
 analysis_test_suite(
     name = "test_suite",
     targets = TARGETS,
diff --git a/tests/rule_based_toolchain/features/features_test.bzl b/tests/rule_based_toolchain/features/features_test.bzl
index 75ae7ee..b6a7db6 100644
--- a/tests/rule_based_toolchain/features/features_test.bzl
+++ b/tests/rule_based_toolchain/features/features_test.bzl
@@ -16,7 +16,9 @@
 load(
     "//cc/toolchains:cc_toolchain_info.bzl",
     "ArgsInfo",
+    "FeatureConstraintInfo",
     "FeatureInfo",
+    "FeatureSetInfo",
     "MutuallyExclusiveCategoryInfo",
 )
 
@@ -43,7 +45,7 @@
     env.expect.that_target(targets.requires).provider(
         FeatureInfo,
     ).requires_any_of().contains_exactly([
-        targets.simple.label,
+        targets.feature_set.label,
     ])
 
 def _feature_collects_implies_test(env, targets):
@@ -63,12 +65,44 @@
         targets.simple.label,
     ])
 
+def _feature_set_collects_features_test(env, targets):
+    env.expect.that_target(targets.feature_set).provider(
+        FeatureSetInfo,
+    ).features().contains_exactly([
+        targets.simple.label,
+        targets.simple2.label,
+    ])
+
+def _feature_constraint_collects_direct_features_test(env, targets):
+    constraint = env.expect.that_target(targets.direct_constraint).provider(
+        FeatureConstraintInfo,
+    )
+    constraint.all_of().contains_exactly([targets.simple.label])
+    constraint.none_of().contains_exactly([targets.simple2.label])
+
+def _feature_constraint_collects_transitive_features_test(env, targets):
+    constraint = env.expect.that_target(targets.transitive_constraint).provider(
+        FeatureConstraintInfo,
+    )
+    constraint.all_of().contains_exactly([
+        targets.simple.label,
+        targets.requires.label,
+    ])
+    constraint.none_of().contains_exactly([
+        targets.simple2.label,
+        targets.implies.label,
+    ])
+
 TARGETS = [
     ":c_compile",
     ":simple",
+    ":simple2",
+    ":feature_set",
     ":requires",
     ":implies",
     ":mutual_exclusion_feature",
+    ":direct_constraint",
+    ":transitive_constraint",
 ]
 
 # @unsorted-dict-items
@@ -77,4 +111,7 @@
     "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,
+    "feature_set_collects_features_test": _feature_set_collects_features_test,
+    "feature_constraint_collects_direct_features_test": _feature_constraint_collects_direct_features_test,
+    "feature_constraint_collects_transitive_features_test": _feature_constraint_collects_transitive_features_test,
 }
diff --git a/tests/rule_based_toolchain/subjects.bzl b/tests/rule_based_toolchain/subjects.bzl
index 23f5ec7..2525887 100644
--- a/tests/rule_based_toolchain/subjects.bzl
+++ b/tests/rule_based_toolchain/subjects.bzl
@@ -88,7 +88,7 @@
 _FeatureSetFactory = generate_factory(
     FeatureSetInfo,
     "FeatureSetInfo",
-    dict(features = _FakeFeatureFactory),
+    dict(features = ProviderDepset(_FakeFeatureFactory)),
 )
 
 # buildifier: disable=name-conventions