Skip to content

Commit 6a50703

Browse files
committed
[Explicit Module Builds] Simplify '-explain-module-dependency' behavior and introduce '-explain-module-dependency-detailed'
The former will now simply print the first discovered path to the specified dependency module. While the latter will preserve prior behavior of finding *all possible paths* to the specified module
1 parent 598e3e2 commit 6a50703

File tree

6 files changed

+173
-69
lines changed

6 files changed

+173
-69
lines changed

Sources/SwiftDriver/Driver/Driver.swift

+36-28
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,38 @@ extension Diagnostic.Message {
14741474
}
14751475
}
14761476

1477+
extension Driver {
1478+
func explainModuleDependency(_ explainModuleName: String, allPaths: Bool) throws {
1479+
guard let dependencyPlanner = explicitDependencyBuildPlanner else {
1480+
fatalError("Cannot explain dependency without Explicit Build Planner")
1481+
}
1482+
guard let dependencyPaths = try dependencyPlanner.explainDependency(explainModuleName, allPaths: allPaths) else {
1483+
diagnosticEngine.emit(.remark("No such module dependency found: '\(explainModuleName)'"))
1484+
return
1485+
}
1486+
diagnosticEngine.emit(.remark("Module '\(moduleOutputInfo.name)' depends on '\(explainModuleName)'"))
1487+
for path in dependencyPaths {
1488+
var pathString:String = ""
1489+
for (index, moduleId) in path.enumerated() {
1490+
switch moduleId {
1491+
case .swift(let moduleName):
1492+
pathString = pathString + "[" + moduleName + "]"
1493+
case .swiftPrebuiltExternal(let moduleName):
1494+
pathString = pathString + "[" + moduleName + "]"
1495+
case .clang(let moduleName):
1496+
pathString = pathString + "[" + moduleName + "](ObjC)"
1497+
case .swiftPlaceholder(_):
1498+
fatalError("Unexpected unresolved Placeholder module")
1499+
}
1500+
if index < path.count - 1 {
1501+
pathString = pathString + " -> "
1502+
}
1503+
}
1504+
diagnosticEngine.emit(.note(pathString))
1505+
}
1506+
}
1507+
}
1508+
14771509
extension Driver {
14781510
/// Determine the driver kind based on the command-line arguments, consuming the arguments
14791511
/// conveying this information.
@@ -1520,34 +1552,10 @@ extension Driver {
15201552
}
15211553

15221554
// If we're only supposed to explain a dependency on a given module, do so now.
1523-
if let explainModuleName = parsedOptions.getLastArgument(.explainModuleDependency) {
1524-
guard let dependencyPlanner = explicitDependencyBuildPlanner else {
1525-
fatalError("Cannot explain dependency without Explicit Build Planner")
1526-
}
1527-
guard let dependencyPaths = try dependencyPlanner.explainDependency(explainModuleName.asSingle) else {
1528-
diagnosticEngine.emit(.remark("No such module dependency found: '\(explainModuleName.asSingle)'"))
1529-
return
1530-
}
1531-
diagnosticEngine.emit(.remark("Module '\(moduleOutputInfo.name)' depends on '\(explainModuleName.asSingle)'"))
1532-
for path in dependencyPaths {
1533-
var pathString:String = ""
1534-
for (index, moduleId) in path.enumerated() {
1535-
switch moduleId {
1536-
case .swift(let moduleName):
1537-
pathString = pathString + "[" + moduleName + "]"
1538-
case .swiftPrebuiltExternal(let moduleName):
1539-
pathString = pathString + "[" + moduleName + "]"
1540-
case .clang(let moduleName):
1541-
pathString = pathString + "[" + moduleName + "](ObjC)"
1542-
case .swiftPlaceholder(_):
1543-
fatalError("Unexpected unresolved Placeholder module")
1544-
}
1545-
if index < path.count - 1 {
1546-
pathString = pathString + " -> "
1547-
}
1548-
}
1549-
diagnosticEngine.emit(.note(pathString))
1550-
}
1555+
if let explainModuleName = parsedOptions.getLastArgument(.explainModuleDependencyDetailed) {
1556+
try explainModuleDependency(explainModuleName.asSingle, allPaths: true)
1557+
} else if let explainModuleNameDetailed = parsedOptions.getLastArgument(.explainModuleDependency) {
1558+
try explainModuleDependency(explainModuleNameDetailed.asSingle, allPaths: false)
15511559
}
15521560

15531561
if parsedOptions.contains(.driverPrintOutputFileMap) {

Sources/SwiftDriver/ExplicitModuleBuilds/ExplicitDependencyBuildPlanner.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -618,8 +618,19 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT
618618
}
619619

620620
internal extension ExplicitDependencyBuildPlanner {
621-
func explainDependency(_ dependencyModuleName: String) throws -> [[ModuleDependencyId]]? {
622-
return try dependencyGraph.explainDependency(dependencyModuleName: dependencyModuleName)
621+
func explainDependency(_ dependencyModuleName: String, allPaths: Bool) throws -> [[ModuleDependencyId]]? {
622+
return try dependencyGraph.explainDependency(dependencyModuleName: dependencyModuleName, allPaths: allPaths)
623+
}
624+
625+
func findPath(from source: ModuleDependencyId, to destination: ModuleDependencyId) throws -> [ModuleDependencyId]? {
626+
guard dependencyGraph.modules.contains(where: { $0.key == destination }) else { return nil }
627+
var result: [ModuleDependencyId]? = nil
628+
var visited: Set<ModuleDependencyId> = []
629+
try dependencyGraph.findAPath(source: source,
630+
pathSoFar: [source],
631+
visited: &visited,
632+
result: &result) { $0 == destination }
633+
return result
623634
}
624635
}
625636

Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift

+67-16
Original file line numberDiff line numberDiff line change
@@ -416,46 +416,97 @@ internal extension InterModuleDependencyGraph {
416416
}
417417

418418
internal extension InterModuleDependencyGraph {
419-
func explainDependency(dependencyModuleName: String) throws -> [[ModuleDependencyId]]? {
419+
func explainDependency(dependencyModuleName: String, allPaths: Bool) throws -> [[ModuleDependencyId]]? {
420420
guard modules.contains(where: { $0.key.moduleName == dependencyModuleName }) else { return nil }
421-
var results = Set<[ModuleDependencyId]>()
422-
try findAllPaths(source: .swift(mainModuleName),
423-
to: dependencyModuleName,
424-
pathSoFar: [.swift(mainModuleName)],
425-
results: &results)
426-
return results.sorted(by: { $0.count < $1.count })
421+
var result: Set<[ModuleDependencyId]> = []
422+
if allPaths {
423+
try findAllPaths(source: .swift(mainModuleName),
424+
pathSoFar: [.swift(mainModuleName)],
425+
results: &result,
426+
destinationMatch: { $0.moduleName == dependencyModuleName })
427+
} else {
428+
var visited: Set<ModuleDependencyId> = []
429+
var singlePathResult: [ModuleDependencyId]? = nil
430+
if try findAPath(source: .swift(mainModuleName),
431+
pathSoFar: [.swift(mainModuleName)],
432+
visited: &visited,
433+
result: &singlePathResult,
434+
destinationMatch: { $0.moduleName == dependencyModuleName }),
435+
let resultingPath = singlePathResult {
436+
result = [resultingPath]
437+
}
438+
}
439+
return Array(result)
427440
}
428441

442+
@discardableResult
443+
func findAPath(source: ModuleDependencyId,
444+
pathSoFar: [ModuleDependencyId],
445+
visited: inout Set<ModuleDependencyId>,
446+
result: inout [ModuleDependencyId]?,
447+
destinationMatch: (ModuleDependencyId) -> Bool) throws -> Bool {
448+
// Mark this node as visited
449+
visited.insert(source)
450+
let sourceInfo = try moduleInfo(of: source)
451+
if destinationMatch(source) {
452+
// If the source is a target Swift module, also check if it
453+
// depends on a corresponding Clang module with the same name.
454+
// If it does, add it to the path as well.
455+
var completePath = pathSoFar
456+
if let dependencies = sourceInfo.directDependencies,
457+
dependencies.contains(.clang(source.moduleName)) {
458+
completePath.append(.clang(source.moduleName))
459+
}
460+
result = completePath
461+
return true
462+
}
463+
464+
var allDependencies = sourceInfo.directDependencies ?? []
465+
if case .swift(let swiftModuleDetails) = sourceInfo.details,
466+
let overlayDependencies = swiftModuleDetails.swiftOverlayDependencies {
467+
allDependencies.append(contentsOf: overlayDependencies)
468+
}
469+
470+
for dependency in allDependencies {
471+
if try findAPath(source: dependency,
472+
pathSoFar: pathSoFar + [dependency],
473+
visited: &visited,
474+
result: &result,
475+
destinationMatch: destinationMatch) {
476+
return true
477+
}
478+
}
479+
return false
480+
}
429481

430482
private func findAllPaths(source: ModuleDependencyId,
431-
to moduleName: String,
432483
pathSoFar: [ModuleDependencyId],
433-
results: inout Set<[ModuleDependencyId]>) throws {
484+
results: inout Set<[ModuleDependencyId]>,
485+
destinationMatch: (ModuleDependencyId) -> Bool) throws {
434486
let sourceInfo = try moduleInfo(of: source)
435-
// If the source is our target, we are done
436-
if source.moduleName == moduleName {
487+
if destinationMatch(source) {
437488
// If the source is a target Swift module, also check if it
438489
// depends on a corresponding Clang module with the same name.
439490
// If it does, add it to the path as well.
440491
var completePath = pathSoFar
441492
if let dependencies = sourceInfo.directDependencies,
442-
dependencies.contains(.clang(moduleName)) {
443-
completePath.append(.clang(moduleName))
493+
dependencies.contains(.clang(source.moduleName)) {
494+
completePath.append(.clang(source.moduleName))
444495
}
445496
results.insert(completePath)
497+
return
446498
}
447499

448500
var allDependencies = sourceInfo.directDependencies ?? []
449501
if case .swift(let swiftModuleDetails) = sourceInfo.details,
450502
let overlayDependencies = swiftModuleDetails.swiftOverlayDependencies {
451503
allDependencies.append(contentsOf: overlayDependencies)
452504
}
453-
454505
for dependency in allDependencies {
455506
try findAllPaths(source: dependency,
456-
to: moduleName,
457507
pathSoFar: pathSoFar + [dependency],
458-
results: &results)
508+
results: &results,
509+
destinationMatch: destinationMatch)
459510
}
460511
}
461512
}

Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension Driver {
5151
/// If the driver is in Explicit Module Build mode, the dependency graph has been computed
5252
case computed
5353
}
54+
5455
/// Add frontend options that are common to different frontend invocations.
5556
mutating func addCommonFrontendOptions(
5657
commandLine: inout [Job.ArgTemplate],

Sources/SwiftOptions/Options.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,8 @@ extension Option {
499499
public static let experimentalSpiImports: Option = Option("-experimental-spi-imports", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable experimental support for SPI imports")
500500
public static let experimentalSpiOnlyImports: Option = Option("-experimental-spi-only-imports", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable use of @_spiOnly imports")
501501
public static let enableExperimentalSwiftBasedClosureSpecialization: Option = Option("-experimental-swift-based-closure-specialization", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Use the experimental Swift based closure-specialization optimization pass instead of the existing C++ one")
502-
public static let explainModuleDependency: Option = Option("-explain-module-dependency", .separate, attributes: [], helpText: "Emit remark/notes describing why compilation may depend on a module with a given name.")
502+
public static let explainModuleDependencyDetailed: Option = Option("-explain-module-dependency-detailed", .separate, attributes: [], helpText: "Emit remarks describing every possible dependency path that explains why compilation may depend on a module with a given name.")
503+
public static let explainModuleDependency: Option = Option("-explain-module-dependency", .separate, attributes: [], helpText: "Emit remark describing why compilation may depend on a module with a given name.")
503504
public static let explicitAutoLinking: Option = Option("-explicit-auto-linking", .flag, attributes: [], helpText: "Instead of linker-load directives, have the driver specify all link dependencies on the linker invocation. Requires '-explicit-module-build'.")
504505
public static let explicitDependencyGraphFormat: Option = Option("-explicit-dependency-graph-format=", .joined, attributes: [.helpHidden, .doesNotAffectIncrementalBuild], helpText: "Specify the explicit dependency graph output format to either 'json' or 'dot'")
505506
public static let explicitInterfaceModuleBuild: Option = Option("-explicit-interface-module-build", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Use the specified command-line to build the module from interface, instead of flags specified in the interface")
@@ -1371,6 +1372,7 @@ extension Option {
13711372
Option.experimentalSpiImports,
13721373
Option.experimentalSpiOnlyImports,
13731374
Option.enableExperimentalSwiftBasedClosureSpecialization,
1375+
Option.explainModuleDependencyDetailed,
13741376
Option.explainModuleDependency,
13751377
Option.explicitAutoLinking,
13761378
Option.explicitDependencyGraphFormat,

Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift

+53-22
Original file line numberDiff line numberDiff line change
@@ -2252,30 +2252,61 @@ final class ExplicitModuleBuildTests: XCTestCase {
22522252
try testInputsPath.appending(component: "ExplicitModuleBuilds")
22532253
.appending(component: "Swift")
22542254
let sdkArgumentsForTesting = (try? Driver.sdkArgumentsForTesting()) ?? []
2255-
var driver = try Driver(args: ["swiftc",
2256-
"-I", cHeadersPath.nativePathString(escaped: true),
2257-
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
2258-
"-explicit-module-build", "-v",
2259-
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
2260-
"-working-directory", path.nativePathString(escaped: true),
2261-
"-explain-module-dependency", "A",
2262-
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
2263-
env: ProcessEnv.vars)
2264-
let jobs = try driver.planBuild()
2265-
try driver.run(jobs: jobs)
2266-
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
2267-
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
2268-
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})
22692255

2270-
for diag in driver.diagnosticEngine.diagnostics {
2271-
print(diag.behavior)
2272-
print(diag.message)
2256+
// Detailed explain (all possible paths)
2257+
do {
2258+
var driver = try Driver(args: ["swiftc",
2259+
"-I", cHeadersPath.nativePathString(escaped: true),
2260+
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
2261+
"-explicit-module-build", "-v",
2262+
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
2263+
"-working-directory", path.nativePathString(escaped: true),
2264+
"-explain-module-dependency-detailed", "A",
2265+
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
2266+
env: ProcessEnv.vars)
2267+
let jobs = try driver.planBuild()
2268+
try driver.run(jobs: jobs)
2269+
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
2270+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
2271+
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})
2272+
2273+
for diag in driver.diagnosticEngine.diagnostics {
2274+
print(diag.behavior)
2275+
print(diag.message)
2276+
}
2277+
XCTAssertEqual(driver.diagnosticEngine.diagnostics.filter { $0.behavior == .note}.count, 2)
2278+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
2279+
$0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)"})
2280+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
2281+
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)"})
2282+
}
2283+
2284+
// Simple explain (first available path)
2285+
do {
2286+
var driver = try Driver(args: ["swiftc",
2287+
"-I", cHeadersPath.nativePathString(escaped: true),
2288+
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
2289+
"-explicit-module-build", "-v",
2290+
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
2291+
"-working-directory", path.nativePathString(escaped: true),
2292+
"-explain-module-dependency", "A",
2293+
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
2294+
env: ProcessEnv.vars)
2295+
let jobs = try driver.planBuild()
2296+
try driver.run(jobs: jobs)
2297+
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
2298+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
2299+
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})
2300+
2301+
for diag in driver.diagnosticEngine.diagnostics {
2302+
print(diag.behavior)
2303+
print(diag.message)
2304+
}
2305+
XCTAssertEqual(driver.diagnosticEngine.diagnostics.filter { $0.behavior == .note}.count, 1)
2306+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
2307+
($0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)" ||
2308+
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)")})
22732309
}
2274-
XCTAssertEqual(driver.diagnosticEngine.diagnostics.filter { $0.behavior == .note}.count, 2)
2275-
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
2276-
$0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)"})
2277-
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
2278-
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)"})
22792310
}
22802311
}
22812312

0 commit comments

Comments
 (0)