Skip to content

Commit ed30d34

Browse files
committed
Fix duplicate modulemap errors with macro and plugin deps (swiftlang#8472)
We were including flags to hook up modulemaps and include files to C library dependencies in macros and plugin tools through to the modules that depend on those. This adds the capability to prune the depth first searches through the dependencies to ensure these are skipped when crossing macro and plugin boundaries.
1 parent 01e7c31 commit ed30d34

File tree

7 files changed

+150
-8
lines changed

7 files changed

+150
-8
lines changed

Sources/Basics/Graph/GraphAlgorithms.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,50 @@ public func depthFirstSearch<T: Hashable>(
131131
}
132132
}
133133
}
134+
135+
/// Implements a pre-order depth-first search that traverses the whole graph and
136+
/// doesn't distinguish between unique and duplicate nodes. The visitor can abort
137+
/// a path as needed to prune the tree.
138+
/// The method expects the graph to be acyclic but doesn't check that.
139+
///
140+
/// - Parameters:
141+
/// - nodes: The list of input nodes to sort.
142+
/// - successors: A closure for fetching the successors of a particular node.
143+
/// - onNext: A callback to indicate the node currently being processed
144+
/// including its parent (if any) and its depth. Returns whether to
145+
/// continue down the current path.
146+
///
147+
/// - Complexity: O(v + e) where (v, e) are the number of vertices and edges
148+
/// reachable from the input nodes via the relation.
149+
public enum DepthFirstContinue {
150+
case `continue`
151+
case abort
152+
}
153+
154+
public func depthFirstSearch<T: Hashable>(
155+
_ nodes: [T],
156+
successors: (T) throws -> [T],
157+
visitNext: (T, _ parent: T?) throws -> DepthFirstContinue
158+
) rethrows {
159+
var stack = OrderedSet<TraversalNode<T>>()
160+
161+
for node in nodes {
162+
precondition(stack.isEmpty)
163+
stack.append(TraversalNode(parent: nil, curr: node))
164+
165+
while !stack.isEmpty {
166+
let node = stack.removeLast()
167+
168+
if try visitNext(node.curr, node.parent) == .continue {
169+
for succ in try successors(node.curr) {
170+
stack.append(
171+
TraversalNode(
172+
parent: node.curr,
173+
curr: succ
174+
)
175+
)
176+
}
177+
}
178+
}
179+
}
180+
}

Sources/Build/BuildDescription/ModuleBuildDescription.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,30 @@ extension ModuleBuildDescription {
187187
var dependencies: [Dependency] = []
188188
plan.traverseDependencies(of: self) { product, _, description in
189189
dependencies.append(.product(product, description))
190+
return .continue
190191
} onModule: { module, _, description in
191192
dependencies.append(.module(module, description))
193+
return .continue
194+
}
195+
return dependencies
196+
}
197+
198+
package func recursiveLinkDependencies(using plan: BuildPlan) -> [Dependency] {
199+
var dependencies: [Dependency] = []
200+
plan.traverseDependencies(of: self) { product, _, description in
201+
guard product.type != .macro && product.type != .plugin else {
202+
return .abort
203+
}
204+
205+
dependencies.append(.product(product, description))
206+
return .continue
207+
} onModule: { module, _, description in
208+
guard module.type != .macro && module.type != .plugin else {
209+
return .abort
210+
}
211+
212+
dependencies.append(.module(module, description))
213+
return .continue
192214
}
193215
return dependencies
194216
}

Sources/Build/BuildDescription/SwiftModuleBuildDescription.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,12 @@ extension SwiftModuleBuildDescription {
10421042
ModuleBuildDescription.swift(self).dependencies(using: plan)
10431043
}
10441044

