Skip to content

Fix dup modmaps taking into account macro test targets. #8524

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

Merged
merged 3 commits into from
Apr 30, 2025
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
47 changes: 0 additions & 47 deletions Sources/Basics/Graph/GraphAlgorithms.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,50 +131,3 @@ public func depthFirstSearch<T: Hashable>(
}
}
}

/// Implements a pre-order depth-first search that traverses the whole graph and
/// doesn't distinguish between unique and duplicate nodes. The visitor can abort
/// a path as needed to prune the tree.
/// The method expects the graph to be acyclic but doesn't check that.
///
/// - Parameters:
/// - nodes: The list of input nodes to sort.
/// - successors: A closure for fetching the successors of a particular node.
/// - onNext: A callback to indicate the node currently being processed
/// including its parent (if any) and its depth. Returns whether to
/// continue down the current path.
///
/// - Complexity: O(v + e) where (v, e) are the number of vertices and edges
/// reachable from the input nodes via the relation.
public enum DepthFirstContinue {
case `continue`
case abort
}

public func depthFirstSearch<T: Hashable>(
_ nodes: [T],
successors: (T) throws -> [T],
visitNext: (T, _ parent: T?) throws -> DepthFirstContinue
) rethrows {
var stack = OrderedSet<TraversalNode<T>>()

for node in nodes {
precondition(stack.isEmpty)
stack.append(TraversalNode(parent: nil, curr: node))

while !stack.isEmpty {
let node = stack.removeLast()

if try visitNext(node.curr, node.parent) == .continue {
for succ in try successors(node.curr) {
stack.append(
TraversalNode(
parent: node.curr,
curr: succ
)
)
}
}
}
}
}
14 changes: 1 addition & 13 deletions Sources/Build/BuildDescription/ModuleBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,30 +187,18 @@ extension ModuleBuildDescription {
var dependencies: [Dependency] = []
plan.traverseDependencies(of: self) { product, _, description in
dependencies.append(.product(product, description))
return .continue
} onModule: { module, _, description in
dependencies.append(.module(module, description))
return .continue
}
return dependencies
}

