Skip to content

Commit

Permalink
Add the swift_module_mapping_test rule
Browse files Browse the repository at this point in the history
This rule is used to validate that a `swift_module_mapping` covers the transitive closure of a set of dependencies, preventing unaliased dependencies from being added to those libraries without the target owner's knowledge.

PiperOrigin-RevId: 486711497
(cherry picked from commit 8e34b95)
Signed-off-by: Brentley Jones <github@brentleyjones.com>
  • Loading branch information
allevato authored and brentleyjones committed Oct 4, 2024
1 parent 80cac26 commit 185e769
Show file tree
Hide file tree
Showing 8 changed files with 386 additions and 0 deletions.
2 changes: 2 additions & 0 deletions doc/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ _DOC_SRCS = {
"swift_library_group",
"mixed_language_library",
"swift_module_alias",
"swift_module_mapping",
"swift_module_mapping_test",
"swift_package_configuration",
"swift_test",
"swift_proto_library",
Expand Down
10 changes: 10 additions & 0 deletions doc/doc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ load(
"//swift:swift_module_alias.bzl",
_swift_module_alias = "swift_module_alias",
)
load(
"//swift:swift_module_mapping.bzl",
_swift_module_mapping = "swift_module_mapping",
)
load(
"//swift:swift_module_mapping_test.bzl",
_swift_module_mapping_test = "swift_module_mapping_test",
)
load(
"//swift:swift_package_configuration.bzl",
_swift_package_configuration = "swift_package_configuration",
Expand Down Expand Up @@ -108,5 +116,7 @@ swift_library = _swift_library
swift_library_group = _swift_library_group
mixed_language_library = _mixed_language_library
swift_module_alias = _swift_module_alias
swift_module_mapping = _swift_module_mapping
swift_module_mapping_test = _swift_module_mapping_test
swift_package_configuration = _swift_package_configuration
swift_test = _swift_test
101 changes: 101 additions & 0 deletions doc/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ On this page:
* [swift_library_group](#swift_library_group)
* [mixed_language_library](#mixed_language_library)
* [swift_module_alias](#swift_module_alias)
* [swift_module_mapping](#swift_module_mapping)
* [swift_module_mapping_test](#swift_module_mapping_test)
* [swift_package_configuration](#swift_package_configuration)
* [swift_test](#swift_test)
* [swift_proto_library](#swift_proto_library)
Expand Down Expand Up @@ -470,6 +472,105 @@ symbol is defined; it is not repeated by the alias module.)
| <a id="swift_module_alias-module_name"></a>module_name | The name of the Swift module being built.<br><br>If left unspecified, the module name will be computed based on the target's build label, by stripping the leading `//` and replacing `/`, `:`, and other non-identifier characters with underscores. | String | optional | `""` |


<a id="swift_module_mapping"></a>

## swift_module_mapping

<pre>
swift_module_mapping(<a href="#swift_module_mapping-name">name</a>, <a href="#swift_module_mapping-aliases">aliases</a>)
</pre>

Defines a set of
[module aliases](https://github.com/apple/swift-evolution/blob/main/proposals/0339-module-aliasing-for-disambiguation.md)
that will be passed to the Swift compiler.

This rule defines a mapping from original module names to aliased names. This is
useful if you are building a library or framework for external use and want to
ensure that dependencies do not conflict with other versions of the same library
that another framework or the client may use.

To use this feature, first define a `swift_module_mapping` target that lists the
aliases you need:

```build
# //some/package/BUILD
swift_library(
name = "Utils",
srcs = [...],
module_name = "Utils",
)
swift_library(
name = "Framework",
srcs = [...],
module_name = "Framework",
deps = [":Utils"],
)
swift_module_mapping(
name = "mapping",
aliases = {
"Utils": "GameUtils",
},
)
```

Then, pass the label of that target to Bazel using the
`--@build_bazel_rules_swift//swift:module_mapping` build flag:

```shell
bazel build //some/package:Framework \
--@build_bazel_rules_swift//swift:module_mapping=//some/package:mapping
```

When `Utils` is compiled, it will be given the module name `GameUtils` instead.
Then, when `Framework` is compiled, it will import `GameUtils` anywhere that the
source asked to `import Utils`.

**ATTRIBUTES**


| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="swift_module_mapping-name"></a>name | A unique name for this target. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
| <a id="swift_module_mapping-aliases"></a>aliases | A dictionary that remaps the names of Swift modules.<br><br>Each key in the dictionary is the name of a module as it is written in source code. The corresponding value is the replacement module name to use when compiling it and/or any modules that depend on it. | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | required | |


<a id="swift_module_mapping_test"></a>

## swift_module_mapping_test

<pre>
swift_module_mapping_test(<a href="#swift_module_mapping_test-name">name</a>, <a href="#swift_module_mapping_test-deps">deps</a>, <a href="#swift_module_mapping_test-exclude">exclude</a>, <a href="#swift_module_mapping_test-mapping">mapping</a>)
</pre>

Validates that a `swift_module_mapping` target covers all the modules in the
transitive closure of a list of dependencies.

If you are building a static library or framework for external distribution and
you are using `swift_module_mapping` to rename some of the modules used by your
implementation, this rule will detect if any of your dependencies have taken on
a new dependency that you need to add to the mapping (otherwise, its symbols
would leak into your library with their original names).

When executed, this test will collect the names of all Swift modules in the
transitive closure of `deps`. System modules and modules whose names are listed
in the `exclude` attribute are omitted. Then, the test will fail if any of the
remaining modules collected are not present in the `aliases` of the
`swift_module_mapping` target specified by the `mapping` attribute.

**ATTRIBUTES**


| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="swift_module_mapping_test-name"></a>name | A unique name for this target. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
| <a id="swift_module_mapping_test-deps"></a>deps | A list of Swift targets whose transitive closure will be validated against the `swift_module_mapping` target specified by `mapping`. | <a href="https://bazel.build/concepts/labels">List of labels</a> | required | |
| <a id="swift_module_mapping_test-exclude"></a>exclude | A list of module names that may be in the transitive closure of `deps` but are not required to be covered by `mapping`. | List of strings | optional | `[]` |
| <a id="swift_module_mapping_test-mapping"></a>mapping | The label of a `swift_module_mapping` target against which the transitive closure of `deps` will be validated. | <a href="https://bazel.build/concepts/labels">Label</a> | required | |


<a id="swift_package_configuration"></a>

## swift_package_configuration
Expand Down
10 changes: 10 additions & 0 deletions swift/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ bzl_library(
],
)

bzl_library(
name = "swift_module_mapping_test",
srcs = ["swift_module_mapping_test.bzl"],
deps = [
"//swift/internal:providers",
],
)

bzl_library(
name = "swift_package_configuration",
srcs = ["swift_package_configuration.bzl"],
Expand Down Expand Up @@ -263,6 +271,8 @@ bzl_library(
":swift_library",
":swift_library_group",
":swift_module_alias",
":swift_module_mapping",
":swift_module_mapping_test",
":swift_package_configuration",
":swift_symbol_graph_aspect",
":swift_test",
Expand Down
176 changes: 176 additions & 0 deletions swift/swift_module_mapping_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# 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.

"""Implementation of the `swift_module_mapping_test` rule."""

load("@build_bazel_rules_swift//swift:providers.bzl", "SwiftInfo")
load(
"@build_bazel_rules_swift//swift/internal:providers.bzl",
"SwiftModuleAliasesInfo",
)

_SwiftModulesToValidateMappingInfo = provider(
doc = "Propagates module names to have their mapping validated.",
fields = {
"module_names": """\
A `depset` containing the names of non-system Swift modules that should be
validated against a module mapping.
""",
},
)

def _swift_module_mapping_test_module_collector_impl(target, aspect_ctx):
deps = (
getattr(aspect_ctx.rule.attr, "deps", []) +
getattr(aspect_ctx.rule.attr, "private_deps", [])
)

direct_module_names = []
transitive_module_names = [
dep[_SwiftModulesToValidateMappingInfo].module_names
for dep in deps
if _SwiftModulesToValidateMappingInfo in dep
]

if SwiftInfo in target:
for module_context in target[SwiftInfo].direct_modules:
# Ignore system modules and non-Swift modules, which aren't expected
# to be/cannot be aliased.
if module_context.is_system:
continue

swift_module = module_context.swift
if not swift_module:
continue

# Collect the original module name if it is present; otherwise,
# collect the regular module name (which is the original name when
# the mapping isn't applied). This ensures that the test isn't
# dependent on whether or not the module mapping flag is enabled.
direct_module_names.append(
swift_module.original_module_name or module_context.name,
)

return [
_SwiftModulesToValidateMappingInfo(
module_names = depset(
direct_module_names,
transitive = transitive_module_names,
),
),
]

_swift_module_mapping_test_module_collector = aspect(
attr_aspects = [
"deps",
"private_deps",
],
implementation = _swift_module_mapping_test_module_collector_impl,
provides = [_SwiftModulesToValidateMappingInfo],
)

def _swift_module_mapping_test_impl(ctx):
aliases = ctx.attr.mapping[SwiftModuleAliasesInfo].aliases
excludes = ctx.attr.exclude
unaliased_dep_modules = {}

for dep in ctx.attr.deps:
label = str(dep.label)
dep_modules = dep[_SwiftModulesToValidateMappingInfo].module_names
for module_name in dep_modules.to_list():
if module_name in excludes:
continue
if module_name in aliases:
continue

if label not in unaliased_dep_modules:
unaliased_dep_modules[label] = [module_name]
else:
unaliased_dep_modules[label].append(module_name)

test_script = """\
#!/bin/bash
set -eu
"""

if unaliased_dep_modules:
test_script += "echo 'Module mapping {} is incomplete:'\n\n".format(
ctx.attr.mapping.label,
)
for label, unaliased_names in unaliased_dep_modules.items():
test_script += "echo 'The following transitive dependencies of {} are not aliased:'\n".format(label)
for name in unaliased_names:
test_script += "echo ' {}'\n".format(name)
test_script += "echo\n\n"
test_script += "exit 1\n"
else:
test_script += "exit 0\n"

ctx.actions.write(
content = test_script,
is_executable = True,
output = ctx.outputs.executable,
)

return [DefaultInfo(executable = ctx.outputs.executable)]

swift_module_mapping_test = rule(
attrs = {
"exclude": attr.string_list(
default = [],
doc = """\
A list of module names that may be in the transitive closure of `deps` but are
not required to be covered by `mapping`.
""",
mandatory = False,
),
"mapping": attr.label(
doc = """\
The label of a `swift_module_mapping` target against which the transitive
closure of `deps` will be validated.
""",
mandatory = True,
providers = [[SwiftModuleAliasesInfo]],
),
"deps": attr.label_list(
allow_empty = False,
aspects = [_swift_module_mapping_test_module_collector],
doc = """\
A list of Swift targets whose transitive closure will be validated against the
`swift_module_mapping` target specified by `mapping`.
""",
mandatory = True,
providers = [[SwiftInfo]],
),
},
doc = """\
Validates that a `swift_module_mapping` target covers all the modules in the
transitive closure of a list of dependencies.
If you are building a static library or framework for external distribution and
you are using `swift_module_mapping` to rename some of the modules used by your
implementation, this rule will detect if any of your dependencies have taken on
a new dependency that you need to add to the mapping (otherwise, its symbols
would leak into your library with their original names).
When executed, this test will collect the names of all Swift modules in the
transitive closure of `deps`. System modules and modules whose names are listed
in the `exclude` attribute are omitted. Then, the test will fail if any of the
remaining modules collected are not present in the `aliases` of the
`swift_module_mapping` target specified by the `mapping` attribute.
""",
implementation = _swift_module_mapping_test_impl,
test = True,
)
Loading

0 comments on commit 185e769

Please sign in to comment.