Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the swift_module_mapping_test rule #1340

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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