diff --git a/doc/BUILD b/doc/BUILD
index 1895bfd3c..109ced893 100644
--- a/doc/BUILD
+++ b/doc/BUILD
@@ -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",
diff --git a/doc/doc.bzl b/doc/doc.bzl
index 2625ddb4a..3e6d17595 100644
--- a/doc/doc.bzl
+++ b/doc/doc.bzl
@@ -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",
@@ -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
diff --git a/doc/rules.md b/doc/rules.md
index ec9e4d091..59ed5e9d7 100644
--- a/doc/rules.md
+++ b/doc/rules.md
@@ -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)
@@ -470,6 +472,105 @@ symbol is defined; it is not repeated by the alias module.)
| module_name | The name of the Swift module being built.
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 | `""` |
+
+
+## swift_module_mapping
+
+
+swift_module_mapping(name, aliases) ++ +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 | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| aliases | A dictionary that remaps the names of Swift modules.
+swift_module_mapping_test(name, deps, exclude, mapping) ++ +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 | +| :------------- | :------------- | :------------- | :------------- | :------------- | +| name | A unique name for this target. | Name | required | | +| deps | A list of Swift targets whose transitive closure will be validated against the `swift_module_mapping` target specified by `mapping`. | List of labels | required | | +| 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 | `[]` | +| mapping | The label of a `swift_module_mapping` target against which the transitive closure of `deps` will be validated. | Label | required | | + + ## swift_package_configuration diff --git a/swift/BUILD b/swift/BUILD index 31d5e82e6..0cf87f644 100644 --- a/swift/BUILD +++ b/swift/BUILD @@ -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"], @@ -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", diff --git a/swift/swift_module_mapping_test.bzl b/swift/swift_module_mapping_test.bzl new file mode 100644 index 000000000..2503f7ac0 --- /dev/null +++ b/swift/swift_module_mapping_test.bzl @@ -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, +) diff --git a/test/fixtures/module_mapping/BUILD b/test/fixtures/module_mapping/BUILD index 92dde5178..d3fac63c1 100644 --- a/test/fixtures/module_mapping/BUILD +++ b/test/fixtures/module_mapping/BUILD @@ -1,5 +1,6 @@ load("//swift:swift_library.bzl", "swift_library") load("//swift:swift_module_mapping.bzl", "swift_module_mapping") +load("//swift:swift_module_mapping_test.bzl", "swift_module_mapping_test") load( "//test/fixtures:common.bzl", "FIXTURE_TAGS", @@ -36,6 +37,7 @@ swift_module_mapping( aliases = { "Common": "MySDKInternal_Common", }, + tags = FIXTURE_TAGS, ) # This is the target that will be tested in `module_mapping.bzl`, to force the @@ -43,5 +45,48 @@ swift_module_mapping( apply_mapping( name = "MySDK_with_mapping", mapping = ":MySDK_module_mapping", + tags = FIXTURE_TAGS, target = ":MySDK", ) + +swift_library( + name = "ExistingLibrary", + srcs = ["Empty.swift"], + module_name = "ExistingLibrary", + tags = FIXTURE_TAGS, + deps = [":NewDependency"], +) + +swift_library( + name = "NewDependency", + srcs = ["Empty.swift"], + module_name = "NewDependency", + tags = FIXTURE_TAGS, +) + +swift_module_mapping( + name = "ExistingLibrary_module_mapping_incomplete", + aliases = { + "ExistingLibrary": "MySDKInternal_ExistingLibrary", + }, + tags = FIXTURE_TAGS, +) + +swift_module_mapping( + name = "ExistingLibrary_module_mapping_complete", + aliases = { + "ExistingLibrary": "MySDKInternal_ExistingLibrary", + "NewDependency": "MySDKInternal_NewDependency", + }, + tags = FIXTURE_TAGS, +) + +# We can't write a test that verifies that this *test fails at execution time*. +# It's been marked manual so we can run it directly to verify its behavior. +# Other tests that do work automatically are in `module_mapping_tests.bzl`. +swift_module_mapping_test( + name = "ExistingLibrary_module_mapping_incomplete_test", + mapping = ":ExistingLibrary_module_mapping_incomplete", + tags = FIXTURE_TAGS, + deps = [":ExistingLibrary"], +) diff --git a/test/fixtures/module_mapping/Empty.swift b/test/fixtures/module_mapping/Empty.swift new file mode 100644 index 000000000..db385ddda --- /dev/null +++ b/test/fixtures/module_mapping/Empty.swift @@ -0,0 +1,15 @@ +// 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. + +// Intentionally empty. diff --git a/test/module_mapping_tests.bzl b/test/module_mapping_tests.bzl index 7f6f16c53..1ccc2026b 100644 --- a/test/module_mapping_tests.bzl +++ b/test/module_mapping_tests.bzl @@ -14,6 +14,10 @@ """Tests for Swift module aliases using the `:module_mapping` flag.""" +load( + "@build_bazel_rules_swift//swift:swift_module_mapping_test.bzl", + "swift_module_mapping_test", +) load( "@bazel_skylib//rules:build_test.bzl", "build_test", @@ -38,6 +42,29 @@ def module_mapping_test_suite(name, tags = []): tags = all_tags, ) + # Verify that a `swift_module_mapping_test` with a complete mapping + # succeeds. + swift_module_mapping_test( + name = "{}_module_mapping_test_succeeds_with_complete_mapping".format(name), + mapping = "@build_bazel_rules_swift//test/fixtures/module_mapping:ExistingLibrary_module_mapping_complete", + tags = all_tags, + deps = [ + "@build_bazel_rules_swift//test/fixtures/module_mapping:ExistingLibrary", + ], + ) + + # Verify that a `swift_module_mapping_test` with an incomplete mapping + # succeeds if the missing modules are listed in `exclude`. + swift_module_mapping_test( + name = "{}_module_mapping_test_succeeds_with_exclusions".format(name), + exclude = ["NewDependency"], + mapping = "@build_bazel_rules_swift//test/fixtures/module_mapping:ExistingLibrary_module_mapping_incomplete", + tags = all_tags, + deps = [ + "@build_bazel_rules_swift//test/fixtures/module_mapping:ExistingLibrary", + ], + ) + native.test_suite( name = name, tags = all_tags,