diff --git a/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Package.swift b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Package.swift new file mode 100644 index 00000000000..f7fb3f07c60 --- /dev/null +++ b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Package.swift @@ -0,0 +1,32 @@ +// swift-tools-version:999.0 +import PackageDescription + +let package = Package( + name: "AppPkg", + platforms: [ + .macOS(.v10_12), + .iOS(.v10), + .tvOS(.v11), + .watchOS(.v5) + ], + dependencies: [ + .package(url: "../GamePkg", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + "Utils", + .product(name: "Utils", + package: "GamePkg", + moduleAliases: ["Utils": "GameUtils"]) + ], + path: "./Sources/App"), + .target( + name: "Utils", + dependencies: [], + path: "./Sources/Utils" + ) + ] + ) + diff --git a/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Sources/App/main.swift b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Sources/App/main.swift new file mode 100644 index 00000000000..e0a183ceb47 --- /dev/null +++ b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Sources/App/main.swift @@ -0,0 +1,17 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Utils +import GameUtils + +Utils.echoModule() + +let level = LevelDetector.detect(for: "TestUser") +print(level) diff --git a/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Sources/Utils/FileUtils.swift b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Sources/Utils/FileUtils.swift new file mode 100644 index 00000000000..7dfbf02100c --- /dev/null +++ b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/AppPkg/Sources/Utils/FileUtils.swift @@ -0,0 +1,13 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +public func echoModule() { + print("Utils") +} diff --git a/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Package.swift b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Package.swift new file mode 100644 index 00000000000..84f60b6bf45 --- /dev/null +++ b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Package.swift @@ -0,0 +1,14 @@ +// swift-tools-version:5.5 +import PackageDescription + +let package = Package( + name: "GamePkg", + products: [ + .library(name: "Game", targets: ["Game"]), + .library(name: "Utils", targets: ["Utils"]), + ], + targets: [ + .target(name: "Game", dependencies: ["Utils"]), + .target(name: "Utils", dependencies: []), + ] +) diff --git a/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Sources/Game/FileGame.swift b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Sources/Game/FileGame.swift new file mode 100644 index 00000000000..88741bb6adc --- /dev/null +++ b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Sources/Game/FileGame.swift @@ -0,0 +1,22 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Utils + +public struct Game: Equatable { + public var levels: [LevelDetector] + + public static func startGame(for user: String) -> Int { + if user.isEmpty { + return -1 + } + return LevelDetector.detect(for: user) + } +} diff --git a/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Sources/Utils/FileUtils.swift b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Sources/Utils/FileUtils.swift new file mode 100644 index 00000000000..a8e3a9ae04d --- /dev/null +++ b/Fixtures/Miscellaneous/ModuleAliasing/DirectDeps/GamePkg/Sources/Utils/FileUtils.swift @@ -0,0 +1,15 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +public struct LevelDetector: Equatable { + public static func detect(for user: String) -> Int { + return user.count + } +} diff --git a/Sources/Build/LLBuildManifestBuilder.swift b/Sources/Build/LLBuildManifestBuilder.swift index cb6f2714135..03912909d8e 100644 --- a/Sources/Build/LLBuildManifestBuilder.swift +++ b/Sources/Build/LLBuildManifestBuilder.swift @@ -514,6 +514,7 @@ extension LLBuildManifestBuilder { outputs: cmdOutputs, executable: buildParameters.toolchain.swiftCompiler, moduleName: target.target.c99name, + moduleAliases: target.target.moduleAliases, moduleOutputPath: target.moduleOutputPath, importPath: buildParameters.buildPath, tempsPath: target.tempsPath, diff --git a/Sources/LLBuildManifest/BuildManifest.swift b/Sources/LLBuildManifest/BuildManifest.swift index 610ad1e8d6a..e5d5a72e807 100644 --- a/Sources/LLBuildManifest/BuildManifest.swift +++ b/Sources/LLBuildManifest/BuildManifest.swift @@ -163,6 +163,7 @@ public struct BuildManifest { outputs: [Node], executable: AbsolutePath, moduleName: String, + moduleAliases: [String: String]?, moduleOutputPath: AbsolutePath, importPath: AbsolutePath, tempsPath: AbsolutePath, @@ -178,6 +179,7 @@ public struct BuildManifest { outputs: outputs, executable: executable, moduleName: moduleName, + moduleAliases: moduleAliases, moduleOutputPath: moduleOutputPath, importPath: importPath, tempsPath: tempsPath, diff --git a/Sources/LLBuildManifest/Tools.swift b/Sources/LLBuildManifest/Tools.swift index 9e24ec0ad86..90e117cadb2 100644 --- a/Sources/LLBuildManifest/Tools.swift +++ b/Sources/LLBuildManifest/Tools.swift @@ -221,6 +221,7 @@ public struct SwiftCompilerTool: ToolProtocol { public var executable: AbsolutePath public var moduleName: String + public var moduleAliases: [String: String]? public var moduleOutputPath: AbsolutePath public var importPath: AbsolutePath public var tempsPath: AbsolutePath @@ -235,6 +236,7 @@ public struct SwiftCompilerTool: ToolProtocol { outputs: [Node], executable: AbsolutePath, moduleName: String, + moduleAliases: [String: String]?, moduleOutputPath: AbsolutePath, importPath: AbsolutePath, tempsPath: AbsolutePath, @@ -248,6 +250,7 @@ public struct SwiftCompilerTool: ToolProtocol { self.outputs = outputs self.executable = executable self.moduleName = moduleName + self.moduleAliases = moduleAliases self.moduleOutputPath = moduleOutputPath self.importPath = importPath self.tempsPath = tempsPath @@ -261,6 +264,9 @@ public struct SwiftCompilerTool: ToolProtocol { public func write(to stream: ManifestToolStream) { stream["executable"] = executable stream["module-name"] = moduleName + if let moduleAliases = moduleAliases { + stream["module-alias"] = moduleAliases + } stream["module-output-path"] = moduleOutputPath stream["import-paths"] = [importPath] stream["temps-path"] = tempsPath diff --git a/Sources/PackageDescription/PackageDependency.swift b/Sources/PackageDescription/PackageDependency.swift index 8238716a658..346c11f08c3 100644 --- a/Sources/PackageDescription/PackageDependency.swift +++ b/Sources/PackageDescription/PackageDependency.swift @@ -65,6 +65,11 @@ extension Package { } } + /// Module aliases for targets in this dependency. The key is an original target name and + /// the value is a new unique name mapped to the name of the .swiftmodule binary. + @available(_PackageDescription, introduced: 999.0) + public var moduleAliases: [String: String]? + /// The requirement of the dependency. @available(_PackageDescription, deprecated: 5.6, message: "use kind instead") public var requirement: Requirement { diff --git a/Sources/PackageDescription/PackageDescriptionSerialization.swift b/Sources/PackageDescription/PackageDescriptionSerialization.swift index 5bde9fece68..9988430a0a2 100644 --- a/Sources/PackageDescription/PackageDescriptionSerialization.swift +++ b/Sources/PackageDescription/PackageDescriptionSerialization.swift @@ -292,6 +292,7 @@ extension Target.Dependency: Encodable { case type case name case package + case moduleAliases case condition } @@ -308,10 +309,11 @@ extension Target.Dependency: Encodable { try container.encode(Kind.target, forKey: .type) try container.encode(name, forKey: .name) try container.encode(condition, forKey: .condition) - case .productItem(let name, let package, let condition): + case .productItem(let name, let package, let moduleAliases, let condition): try container.encode(Kind.product, forKey: .type) try container.encode(name, forKey: .name) try container.encode(package, forKey: .package) + try container.encode(moduleAliases, forKey: .moduleAliases) try container.encode(condition, forKey: .condition) case .byNameItem(let name, let condition): try container.encode(Kind.byName, forKey: .type) diff --git a/Sources/PackageDescription/Target.swift b/Sources/PackageDescription/Target.swift index 60b413df9a4..8bfe8c559bc 100644 --- a/Sources/PackageDescription/Target.swift +++ b/Sources/PackageDescription/Target.swift @@ -37,7 +37,7 @@ public final class Target { /// The different types of a target's dependency on another entity. public enum Dependency { case targetItem(name: String, condition: TargetDependencyCondition?) - case productItem(name: String, package: String?, condition: TargetDependencyCondition?) + case productItem(name: String, package: String?, moduleAliases: [String: String]?, condition: TargetDependencyCondition?) case byNameItem(name: String, condition: TargetDependencyCondition?) } @@ -936,9 +936,20 @@ extension Target.Dependency { /// - package: The name of the package. @available(_PackageDescription, obsoleted: 5.2, message: "the 'package' argument is mandatory as of tools version 5.2") public static func product(name: String, package: String? = nil) -> Target.Dependency { - return .productItem(name: name, package: package, condition: nil) + return .productItem(name: name, package: package, moduleAliases: nil, condition: nil) } - + + /// Creates a dependency on a product from a dependent package. + /// + /// - parameters: + /// - name: The name of the product. + /// - moduleAliases: The module aliases for targets in the product. + /// - package: The name of the package. + @available(_PackageDescription, introduced: 999.0) + public static func product(name: String, package: String? = nil, moduleAliases: [String: String]? = nil) -> Target.Dependency { + return .productItem(name: name, package: package, moduleAliases: moduleAliases, condition: nil) + } + /// Creates a dependency that resolves to either a target or a product with the specified name. /// /// - parameters: @@ -960,7 +971,7 @@ extension Target.Dependency { name: String, package: String ) -> Target.Dependency { - return .productItem(name: name, package: package, condition: nil) + return .productItem(name: name, package: package, moduleAliases: nil, condition: nil) } /// Creates a dependency on a target in the same package. @@ -981,15 +992,33 @@ extension Target.Dependency { /// - package: The name of the package. /// - condition: A condition that limits the application of the target dependency. For example, only apply a /// dependency for a specific platform. - @available(_PackageDescription, introduced: 5.3) + @available(_PackageDescription, introduced: 5.3, obsoleted: 999.0) public static func product( name: String, package: String, condition: TargetDependencyCondition? = nil ) -> Target.Dependency { - return .productItem(name: name, package: package, condition: condition) + return .productItem(name: name, package: package, moduleAliases: nil, condition: condition) } - + + /// Creates a target dependency on a product from a package dependency. + /// + /// - parameters: + /// - name: The name of the product. + /// - package: The name of the package. + /// - moduleAliases: The module aliases for targets in the product. + /// - condition: A condition that limits the application of the target dependency. For example, only apply a + /// dependency for a specific platform. + @available(_PackageDescription, introduced: 999.0) + public static func product( + name: String, + package: String, + moduleAliases: [String: String]? = nil, + condition: TargetDependencyCondition? = nil + ) -> Target.Dependency { + return .productItem(name: name, package: package, moduleAliases: moduleAliases, condition: condition) + } + /// Creates a by-name dependency that resolves to either a target or a product but after the Swift Package Manager /// has loaded the package graph. /// diff --git a/Sources/PackageGraph/PackageGraph+Loading.swift b/Sources/PackageGraph/PackageGraph+Loading.swift index d45aace1cb4..01de2e1c424 100644 --- a/Sources/PackageGraph/PackageGraph+Loading.swift +++ b/Sources/PackageGraph/PackageGraph+Loading.swift @@ -197,6 +197,32 @@ private func checkAllDependenciesAreUsed(_ rootPackages: [ResolvedPackage], obse } } +extension Package { + // Add module aliases specified for applicable targets + fileprivate func setModuleAliasesForTargets(with moduleAliasMap: [String: String]) { + // Set module aliases for each target's dependencies + for (entryName, entryAlias) in moduleAliasMap { + for target in self.targets { + // First add dependency module aliases for this target + if entryName != target.name { + target.addModuleAlias(for: entryName, as: entryAlias) + } + } + } + + // This loop should run after the loop above as it may rename the target + // as an alias if specified + for (entryName, entryAlias) in moduleAliasMap { + for target in self.targets { + // Then set this target to be aliased if specified + if entryName == target.name { + target.addModuleAlias(for: target.name, as: entryAlias) + } + } + } + } +} + fileprivate extension ResolvedProduct { /// Returns true if and only if the product represents a command plugin target. var isCommandPlugin: Bool { @@ -239,6 +265,9 @@ private func createResolvedPackages( return ($0.package.identity, $0) } + // Gather all module aliases specified for dependencies from each package + let pkgToAliasesMap = gatherModuleAliases(from: packageBuilders) + // Scan and validate the dependencies for packageBuilder in packageBuilders { let package = packageBuilder.package @@ -247,6 +276,10 @@ private func createResolvedPackages( description: "Validating package dependencies", metadata: package.diagnosticsMetadata ) + + if let aliasMap = pkgToAliasesMap[package.manifest.displayName] { + package.setModuleAliasesForTargets(with: aliasMap) + } var dependencies = OrderedCollections.OrderedDictionary() var dependenciesByNameForTargetDependencyResolution = [String: ResolvedPackageBuilder]() @@ -490,6 +523,36 @@ private func createResolvedPackages( return try packageBuilders.map{ try $0.construct() } } +// Create a map between a package and module aliases specified for its targets +private func gatherModuleAliases(from packageBuilders: [ResolvedPackageBuilder]) -> [String: [String: String]] { + // Use a list to track all the module aliases specified for a given target to handle an override later + var nameToAliasAndPkgMap = [String: [(String, String)]]() + packageBuilders.forEach { $0.package.targets.forEach { $0.dependencies.forEach { dep in + if case let .product(prodRef, _) = dep { + if let prodPkg = prodRef.package { + if let prodModuleAliases = prodRef.moduleAliases { + for (depName, depAlias) in prodModuleAliases { + // Add a module alias to a map if specified + nameToAliasAndPkgMap[depName, default: []].append((depAlias, prodPkg)) + } + } + } + } + }}} + + var pkgToAliasesMap = [String: [String: String]]() + nameToAliasAndPkgMap.forEach { (keyName, aliasAndPkgList) in + aliasAndPkgList.forEach { (elemAlias, elemPkg) in + // FIXME: handle an override for multiple aliases + // Use one of the module aliases specified for now + if pkgToAliasesMap[elemPkg]?[keyName] == nil, !elemAlias.isEmpty { + pkgToAliasesMap[elemPkg] = [keyName: elemAlias] + } + } + } + return pkgToAliasesMap +} + /// A generic builder for `Resolved` models. private class ResolvedBuilder { /// The constructed object, available after the first call to `construct()`. diff --git a/Sources/PackageGraph/ResolvedTarget.swift b/Sources/PackageGraph/ResolvedTarget.swift index 1215a8a7874..6166c0d18f2 100644 --- a/Sources/PackageGraph/ResolvedTarget.swift +++ b/Sources/PackageGraph/ResolvedTarget.swift @@ -110,6 +110,13 @@ public final class ResolvedTarget { return underlyingTarget.c99name } + /// Module aliases for dependencies of this target. The key is an + /// original target name and the value is a new unique name mapped + /// to the name of its .swiftmodule binary. + public var moduleAliases: [String: String]? { + return underlyingTarget.moduleAliases + } + /// The "type" of target. public var type: Target.Kind { return underlyingTarget.type diff --git a/Sources/PackageLoading/ManifestJSONParser.swift b/Sources/PackageLoading/ManifestJSONParser.swift index 5a311f4c3e0..6b4d84b3c5a 100644 --- a/Sources/PackageLoading/ManifestJSONParser.swift +++ b/Sources/PackageLoading/ManifestJSONParser.swift @@ -569,7 +569,8 @@ extension TargetDescription.Dependency { case "product": let name = try json.get(String.self, forKey: "name") - self = .product(name: name, package: json.get("package"), condition: condition) + let moduleAliases: [String: String]? = try? json.get("moduleAliases") + self = .product(name: name, package: json.get("package"), moduleAliases: moduleAliases, condition: condition) case "byname": self = try .byName(name: json.get("name"), condition: condition) diff --git a/Sources/PackageLoading/ManifestLoader.swift b/Sources/PackageLoading/ManifestLoader.swift index 588d167c1bd..4f0663cbe88 100644 --- a/Sources/PackageLoading/ManifestLoader.swift +++ b/Sources/PackageLoading/ManifestLoader.swift @@ -443,7 +443,7 @@ public final class ManifestLoader: ManifestLoaderProtocol { case .target: // If this is a target dependency, we don't need to check anything. break - case .product(_, let packageName, _): + case .product(_, let packageName, _, _): if manifest.packageDependency(referencedBy: targetDependency) == nil { observabilityScope.emit(.unknownTargetPackageDependency( packageName: packageName ?? "unknown package name", diff --git a/Sources/PackageLoading/PackageBuilder.swift b/Sources/PackageLoading/PackageBuilder.swift index 1b580eb06a2..b995a2cf9ba 100644 --- a/Sources/PackageLoading/PackageBuilder.swift +++ b/Sources/PackageLoading/PackageBuilder.swift @@ -680,12 +680,11 @@ public final class PackageBuilder { guard let target = targets[name] else { return nil } return .target(target, conditions: buildConditions(from: condition)) - case .product(let name, let package, let condition): + case .product(let name, let package, let moduleAliases, let condition): return .product( - .init(name: name, package: package), + .init(name: name, package: package, moduleAliases: moduleAliases), conditions: buildConditions(from: condition) ) - case .byName(let name, let condition): // We don't create an object for targets which have no sources. if emptyModules.contains(name) { return nil } diff --git a/Sources/PackageModel/Manifest.swift b/Sources/PackageModel/Manifest.swift index d0fe36e8073..d9033ffc60f 100644 --- a/Sources/PackageModel/Manifest.swift +++ b/Sources/PackageModel/Manifest.swift @@ -298,7 +298,7 @@ public final class Manifest { let packageName: String switch targetDependency { - case .product(_, package: let name?, _), + case .product(_, package: let name?, _, _), .byName(name: let name, _): packageName = name default: @@ -340,7 +340,7 @@ public final class Manifest { switch targetDependency { case .target: break - case .product(let product, let package, _): + case .product(let product, let package, _, _): if let package = package { // ≥ 5.2 if !register( product: product, diff --git a/Sources/PackageModel/Manifest/TargetDescription.swift b/Sources/PackageModel/Manifest/TargetDescription.swift index 9320f351dfe..7f358cfea80 100644 --- a/Sources/PackageModel/Manifest/TargetDescription.swift +++ b/Sources/PackageModel/Manifest/TargetDescription.swift @@ -24,15 +24,15 @@ public struct TargetDescription: Equatable, Codable { /// Represents a target's dependency on another entity. public enum Dependency: Equatable { case target(name: String, condition: PackageConditionDescription?) - case product(name: String, package: String?, condition: PackageConditionDescription?) + case product(name: String, package: String?, moduleAliases: [String: String]? = nil, condition: PackageConditionDescription?) case byName(name: String, condition: PackageConditionDescription?) public static func target(name: String) -> Dependency { return .target(name: name, condition: nil) } - public static func product(name: String, package: String? = nil) -> Dependency { - return .product(name: name, package: package, condition: nil) + public static func product(name: String, package: String? = nil, moduleAliases: [String: String]? = nil) -> Dependency { + return .product(name: name, package: package, moduleAliases: moduleAliases, condition: nil) } } @@ -224,11 +224,12 @@ extension TargetDescription.Dependency: Codable { var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .target) try unkeyedContainer.encode(a1) try unkeyedContainer.encode(a2) - case let .product(a1, a2, a3): + case let .product(a1, a2, a3, a4): var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .product) try unkeyedContainer.encode(a1) try unkeyedContainer.encode(a2) try unkeyedContainer.encode(a3) + try unkeyedContainer.encode(a4) case let .byName(a1, a2): var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .byName) try unkeyedContainer.encode(a1) @@ -251,8 +252,9 @@ extension TargetDescription.Dependency: Codable { var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) let a1 = try unkeyedValues.decode(String.self) let a2 = try unkeyedValues.decodeIfPresent(String.self) - let a3 = try unkeyedValues.decodeIfPresent(PackageConditionDescription.self) - self = .product(name: a1, package: a2, condition: a3) + let a3 = try unkeyedValues.decode([String: String].self) + let a4 = try unkeyedValues.decodeIfPresent(PackageConditionDescription.self) + self = .product(name: a1, package: a2, moduleAliases: a3, condition: a4) case .byName: var unkeyedValues = try values.nestedUnkeyedContainer(forKey: key) let a1 = try unkeyedValues.decode(String.self) diff --git a/Sources/PackageModel/ManifestSourceGeneration.swift b/Sources/PackageModel/ManifestSourceGeneration.swift index 1c058d0afd9..09cb5cd6d7b 100644 --- a/Sources/PackageModel/ManifestSourceGeneration.swift +++ b/Sources/PackageModel/ManifestSourceGeneration.swift @@ -319,7 +319,7 @@ fileprivate extension SourceCodeFragment { } self.init(enum: "target", subnodes: params) - case .product(name: let name, package: let packageName, condition: let condition): + case .product(name: let name, package: let packageName, moduleAliases: let aliases, condition: let condition): params.append(SourceCodeFragment(key: "name", string: name)) if let packageName = packageName { params.append(SourceCodeFragment(key: "package", string: packageName)) diff --git a/Sources/PackageModel/Target.swift b/Sources/PackageModel/Target.swift index 13e67c0c05c..979bd7821fe 100644 --- a/Sources/PackageModel/Target.swift +++ b/Sources/PackageModel/Target.swift @@ -40,10 +40,16 @@ public class Target: PolymorphicCodableProtocol { /// The name of the package containing the product. public let package: String? + /// Module aliases for targets of this product dependency. The key is an + /// original target name and the value is a new unique name that also + /// becomes the name of its .swiftmodule binary. + public let moduleAliases: [String: String]? + /// Creates a product reference instance. - public init(name: String, package: String?) { + public init(name: String, package: String?, moduleAliases: [String: String]? = nil) { self.name = name self.package = package + self.moduleAliases = moduleAliases } } @@ -104,8 +110,38 @@ public class Target: PolymorphicCodableProtocol { /// /// NOTE: This name is not the language-level target (i.e., the importable /// name) name in many cases, instead use c99name if you need uniqueness. - public let name: String + public private(set) var name: String + + /// Module aliases needed to build this target. The key is an original name of a + /// dependent target and the value is a new unique name mapped to the name + /// of its .swiftmodule binary. + public private(set) var moduleAliases: [String: String]? + /// Add module aliases (if applicable) for dependencies of this target. + /// + /// For example, adding an alias `Bar` for a target name `Foo` will result in + /// compiling references to `Foo` in source code of this target as `Bar.swiftmodule`. + /// If the name argument `Foo` is the same as this target's name, this target will be + /// renamed as `Bar` and the resulting binary will be `Bar.swiftmodule`. + /// + /// - Parameters: + /// - name: The original name of a dependent target or this target + /// - alias: A new unique name mapped to the resulting binary name + public func addModuleAlias(for name: String, as alias: String) { + if moduleAliases == nil { + moduleAliases = [name: alias] + } else { + moduleAliases?[name] = alias + } + + // If the argument name is same as this target's name, this + // target should be renamed as the argument alias. + if name == self.name { + self.name = alias + self.c99name = alias.spm_mangledToC99ExtendedIdentifier() + } + } + /// The default localization for resources. public let defaultLocalization: String? @@ -113,7 +149,7 @@ public class Target: PolymorphicCodableProtocol { public let dependencies: [Dependency] /// The language-level target name. - public let c99name: String + public private(set) var c99name: String /// The bundle name, if one is being generated. public let bundleName: String? diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index 675e4581865..fb7e7bce6b0 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -1637,6 +1637,158 @@ final class BuildPlanTests: XCTestCase { } } + func testModuleAliasingDirectDeps() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/thisPkg/Sources/exe/main.swift", + "/thisPkg/Sources/Logging/file.swift", + "/fooPkg/Sources/Logging/fileLogging.swift", + "/barPkg/Sources/Logging/fileLogging.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadPackageGraph( + fs: fs, + manifests: [ + Manifest.createRootManifest( + name: "fooPkg", + path: .init("/fooPkg"), + products: [ + ProductDescription(name: "Foo", type: .library(.automatic), targets: ["Logging"]), + ], + targets: [ + TargetDescription(name: "Logging", dependencies: []), + ]), + Manifest.createRootManifest( + name: "barPkg", + path: .init("/barPkg"), + products: [ + ProductDescription(name: "Bar", type: .library(.automatic), targets: ["Logging"]), + ], + targets: [ + TargetDescription(name: "Logging", dependencies: []), + ]), + Manifest.createRootManifest( + name: "thisPkg", + path: .init("/thisPkg"), + dependencies: [ + .localSourceControl(path: .init("/fooPkg"), requirement: .upToNextMajor(from: "1.0.0")), + .localSourceControl(path: .init("/barPkg"), requirement: .upToNextMajor(from: "2.0.0")), + ], + targets: [ + TargetDescription(name: "exe", + dependencies: ["Logging", + .product(name: "Foo", + package: "fooPkg", + moduleAliases: ["Logging": "FooLogging"] + ), + .product(name: "Bar", + package: "barPkg", + moduleAliases: ["Logging": "BarLogging"] + ) + ]), + TargetDescription(name: "Logging", dependencies: []), + ]), + ], + observabilityScope: observability.topScope + ) + + XCTAssertNoDiagnostics(observability.diagnostics) + + let result = try BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(shouldLinkStaticSwiftStdlib: true), + graph: graph, + fileSystem: fs, + observabilityScope: observability.topScope + )) + + result.checkProductsCount(1) + result.checkTargetsCount(4) + + XCTAssertTrue(result.targetMap.values.contains { $0.target.name == "FooLogging" && $0.target.moduleAliases?["Logging"] == "FooLogging" }) + XCTAssertTrue(result.targetMap.values.contains { $0.target.name == "BarLogging" && $0.target.moduleAliases?["Logging"] == "BarLogging" }) + XCTAssertTrue(result.targetMap.values.contains { $0.target.name == "Logging" && $0.target.moduleAliases == nil }) + + let fooLoggingArgs = try result.target(for: "FooLogging").swiftTarget().compileArguments() + let barLoggingArgs = try result.target(for: "BarLogging").swiftTarget().compileArguments() + let loggingArgs = try result.target(for: "Logging").swiftTarget().compileArguments() + #if os(macOS) + XCTAssertMatch(fooLoggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/FooLogging.build/FooLogging-Swift.h", .anySequence]) + XCTAssertMatch(barLoggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/BarLogging.build/BarLogging-Swift.h", .anySequence]) + XCTAssertMatch(loggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Logging.build/Logging-Swift.h", .anySequence]) + #else + XCTAssertNoMatch(fooLoggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/FooLogging.build/FooLogging-Swift.h", .anySequence]) + XCTAssertNoMatch(barLoggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/BarLogging.build/BarLogging-Swift.h", .anySequence]) + XCTAssertNoMatch(loggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Logging.build/Logging-Swift.h", .anySequence]) + #endif + } + + func testModuleAliasingDuplicateTargetNameInUpstream() throws { + let fs = InMemoryFileSystem(emptyFiles: + "/thisPkg/Sources/exe/main.swift", + "/thisPkg/Sources/Logging/file.swift", + "/otherPkg/Sources/Utils/fileUtils.swift", + "/otherPkg/Sources/Logging/fileLogging.swift" + ) + + let observability = ObservabilitySystem.makeForTesting() + let graph = try loadPackageGraph( + fs: fs, + manifests: [ + Manifest.createRootManifest( + name: "otherPkg", + path: .init("/otherPkg"), + products: [ + ProductDescription(name: "Utils", type: .library(.automatic), targets: ["Utils"]), + ], + targets: [ + TargetDescription(name: "Utils", dependencies: ["Logging"]), + TargetDescription(name: "Logging", dependencies: []), + ]), + Manifest.createRootManifest( + name: "thisPkg", + path: .init("/thisPkg"), + dependencies: [ + .localSourceControl(path: .init("/otherPkg"), requirement: .upToNextMajor(from: "1.0.0")), + ], + targets: [ + TargetDescription(name: "exe", + dependencies: ["Logging", + .product(name: "Utils", + package: "otherPkg", + moduleAliases: ["Logging": "OtherLogging"])]), + TargetDescription(name: "Logging", dependencies: []), + ]), + ], + observabilityScope: observability.topScope + ) + XCTAssertNoDiagnostics(observability.diagnostics) + + let result = try BuildPlanResult(plan: try BuildPlan( + buildParameters: mockBuildParameters(shouldLinkStaticSwiftStdlib: true), + graph: graph, + fileSystem: fs, + observabilityScope: observability.topScope + )) + + result.checkProductsCount(1) + result.checkTargetsCount(4) + + XCTAssertTrue(result.targetMap.values.contains { $0.target.name == "OtherLogging" && $0.target.moduleAliases?["Logging"] == "OtherLogging" }) + XCTAssertTrue(result.targetMap.values.contains { $0.target.name == "Utils" && $0.target.moduleAliases?["Logging"] == "OtherLogging" }) + XCTAssertTrue(result.targetMap.values.contains { $0.target.name == "Logging" && $0.target.moduleAliases == nil }) + + let otherLoggingArgs = try result.target(for: "OtherLogging").swiftTarget().compileArguments() + let loggingArgs = try result.target(for: "Logging").swiftTarget().compileArguments() + + #if os(macOS) + XCTAssertMatch(otherLoggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/OtherLogging.build/OtherLogging-Swift.h", .anySequence]) + XCTAssertMatch(loggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Logging.build/Logging-Swift.h", .anySequence]) + #else + XCTAssertNoMatch(otherLoggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/OtherLogging.build/OtherLogging-Swift.h", .anySequence]) + XCTAssertNoMatch(loggingArgs, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Logging.build/Logging-Swift.h", .anySequence]) + #endif + } + func testSystemPackageBuildPlan() throws { let fs = InMemoryFileSystem(emptyFiles: "/Pkg/module.modulemap" diff --git a/Tests/FunctionalTests/ModuleAliasingTests.swift b/Tests/FunctionalTests/ModuleAliasingTests.swift new file mode 100644 index 00000000000..4ecd0f130fa --- /dev/null +++ b/Tests/FunctionalTests/ModuleAliasingTests.swift @@ -0,0 +1,37 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import Commands +import PackageModel +import SourceControl +import SPMTestSupport +import TSCBasic +import Workspace +import XCTest + +class ModuleAliasingTests: XCTestCase { + +#if swift(>=5.6) + func testExternalSimple() { + fixture(name: "Miscellaneous/ModuleAliasing/DirectDeps") { prefix in + let app = prefix.appending(components: "AppPkg") + XCTAssertBuilds(app) + XCTAssertFileExists(prefix.appending(components: "AppPkg", ".build", UserToolchain.default.triple.platformBuildPathComponent(), "debug", "App")) + XCTAssertFileExists(prefix.appending(components: "AppPkg", ".build", UserToolchain.default.triple.platformBuildPathComponent(), "release", "App")) + XCTAssertFileExists(prefix.appending(components: "AppPkg", ".build", UserToolchain.default.triple.platformBuildPathComponent(), "debug", "GameUtils.swiftmodule")) + XCTAssertFileExists(prefix.appending(components: "AppPkg", ".build", UserToolchain.default.triple.platformBuildPathComponent(), "release", "GameUtils.swiftmodule")) + XCTAssertFileExists(prefix.appending(components: "AppPkg", ".build", UserToolchain.default.triple.platformBuildPathComponent(), "debug", "Utils.swiftmodule")) + XCTAssertFileExists(prefix.appending(components: "AppPkg", ".build", UserToolchain.default.triple.platformBuildPathComponent(), "release", "Utils.swiftmodule")) + let result = try SwiftPMProduct.SwiftBuild.executeProcess([], packagePath: app) + XCTAssertEqual(result.exitStatus, .terminated(code: 0)) + } + } +#endif +}