Skip to content

[6.1] Fix duplicate modulemap errors with macro and plugin deps (#8472) #8491

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

Open
wants to merge 6 commits into
base: release/6.1
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions Sources/Build/BuildDescription/ModuleBuildDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,14 @@ extension ModuleBuildDescription {
}
return dependencies
}

package func recursiveLinkDependencies(using plan: BuildPlan) -> [Dependency] {
var dependencies: [Dependency] = []
plan.traverseLinkDependencies(of: self) { product, _, description in
dependencies.append(.product(product, description))
} onModule: { module, _, description in
dependencies.append(.module(module, description))
}
return dependencies
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,12 @@ extension SwiftModuleBuildDescription {
ModuleBuildDescription.swift(self).dependencies(using: plan)
}

package func recursiveLinkDependencies(
using plan: BuildPlan
) -> [ModuleBuildDescription.Dependency] {
ModuleBuildDescription.swift(self).recursiveLinkDependencies(using: plan)
}

package func recursiveDependencies(
using plan: BuildPlan
) -> [ModuleBuildDescription.Dependency] {
Expand Down
5 changes: 2 additions & 3 deletions Sources/Build/BuildPlan/BuildPlan+Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import class PackageModel.SystemLibraryModule
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) {
// builds against
for case .module(let dependency, let description) in swiftTarget.recursiveLinkDependencies(using: self) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the change on main calls swiftTarget.recursiveDependencies. Is it intentional to call this recursiveLinkDependencies?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a new change on main to fix to call LinkDeps. This isn't a cherry pick any more, I am fixing both at the same time.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for case .module(let dependency, let description) in swiftTarget.recursiveLinkDependencies(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 Expand Up @@ -53,5 +53,4 @@ extension BuildPlan {
}
}
}

}
76 changes: 76 additions & 0 deletions Sources/Build/BuildPlan/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,82 @@ extension BuildPlan {
}
}

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):
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):
Expand Down
82 changes: 82 additions & 0 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6842,4 +6842,86 @@ final class BuildPlanTests: XCTestCase {
XCTAssertMatch(contents, .regex(#"args: \[.*"-I","/testpackagedep/SomeArtifact.xcframework/macos/Headers".*,"/testpackage/Sources/CLib/lib.c".*]"#))
XCTAssertMatch(contents, .regex(#"args: \[.*"-module-name","SwiftLib",.*"-I","/testpackagedep/SomeArtifact.xcframework/macos/Headers".*]"#))
}

func testMacroPluginDependencyLeakage() async throws {
// Make sure the include paths from macro and plugin executables don't leak into dependents
let observability = ObservabilitySystem.makeForTesting()
let fs = InMemoryFileSystem(emptyFiles: [
"/LeakTest/Sources/CLib/include/Clib.h",
"/LeakTest/Sources/CLib/Clib.c",
"/LeakTest/Sources/MyMacro/MyMacro.swift",
"/LeakTest/Sources/MyPluginTool/MyPluginTool.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",
])

let graph = try loadModulesGraph(fileSystem: fs, manifests: [
Manifest.createFileSystemManifest(
displayName: "LeakLib",
path: "/LeakLib",
products: [
ProductDescription(name: "MyLib2", type: .library(.automatic), targets: ["MyLib2"]),
ProductDescription(name: "MyMacros2", type: .macro, targets: ["MyMacro2"]),
],
targets: [
TargetDescription(name: "CLib2"),
TargetDescription(name: "MyMacro2", dependencies: ["CLib2"], type: .macro),
TargetDescription(name: "MyPluginTool2", dependencies: ["CLib2"], type: .executable),
TargetDescription(name: "MyPlugin2", dependencies: ["MyPluginTool2"], type: .plugin, pluginCapability: .buildTool),
TargetDescription(name: "MyLib2", dependencies: ["CLib2", "MyMacro2"], pluginUsages: [.plugin(name: "MyPlugin2", package: nil)]),
]
),
Manifest.createRootManifest(
displayName: "LeakTest",
path: "/LeakTest",
dependencies: [
.fileSystem(path: "/LeakLib")
],
targets: [
TargetDescription(name: "CLib"),
TargetDescription(name: "MyMacro", dependencies: ["CLib"], type: .macro),
TargetDescription(name: "MyPluginTool", dependencies: ["CLib"], type: .executable),
TargetDescription(name: "MyPlugin", dependencies: ["MyPluginTool"], type: .plugin, pluginCapability: .buildTool),
TargetDescription(
name: "MyLib",
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)
XCTAssertNoDiagnostics(observability.diagnostics)

let plan = try await mockBuildPlan(
graph: graph,
fileSystem: fs,
observabilityScope: observability.topScope
)
XCTAssertNoDiagnostics(observability.diagnostics)

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

@bkhouri bkhouri Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: the change on branch main has $0.contains("-tool/include"). Is it expected that release/6.1 omit the /include portion of the flag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.


// 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()
XCTAssertTrue(myMacroTests.additionalFlags.contains(where: { $0.contains("CLib/include") }))
XCTAssertTrue(myMacroTests.additionalFlags.contains(where: { $0.contains("CLib-tool.build/module.modulemap") }))
let myMacro2Tests = try XCTUnwrap(plan.targets.first(where: { $0.module.name == "MyMacro2Tests" })).swift()
XCTAssertTrue(myMacro2Tests.additionalFlags.contains(where: { $0.contains("CLib2/include") }))
XCTAssertTrue(myMacro2Tests.additionalFlags.contains(where: { $0.contains("CLib2-tool.build/module.modulemap") }))
}
}