Skip to content

Commit

Permalink
Add the swift_feature_allowlist rule that lets toolchains control w…
Browse files Browse the repository at this point in the history
…hich packages are allowed to enable/disable specific features.

PiperOrigin-RevId: 375484553
  • Loading branch information
allevato authored and swiple-rules-gardener committed May 24, 2021
1 parent 05f18b8 commit 407b1c8
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 3 deletions.
1 change: 1 addition & 0 deletions swift/internal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ bzl_library(
srcs = [
"swift_binary_test.bzl",
"swift_c_module.bzl",
"swift_feature_allowlist.bzl",
"swift_grpc_library.bzl",
"swift_import.bzl",
"swift_library.bzl",
Expand Down
102 changes: 102 additions & 0 deletions swift/internal/features.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ def configure_features(
passed to other `swift_common` functions. Note that the structure of
this value should otherwise not be relied on or inspected directly.
"""
if swift_toolchain.feature_allowlists:
_check_allowlists(
allowlists = swift_toolchain.feature_allowlists,
label = ctx.label,
requested_features = requested_features,
unsupported_features = unsupported_features,
)

# The features to enable for a particular rule/target are the ones requested
# by the toolchain, plus the ones requested by the target itself, *minus*
Expand Down Expand Up @@ -168,3 +175,98 @@ def is_feature_enabled(feature_configuration, feature_name):
),
feature_name = feature_name,
)

def _check_allowlists(
*,
allowlists,
label,
requested_features,
unsupported_features):
"""Checks the toolchain's allowlists to verify the requested features.
If any of the features requested to be enabled or disabled is not allowed in
the target's package by one of the allowlists, the build will fail with an
error message indicating the feature and the allowlist that denied it.
Args:
allowlists: A list of `SwiftFeatureAllowlistInfo` providers that will be
checked.
label: The label of the target being checked against the allowlist.
requested_features: The list of features to be enabled. This is
typically obtained using the `ctx.features` field in a rule
implementation function.
unsupported_features: The list of features that are unsupported by the
current rule. This is typically obtained using the
`ctx.disabled_features` field in a rule implementation function.
"""
features_to_check = list(requested_features)
features_to_check.extend(
["-{}".format(feature) for feature in unsupported_features],
)

for allowlist in allowlists:
for feature_string in features_to_check:
if not _is_feature_allowed_in_package(
allowlist = allowlist,
feature = feature_string,
package = label.package,
workspace_name = label.workspace_name,
):
fail((
"Feature '{feature}' is not allowed to be set by the " +
"target '{target}'; see the allowlist at '{allowlist}' " +
"for more information."
).format(
allowlist = allowlist.allowlist_label,
feature = feature_string,
target = str(label),
))

def _is_feature_allowed_in_package(
allowlist,
feature,
package,
workspace_name = None):
"""Returns a value indicating whether a feature is allowed in a package.
Args:
allowlist: The `SwiftFeatureAllowlistInfo` provider that contains the
allowlist.
feature: The name of the feature (or its negation) being checked.
package: The package part of the label being checked for access (e.g.,
the value of `ctx.label.package`).
workspace_name: The workspace name part of the label being checked for
access (e.g., the value of `ctx.label.workspace_name`).
Returns:
True if the feature is allowed to be used in the package, or False if it
is not.
"""

# Any feature not managed by the allowlist is allowed by default.
if feature not in allowlist.managed_features:
return True

if workspace_name:
package_spec = "@{}//{}".format(workspace_name, package)
else:
package_spec = "//{}".format(package)

is_allowed = False
for package_info in allowlist.packages:
if package_info.match_subpackages:
is_match = (
package_spec == package_info.package or
package_spec.startswith(package_info.package + "/")
)
else:
is_match = package_spec == package_info.package

# Package exclusions always take precedence over package inclusions, so
# if we have an exclusion match, return false immediately.
if package_info.excluded and is_match:
return False
else:
is_allowed = True

return is_allowed
45 changes: 45 additions & 0 deletions swift/internal/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,47 @@

"""Defines Starlark providers that propagated by the Swift BUILD rules."""

SwiftAllowlistPackageInfo = provider(
doc = "Describes a package match in an allowlist.",
fields = {
"excluded": """\
A Boolean value indicating whether the packages described by this value are
exclusions rather than inclusions.
""",
"match_subpackages": """\
A Boolean value indicating whether subpackages of `package` should also be
matched.
""",
"package": """\
A string indicating the name of the package to match, in the form
`//path/to/package`, or `@repository//path/to/package` if an explicit repository
name was given.
""",
},
)

SwiftFeatureAllowlistInfo = provider(
doc = """\
Describes a set of features and the packages that are allowed to request or
disable them.
""",
fields = {
"allowlist_label": """\
A string containing the label of the `swift_feature_allowlist` target that
created this provider.
""",
"managed_features": """\
A list of strings representing feature names or their negations that packages in
the `packages` list are allowed to explicitly request or disable.
""",
"packages": """\
A list of `SwiftAllowlistPackageInfo` values describing packages (possibly
recursive) whose targets are allowed to request or disable a feature managed by
this allowlist.
""",
},
)

SwiftInfo = provider(
doc = """\
Contains information about the compiled artifacts of a Swift module.
Expand Down Expand Up @@ -72,6 +113,10 @@ Swift toolchain depends on.
""",
"cpu": """\
`String`. The CPU architecture that the toolchain is targeting.
""",
"feature_allowlists": """\
A list of `SwiftFeatureAllowlistInfo` providers that allow or prohibit packages
from requesting or disabling features.
""",
"generated_header_module_implicit_deps_providers": """\
A `struct` with the following fields, which are providers from targets that
Expand Down
109 changes: 109 additions & 0 deletions swift/internal/swift_feature_allowlist.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Copyright 2021 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.

"""Support for restricting access to features based on an allowlist."""

load(":providers.bzl", "SwiftAllowlistPackageInfo", "SwiftFeatureAllowlistInfo")

def _parse_allowlist_package(package_spec):
"""Parses an allowlist package specification from a string.
Args:
package_spec: A string that represents a possibly recursive package
specification, with an optional exclusion marker in front.
Returns:
An instance of `SwiftAllowlistPackageInfo` containing the parsed
information from the package specification.
"""
if package_spec.startswith("-"):
excluded = True
package_spec = package_spec[1:]
else:
excluded = False

if package_spec.endswith("/..."):
match_subpackages = True
package_spec = package_spec[:-4]
else:
match_subpackages = False

return SwiftAllowlistPackageInfo(
excluded = excluded,
match_subpackages = match_subpackages,
package = package_spec,
)

def _swift_feature_allowlist_impl(ctx):
return [SwiftFeatureAllowlistInfo(
allowlist_label = str(ctx.label),
managed_features = ctx.attr.managed_features,
packages = [
_parse_allowlist_package(package_spec)
for package_spec in ctx.attr.packages
],
)]

swift_feature_allowlist = rule(
attrs = {
"managed_features": attr.string_list(
allow_empty = True,
doc = """\
A list of feature strings that are permitted to be specified by the targets in
the packages matched by the `packages` attribute. This list may include both
feature names and/or negations (a name with a leading `-`); a regular feature
name means that the targets in the matching packages may explicitly request that
the feature be enabled, and a negated feature means that the target may
explicitly request that the feature be disabled.
For example, `managed_features = ["foo", "-bar"]` means that targets in the
allowlist's packages may request that feature `"foo"` be enabled and that
feature `"bar"` be disabled.
""",
mandatory = False,
),
"packages": attr.string_list(
allow_empty = True,
doc = """\
A list of strings representing packages (possibly recursive) whose targets are
allowed to enable/disable the features in `managed_features`. Each package
pattern is written in the syntax used by the `package_group` function:
* `//foo/bar`: Targets in the package `//foo/bar` but not in subpackages.
* `//foo/bar/...`: Targets in the package `//foo/bar` and any of its
subpackages.
* A leading `-` excludes packages that would otherwise have been included by
the patterns in the list.
Exclusions always take priority over inclusions; order in the list is
irrelevant.
""",
mandatory = True,
),
},
doc = """\
Limits the ability to request or disable certain features to a set of packages
(and possibly subpackages) in the workspace.
A Swift toolchain target can reference any number (zero or more) of
`swift_feature_allowlist` targets. The features managed by these allowlists may
overlap. For some package _P_, a feature is allowed to be used by targets in
that package if _P_ matches the `packages` patterns in *all* of the allowlists
that manage that feature.
A feature that is not managed by any allowlist is allowed to be used by any
package.
""",
implementation = _swift_feature_allowlist_impl,
)
13 changes: 12 additions & 1 deletion swift/internal/swift_toolchain.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ load(
"SWIFT_FEATURE_USE_RESPONSE_FILES",
)
load(":features.bzl", "features_for_build_modes")
load(":providers.bzl", "SwiftToolchainInfo")
load(":providers.bzl", "SwiftFeatureAllowlistInfo", "SwiftToolchainInfo")
load(":toolchain_config.bzl", "swift_toolchain_config")
load(
":utils.bzl",
Expand Down Expand Up @@ -213,6 +213,10 @@ def _swift_toolchain_impl(ctx):
all_files = depset(all_files),
cc_toolchain_info = cc_toolchain,
cpu = ctx.attr.arch,
feature_allowlists = [
target[SwiftFeatureAllowlistInfo]
for target in ctx.attr.feature_allowlists
],
generated_header_module_implicit_deps_providers = (
collect_implicit_deps_providers([])
),
Expand Down Expand Up @@ -250,6 +254,13 @@ architecture-specific content, such as "x86_64" in "lib/swift/linux/x86_64".
""",
mandatory = True,
),
"feature_allowlists": attr.label_list(
doc = """\
A list of `swift_feature_allowlist` targets that allow or prohibit packages from
requesting or disabling features.
""",
providers = [[SwiftFeatureAllowlistInfo]],
),
"os": attr.string(
doc = """\
The name of the operating system that this toolchain targets.
Expand Down
18 changes: 17 additions & 1 deletion swift/internal/xcode_swift_toolchain.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ load(
)
load(":features.bzl", "features_for_build_modes")
load(":toolchain_config.bzl", "swift_toolchain_config")
load(":providers.bzl", "SwiftInfo", "SwiftToolchainInfo")
load(
":providers.bzl",
"SwiftFeatureAllowlistInfo",
"SwiftInfo",
"SwiftToolchainInfo",
)
load(
":utils.bzl",
"collect_implicit_deps_providers",
Expand Down Expand Up @@ -726,6 +731,10 @@ def _xcode_swift_toolchain_impl(ctx):
all_files = depset(all_files),
cc_toolchain_info = cc_toolchain,
cpu = cpu,
feature_allowlists = [
target[SwiftFeatureAllowlistInfo]
for target in ctx.attr.feature_allowlists
],
generated_header_module_implicit_deps_providers = (
collect_implicit_deps_providers(
ctx.attr.generated_header_module_implicit_deps,
Expand Down Expand Up @@ -755,6 +764,13 @@ xcode_swift_toolchain = rule(
attrs = dicts.add(
swift_toolchain_driver_attrs(),
{
"feature_allowlists": attr.label_list(
doc = """\
A list of `swift_feature_allowlist` targets that allow or prohibit packages from
requesting or disabling features.
""",
providers = [[SwiftFeatureAllowlistInfo]],
),
"generated_header_module_implicit_deps": attr.label_list(
doc = """\
Targets whose `SwiftInfo` providers should be treated as compile-time inputs to
Expand Down
7 changes: 6 additions & 1 deletion swift/swift.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ load(
"@build_bazel_rules_swift//swift/internal:swift_common.bzl",
_swift_common = "swift_common",
)
load(
"@build_bazel_rules_swift//swift/internal:swift_feature_allowlist.bzl",
_swift_feature_allowlist = "swift_feature_allowlist",
)
load(
"@build_bazel_rules_swift//swift/internal:swift_grpc_library.bzl",
_swift_grpc_library = "swift_grpc_library",
Expand Down Expand Up @@ -79,12 +83,13 @@ swift_common = _swift_common
# Re-export rules.
swift_binary = _swift_binary
swift_c_module = _swift_c_module
swift_feature_allowlist = _swift_feature_allowlist
swift_grpc_library = _swift_grpc_library
swift_import = _swift_import
swift_library = _swift_library
swift_test = _swift_test
swift_module_alias = _swift_module_alias
swift_proto_library = _swift_proto_library
swift_test = _swift_test

# Re-export public aspects.
swift_clang_module_aspect = _swift_clang_module_aspect
Expand Down

1 comment on commit 407b1c8

@keith
Copy link
Member

@keith keith commented on 407b1c8 May 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.