diff --git a/Sources/Build/BuildDescription/ModuleBuildDescription.swift b/Sources/Build/BuildDescription/ModuleBuildDescription.swift index 8f52d45f370..ad50e78a6e6 100644 --- a/Sources/Build/BuildDescription/ModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/ModuleBuildDescription.swift @@ -192,4 +192,17 @@ extension ModuleBuildDescription { } return dependencies } + + package func recursiveLinkDependencies(using plan: BuildPlan) -> [Dependency] { + var dependencies: [Dependency] = [] + plan.traverseDependencies(of: self) { + // Filter out plugin dependencies + $0.module?.type != .plugin + } onProduct: { product, _, description in + dependencies.append(.product(product, description)) + } onModule: { module, _, description in + dependencies.append(.module(module, description)) + } + return dependencies + } } diff --git a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift index de958a7d370..8f1b763f6ce 100644 --- a/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift @@ -1018,4 +1018,10 @@ extension SwiftModuleBuildDescription { ) -> [ModuleBuildDescription.Dependency] { ModuleBuildDescription.swift(self).recursiveDependencies(using: plan) } + + package func recursiveLinkDependencies( + using plan: BuildPlan + ) -> [ModuleBuildDescription.Dependency] { + ModuleBuildDescription.swift(self).recursiveLinkDependencies(using: plan) + } } diff --git a/Sources/Build/BuildPlan/BuildPlan+Swift.swift b/Sources/Build/BuildPlan/BuildPlan+Swift.swift index 7d387f8cfd5..042906479a5 100644 --- a/Sources/Build/BuildPlan/BuildPlan+Swift.swift +++ b/Sources/Build/BuildPlan/BuildPlan+Swift.swift @@ -20,12 +20,13 @@ extension BuildPlan { func plan(swiftTarget: SwiftModuleBuildDescription) throws { // We need to iterate recursive dependencies because Swift compiler needs to see all the targets a target // depends on. - for case .module(let dependency, let description) in swiftTarget.recursiveDependencies(using: self) { + for case .module(let dependency, let description) in swiftTarget.recursiveLinkDependencies(using: self) { switch dependency.underlying { case let underlyingTarget as ClangModule where underlyingTarget.type == .library: guard case let .clang(target)? = description else { throw InternalError("unexpected clang target \(underlyingTarget)") } + // Add the path to modulemap of the dependency. Currently we require that all Clang targets have a // modulemap but we may want to remove that requirement since it is valid for a target to exist without // one. However, in that case it will not be importable in Swift targets. We may want to emit a warning diff --git a/Sources/Build/BuildPlan/BuildPlan.swift b/Sources/Build/BuildPlan/BuildPlan.swift index 23b83f89465..b11e4a65a78 100644 --- a/Sources/Build/BuildPlan/BuildPlan.swift +++ b/Sources/Build/BuildPlan/BuildPlan.swift @@ -1142,6 +1142,7 @@ extension BuildPlan { package func traverseDependencies( of description: ModuleBuildDescription, + filter: (ResolvedModule.Dependency) -> Bool = { _ in true }, onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> Void, onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> Void ) { @@ -1163,6 +1164,7 @@ extension BuildPlan { ) -> [TraversalNode] { module .dependencies(satisfying: description.buildParameters.buildEnvironment) + .filter({ filter($0) }) .reduce(into: [TraversalNode]()) { partial, dependency in switch dependency { case .product(let product, _): diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index 50c19a9d32a..066e9084fb6 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -6736,4 +6736,74 @@ final class BuildPlanTests: XCTestCase { ) } } + + func testHostTargetCrossingStreams() async throws { + let fs = InMemoryFileSystem(emptyFiles: [ + "/Pkg/Plugins/CCrossingStreams/plugin.swift", + "/Pkg/Sources/CLibrary/lib.c", + "/Pkg/Sources/CLibrary/include/lib.h", + "/Pkg/Sources/Exe/main.swift", + "/Pkg/Sources/Tool/main.swift", + ]) + + let observability = ObservabilitySystem.makeForTesting() + + let graph = try loadModulesGraph( + fileSystem: fs, + manifests: [ + .createRootManifest( + displayName: "Pkg", + path: "/Pkg", + targets: [ + .init(name: "CLibrary"), + .init( + name: "Tool", + dependencies: [ + "CLibrary" + ], + type: .executable + ), + .init( + name: "CCrossingStreams", + dependencies: [ + .target(name: "Tool"), + ], + type: .plugin, + pluginCapability: .buildTool + ), + .init( + name: "Exe", + dependencies: [ + .target(name: "CLibrary"), + ], + type: .executable, + pluginUsages: [ + .plugin(name: "CCrossingStreams", package: nil), + ] + ) + ] + ) + ], + observabilityScope: observability.topScope + ) + + let result = try await BuildPlanResult(plan: mockBuildPlan( + graph: graph, + fileSystem: fs, + observabilityScope: observability.topScope + )) + + let exe = try result.moduleBuildDescription(for: "Exe").swift() + // Ensure the exe doesn't have the tool version of the CLibrary modulemap in it's flags + XCTAssertFalse(exe.additionalFlags.contains(where: { $0.hasSuffix("CLibrary-tool.build/module.modulemap")})) + XCTAssertTrue(exe.additionalFlags.contains(where: { $0.hasSuffix("CLibrary.build/module.modulemap")})) + // Also ensure the include path isn't there twice + XCTAssertEqual(exe.additionalFlags.filter({ $0 == "/Pkg/Sources/CLibrary/include" }).count, 1) + + // And make sure the plugin does get the tool version + // Note, there are two Tools modules, one for host, one for target. + let pluginTool = try XCTUnwrap(result.targetMap.first(where: { $0.module.name == "Tool" && $0.destination == .host })).swift() + XCTAssertTrue(pluginTool.additionalFlags.contains(where: { $0.hasSuffix("CLibrary-tool.build/module.modulemap") })) + XCTAssertEqual(pluginTool.additionalFlags.filter({ $0 == "/Pkg/Sources/CLibrary/include" }).count, 1) + } }