1045+
package func recursiveLinkDependencies(
1046+
using plan: BuildPlan
1047+
) -> [ModuleBuildDescription.Dependency] {
1048+
ModuleBuildDescription.swift(self).recursiveLinkDependencies(using: plan)
1049+
}
1050+
10451051
package func recursiveDependencies(
10461052
using plan: BuildPlan
10471053
) -> [ModuleBuildDescription.Dependency] {

Sources/Build/BuildPlan/BuildPlan+Swift.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import class PackageModel.SystemLibraryModule
1919
extension BuildPlan {
2020
func plan(swiftTarget: SwiftModuleBuildDescription) throws {
2121
// We need to iterate recursive dependencies because Swift compiler needs to see all the targets a target
22-
// depends on.
22+
// builds against
2323
for case .module(let dependency, let description) in swiftTarget.recursiveDependencies(using: self) {
2424
switch dependency.underlying {
2525
case let underlyingTarget as ClangModule where underlyingTarget.type == .library:
@@ -53,5 +53,4 @@ extension BuildPlan {
5353
}
5454
}
5555
}
56-
5756
}

Sources/Build/BuildPlan/BuildPlan.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,8 +1153,8 @@ extension BuildPlan {
11531153

11541154
package func traverseDependencies(
11551155
of description: ModuleBuildDescription,
1156-
onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> Void,
1157-
onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> Void
1156+
onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> DepthFirstContinue,
1157+
onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> DepthFirstContinue
11581158
) {
11591159
var visited = Set<TraversalNode>()
11601160
func successors(
@@ -1195,16 +1195,16 @@ extension BuildPlan {
11951195
case .package:
11961196
[]
11971197
}
1198-
} onNext: { module, _ in
1198+
} visitNext: { module, _ in
11991199
switch module {
12001200
case .package:
1201-
break
1201+
return .continue
12021202

12031203
case .product(let product, let destination):
1204-
onProduct(product, destination, self.description(for: product, context: destination))
1204+
return onProduct(product, destination, self.description(for: product, context: destination))
12051205

12061206
case .module(let module, let destination):
1207-
onModule(module, destination, self.description(for: module, context: destination))
1207+
return onModule(module, destination, self.description(for: module, context: destination))
12081208
}
12091209
}
12101210
}

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6842,4 +6842,70 @@ final class BuildPlanTests: XCTestCase {
68426842
XCTAssertMatch(contents, .regex(#"args: \[.*"-I","/testpackagedep/SomeArtifact.xcframework/macos/Headers".*,"/testpackage/Sources/CLib/lib.c".*]"#))
68436843
XCTAssertMatch(contents, .regex(#"args: \[.*"-module-name","SwiftLib",.*"-I","/testpackagedep/SomeArtifact.xcframework/macos/Headers".*]"#))
68446844
}
6845+
6846+
func testMacroPluginDependencyLeakage() async throws {
6847+
// Make sure the include paths from macro and plugin executables don't leak into dependents
6848+
let observability = ObservabilitySystem.makeForTesting()
6849+
let fs = InMemoryFileSystem(emptyFiles: [
6850+
"/LeakTest/Sources/CLib/include/Clib.h",
6851+
"/LeakTest/Sources/CLib/Clib.c",
6852+
"/LeakTest/Sources/MyMacro/MyMacro.swift",
6853+
"/LeakTest/Sources/MyPluginTool/MyPluginTool.swift",
6854+
"/LeakTest/Plugins/MyPlugin/MyPlugin.swift",
6855+
"/LeakTest/Sources/MyLib/MyLib.swift",
6856+
"/LeakLib/Sources/CLib2/include/Clib.h",
6857+
"/LeakLib/Sources/CLib2/Clib.c",
6858+
"/LeakLib/Sources/MyMacro2/MyMacro.swift",
6859+
"/LeakLib/Sources/MyPluginTool2/MyPluginTool.swift",
6860+
"/LeakLib/Plugins/MyPlugin2/MyPlugin.swift",
6861+
"/LeakLib/Sources/MyLib2/MyLib.swift"
6862+
])
6863+
6864+
let graph = try loadModulesGraph(fileSystem: fs, manifests: [
6865+
Manifest.createFileSystemManifest(
6866+
displayName: "LeakLib",
6867+
path: "/LeakLib",
6868+
products: [
6869+
ProductDescription(name: "MyLib2", type: .library(.automatic), targets: ["MyLib2"]),
6870+
],
6871+
targets: [
6872+
TargetDescription(name: "CLib2"),
6873+
TargetDescription(name: "MyMacro2", dependencies: ["CLib2"], type: .macro),
6874+
TargetDescription(name: "MyPluginTool2", dependencies: ["CLib2"], type: .executable),
6875+
TargetDescription(name: "MyPlugin2", dependencies: ["MyPluginTool2"], type: .plugin, pluginCapability: .buildTool),
6876+
TargetDescription(name: "MyLib2", dependencies: ["CLib2", "MyMacro2"], pluginUsages: [.plugin(name: "MyPlugin2", package: nil)]),
6877+
]
6878+
),
6879+
Manifest.createRootManifest(
6880+
displayName: "LeakTest",
6881+
path: "/LeakTest",
6882+
dependencies: [
6883+
.fileSystem(path: "/LeakLib")
6884+
],
6885+
targets: [
6886+
TargetDescription(name: "CLib"),
6887+
TargetDescription(name: "MyMacro", dependencies: ["CLib"], type: .macro),
6888+
TargetDescription(name: "MyPluginTool", dependencies: ["CLib"], type: .executable),
6889+
TargetDescription(name: "MyPlugin", dependencies: ["MyPluginTool"], type: .plugin, pluginCapability: .buildTool),
6890+
TargetDescription(
6891+
name: "MyLib",
6892+
dependencies: ["CLib", "MyMacro", .product(name: "MyLib2", package: "LeakLib")],
6893+
pluginUsages: [.plugin(name: "MyPlugin", package: nil)]
6894+
),
6895+
]
6896+
)
6897+
], observabilityScope: observability.topScope)
6898+
XCTAssertNoDiagnostics(observability.diagnostics)
6899+
6900+
let plan = try await mockBuildPlan(
6901+
graph: graph,
6902+
fileSystem: fs,
6903+
observabilityScope: observability.topScope
6904+
)
6905+
XCTAssertNoDiagnostics(observability.diagnostics)
6906+
6907+
let myLib = try XCTUnwrap(plan.targets.first(where: { $0.module.name == "MyLib" })).swift()
6908+
print(myLib.additionalFlags)
6909+
XCTAssertFalse(myLib.additionalFlags.contains(where: { $0.contains("-tool/include")}), "flags shouldn't contain tools items")
6910+
}
68456911
}

Tests/BuildTests/BuildPlanTraversalTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,10 @@ final class BuildPlanTraversalTests: XCTestCase {
146146
XCTAssertEqual(product.name, "SwiftSyntax")
147147
XCTAssertEqual(destination, .host)
148148
XCTAssertNil(description)
149+
return .continue
149150
} onModule: { module, destination, description in
150151
moduleDependencies.append((module, destination, description))
152+
return .continue
151153
}
152154

153155
XCTAssertEqual(moduleDependencies.count, 2)

0 commit comments

Comments
 (0)