package func recursiveLinkDependencies(using plan: BuildPlan) -> [Dependency] {
var dependencies: [Dependency] = []
plan.traverseDependencies(of: self) { product, _, description in
guard product.type != .macro && product.type != .plugin else {
return .abort
}

plan.traverseLinkDependencies(of: self) { product, _, description in
dependencies.append(.product(product, description))
return .continue
} onModule: { module, _, description in
guard module.type != .macro && module.type != .plugin else {
return .abort
}

dependencies.append(.module(module, description))
return .continue
}
return dependencies
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Build/BuildPlan/BuildPlan+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension BuildPlan {
func plan(swiftTarget: SwiftModuleBuildDescription) throws {
// We need to iterate recursive dependencies because Swift compiler needs to see all the targets a target
// builds against
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 {
Expand Down
88 changes: 82 additions & 6 deletions Sources/Build/BuildPlan/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1154,8 +1154,8 @@ extension BuildPlan {

package func traverseDependencies(
of description: ModuleBuildDescription,
onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> DepthFirstContinue,
onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> DepthFirstContinue
onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> Void,
onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> Void
) {
var visited = Set<TraversalNode>()
func successors(
Expand Down Expand Up @@ -1196,16 +1196,92 @@ extension BuildPlan {
case .package:
[]
}
} visitNext: { module, _ in
} onNext: { module, _ in
switch module {
case .package:
return .continue
break

case .product(let product, let destination):
onProduct(product, destination, self.description(for: product, context: destination))

case .module(let module, let destination):
onModule(module, destination, self.description(for: module, context: destination))
}
}
}

// Only follow link time dependencies, i.e. skip dependencies on macros and plugins
// except for testTargets that depend on macros.
package func traverseLinkDependencies(
of description: ModuleBuildDescription,
onProduct: (ResolvedProduct, BuildParameters.Destination, ProductBuildDescription?) -> Void,
onModule: (ResolvedModule, BuildParameters.Destination, ModuleBuildDescription?) -> Void
) {
var visited = Set<TraversalNode>()
func successors(
for product: ResolvedProduct,
destination: Destination
) -> [TraversalNode] {
product.modules.map { module in
TraversalNode(module: module, context: destination)
}.filter {
visited.insert($0).inserted
}
}

func successors(
for parentModule: ResolvedModule,
destination: Destination
) -> [TraversalNode] {
parentModule
.dependencies(satisfying: description.buildParameters.buildEnvironment)
.reduce(into: [TraversalNode]()) { partial, dependency in
switch dependency {
case .product(let product, _):
guard product.type != .plugin else {
return
}

guard product.type != .macro || parentModule.type == .test else {
return
}

partial.append(.init(product: product, context: destination))
case .module(let childModule, _):
guard childModule.type != .plugin else {
return
}

guard childModule.type != .macro || parentModule.type == .test else {
return
}

partial.append(.init(module: childModule, context: destination))
}
}.filter {
visited.insert($0).inserted
}
}

depthFirstSearch(successors(for: description.module, destination: description.destination)) {
switch $0 {
case .module(let module, let destination):
successors(for: module, destination: destination)
case .product(let product, let destination):
successors(for: product, destination: destination)
case .package:
[]
}
} onNext: { module, _ in
switch module {
case .package:
break

case .product(let product, let destination):
return onProduct(product, destination, self.description(for: product, context: destination))
onProduct(product, destination, self.description(for: product, context: destination))

case .module(let module, let destination):
return onModule(module, destination, self.description(for: module, context: destination))
onModule(module, destination, self.description(for: module, context: destination))
}
}
}
Expand Down
25 changes: 21 additions & 4 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6918,14 +6918,16 @@ class BuildPlanTestCase: BuildSystemProviderTestCase {
"/LeakTest/Sources/CLib/Clib.c",
"/LeakTest/Sources/MyMacro/MyMacro.swift",
"/LeakTest/Sources/MyPluginTool/MyPluginTool.swift",
"/LeakTest/Plugins/MyPlugin/MyPlugin.swift",
"/LeakTest/Sources/MyLib/MyLib.swift",
"/LeakTest/Plugins/MyPlugin/MyPlugin.swift",
"/LeakTest/Tests/MyMacroTests/MyMacroTests.swift",
"/LeakTest/Tests/MyMacro2Tests/MyMacro2Tests.swift",
"/LeakLib/Sources/CLib2/include/Clib.h",
"/LeakLib/Sources/CLib2/Clib.c",
"/LeakLib/Sources/MyMacro2/MyMacro.swift",
"/LeakLib/Sources/MyPluginTool2/MyPluginTool.swift",
"/LeakLib/Sources/MyLib2/MyLib.swift",
"/LeakLib/Plugins/MyPlugin2/MyPlugin.swift",
"/LeakLib/Sources/MyLib2/MyLib.swift"
])

let graph = try loadModulesGraph(fileSystem: fs, manifests: [
Expand All @@ -6934,6 +6936,7 @@ class BuildPlanTestCase: BuildSystemProviderTestCase {
path: "/LeakLib",
products: [
ProductDescription(name: "MyLib2", type: .library(.automatic), targets: ["MyLib2"]),
ProductDescription(name: "MyMacros2", type: .macro, targets: ["MyMacro2"])
],
targets: [
TargetDescription(name: "CLib2"),
Expand All @@ -6959,6 +6962,11 @@ class BuildPlanTestCase: BuildSystemProviderTestCase {
dependencies: ["CLib", "MyMacro", .product(name: "MyLib2", package: "LeakLib")],
pluginUsages: [.plugin(name: "MyPlugin", package: nil)]
),
TargetDescription(name: "MyMacroTests", dependencies: ["MyMacro"], type: .test),
TargetDescription(
name: "MyMacro2Tests",
dependencies: [.product(name: "MyMacros2", package: "LeakLib")],
type: .test),
]
)
], observabilityScope: observability.topScope)
Expand All @@ -6972,8 +6980,17 @@ class BuildPlanTestCase: BuildSystemProviderTestCase {
XCTAssertNoDiagnostics(observability.diagnostics)

let myLib = try XCTUnwrap(plan.targets.first(where: { $0.module.name == "MyLib" })).swift()
print(myLib.additionalFlags)
XCTAssertFalse(myLib.additionalFlags.contains(where: { $0.contains("-tool/include")}), "flags shouldn't contain tools items")
XCTAssertFalse(myLib.additionalFlags.contains(where: { $0.contains("-tool")}), "flags shouldn't contain tools items")

// Make sure the tests do have the include path and the module map from the lib
let myMacroTests = try XCTUnwrap(plan.targets.first(where: { $0.module.name == "MyMacroTests" })).swift()
let flags = myMacroTests.additionalFlags.joined(separator: " ")
XCTAssertMatch(flags, .regex("CLib[/\\\\]include"))
XCTAssertMatch(flags, .regex("CLib-tool.build[/\\\\]module.modulemap"))
let myMacro2Tests = try XCTUnwrap(plan.targets.first(where: { $0.module.name == "MyMacro2Tests" })).swift()
let flags2 = myMacro2Tests.additionalFlags.joined(separator: " ")
XCTAssertMatch(flags2, .regex("CLib2[/\\\\]include"))
XCTAssertMatch(flags2, .regex("CLib2-tool.build[/\\\\]module.modulemap"))
}
}

Expand Down
2 changes: 0 additions & 2 deletions Tests/BuildTests/BuildPlanTraversalTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,8 @@ final class BuildPlanTraversalTests: XCTestCase {
XCTAssertEqual(product.name, "SwiftSyntax")
XCTAssertEqual(destination, .host)
XCTAssertNil(description)
return .continue
} onModule: { module, destination, description in
moduleDependencies.append((module, destination, description))
return .continue
}

XCTAssertEqual(moduleDependencies.count, 2)
Expand Down