Skip to content
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
64 changes: 36 additions & 28 deletions Sources/SwiftDriver/Driver/Driver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,38 @@ extension Diagnostic.Message {
}
}

extension Driver {
func explainModuleDependency(_ explainModuleName: String, allPaths: Bool) throws {
guard let dependencyPlanner = explicitDependencyBuildPlanner else {
fatalError("Cannot explain dependency without Explicit Build Planner")
}
guard let dependencyPaths = try dependencyPlanner.explainDependency(explainModuleName, allPaths: allPaths) else {
diagnosticEngine.emit(.remark("No such module dependency found: '\(explainModuleName)'"))
return
}
diagnosticEngine.emit(.remark("Module '\(moduleOutputInfo.name)' depends on '\(explainModuleName)'"))
for path in dependencyPaths {
var pathString:String = ""
for (index, moduleId) in path.enumerated() {
switch moduleId {
case .swift(let moduleName):
pathString = pathString + "[" + moduleName + "]"
case .swiftPrebuiltExternal(let moduleName):
pathString = pathString + "[" + moduleName + "]"
case .clang(let moduleName):
pathString = pathString + "[" + moduleName + "](ObjC)"
case .swiftPlaceholder(_):
fatalError("Unexpected unresolved Placeholder module")
}
if index < path.count - 1 {
pathString = pathString + " -> "
}
}
diagnosticEngine.emit(.note(pathString))
}
}
}

extension Driver {
/// Determine the driver kind based on the command-line arguments, consuming the arguments
/// conveying this information.
Expand Down Expand Up @@ -1520,34 +1552,10 @@ extension Driver {
}

// If we're only supposed to explain a dependency on a given module, do so now.
if let explainModuleName = parsedOptions.getLastArgument(.explainModuleDependency) {
guard let dependencyPlanner = explicitDependencyBuildPlanner else {
fatalError("Cannot explain dependency without Explicit Build Planner")
}
guard let dependencyPaths = try dependencyPlanner.explainDependency(explainModuleName.asSingle) else {
diagnosticEngine.emit(.remark("No such module dependency found: '\(explainModuleName.asSingle)'"))
return
}
diagnosticEngine.emit(.remark("Module '\(moduleOutputInfo.name)' depends on '\(explainModuleName.asSingle)'"))
for path in dependencyPaths {
var pathString:String = ""
for (index, moduleId) in path.enumerated() {
switch moduleId {
case .swift(let moduleName):
pathString = pathString + "[" + moduleName + "]"
case .swiftPrebuiltExternal(let moduleName):
pathString = pathString + "[" + moduleName + "]"
case .clang(let moduleName):
pathString = pathString + "[" + moduleName + "](ObjC)"
case .swiftPlaceholder(_):
fatalError("Unexpected unresolved Placeholder module")
}
if index < path.count - 1 {
pathString = pathString + " -> "
}
}
diagnosticEngine.emit(.note(pathString))
}
if let explainModuleName = parsedOptions.getLastArgument(.explainModuleDependencyDetailed) {
try explainModuleDependency(explainModuleName.asSingle, allPaths: true)
} else if let explainModuleNameDetailed = parsedOptions.getLastArgument(.explainModuleDependency) {
try explainModuleDependency(explainModuleNameDetailed.asSingle, allPaths: false)
}

if parsedOptions.contains(.driverPrintOutputFileMap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,19 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT
}

internal extension ExplicitDependencyBuildPlanner {
func explainDependency(_ dependencyModuleName: String) throws -> [[ModuleDependencyId]]? {
return try dependencyGraph.explainDependency(dependencyModuleName: dependencyModuleName)
func explainDependency(_ dependencyModuleName: String, allPaths: Bool) throws -> [[ModuleDependencyId]]? {
return try dependencyGraph.explainDependency(dependencyModuleName: dependencyModuleName, allPaths: allPaths)
}

func findPath(from source: ModuleDependencyId, to destination: ModuleDependencyId) throws -> [ModuleDependencyId]? {
guard dependencyGraph.modules.contains(where: { $0.key == destination }) else { return nil }
var result: [ModuleDependencyId]? = nil
var visited: Set<ModuleDependencyId> = []
try dependencyGraph.findAPath(source: source,
pathSoFar: [source],
visited: &visited,
result: &result) { $0 == destination }
return result
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,46 +415,97 @@ internal extension InterModuleDependencyGraph {
}

internal extension InterModuleDependencyGraph {
func explainDependency(dependencyModuleName: String) throws -> [[ModuleDependencyId]]? {
func explainDependency(dependencyModuleName: String, allPaths: Bool) throws -> [[ModuleDependencyId]]? {
guard modules.contains(where: { $0.key.moduleName == dependencyModuleName }) else { return nil }
var results = Set<[ModuleDependencyId]>()
try findAllPaths(source: .swift(mainModuleName),
to: dependencyModuleName,
pathSoFar: [.swift(mainModuleName)],
results: &results)
return results.sorted(by: { $0.count < $1.count })
var result: Set<[ModuleDependencyId]> = []
if allPaths {
try findAllPaths(source: .swift(mainModuleName),
pathSoFar: [.swift(mainModuleName)],
results: &result,
destinationMatch: { $0.moduleName == dependencyModuleName })
} else {
var visited: Set<ModuleDependencyId> = []
var singlePathResult: [ModuleDependencyId]? = nil
if try findAPath(source: .swift(mainModuleName),
pathSoFar: [.swift(mainModuleName)],
visited: &visited,
result: &singlePathResult,
destinationMatch: { $0.moduleName == dependencyModuleName }),
let resultingPath = singlePathResult {
result = [resultingPath]
}
}
return Array(result)
}

@discardableResult
func findAPath(source: ModuleDependencyId,
pathSoFar: [ModuleDependencyId],
visited: inout Set<ModuleDependencyId>,
result: inout [ModuleDependencyId]?,
destinationMatch: (ModuleDependencyId) -> Bool) throws -> Bool {
// Mark this node as visited
visited.insert(source)
let sourceInfo = try moduleInfo(of: source)
if destinationMatch(source) {
// If the source is a target Swift module, also check if it
// depends on a corresponding Clang module with the same name.
// If it does, add it to the path as well.
var completePath = pathSoFar
if let dependencies = sourceInfo.directDependencies,
dependencies.contains(.clang(source.moduleName)) {
completePath.append(.clang(source.moduleName))
}
result = completePath
return true
}

var allDependencies = sourceInfo.directDependencies ?? []
if case .swift(let swiftModuleDetails) = sourceInfo.details,
let overlayDependencies = swiftModuleDetails.swiftOverlayDependencies {
allDependencies.append(contentsOf: overlayDependencies)
}

for dependency in allDependencies {
if try findAPath(source: dependency,
pathSoFar: pathSoFar + [dependency],
visited: &visited,
result: &result,
destinationMatch: destinationMatch) {
return true
}
}
return false
}

private func findAllPaths(source: ModuleDependencyId,
to moduleName: String,
pathSoFar: [ModuleDependencyId],
results: inout Set<[ModuleDependencyId]>) throws {
results: inout Set<[ModuleDependencyId]>,
destinationMatch: (ModuleDependencyId) -> Bool) throws {
let sourceInfo = try moduleInfo(of: source)
// If the source is our target, we are done
if source.moduleName == moduleName {
if destinationMatch(source) {
// If the source is a target Swift module, also check if it
// depends on a corresponding Clang module with the same name.
// If it does, add it to the path as well.
var completePath = pathSoFar
if let dependencies = sourceInfo.directDependencies,
dependencies.contains(.clang(moduleName)) {
completePath.append(.clang(moduleName))
dependencies.contains(.clang(source.moduleName)) {
completePath.append(.clang(source.moduleName))
}
results.insert(completePath)
return
}

var allDependencies = sourceInfo.directDependencies ?? []
if case .swift(let swiftModuleDetails) = sourceInfo.details,
let overlayDependencies = swiftModuleDetails.swiftOverlayDependencies {
allDependencies.append(contentsOf: overlayDependencies)
}

for dependency in allDependencies {
try findAllPaths(source: dependency,
to: moduleName,
pathSoFar: pathSoFar + [dependency],
results: &results)
results: &results,
destinationMatch: destinationMatch)
}
}
}
1 change: 1 addition & 0 deletions Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension Driver {
/// If the driver is in Explicit Module Build mode, the dependency graph has been computed
case computed
}

/// Add frontend options that are common to different frontend invocations.
mutating func addCommonFrontendOptions(
commandLine: inout [Job.ArgTemplate],
Expand Down
4 changes: 3 additions & 1 deletion Sources/SwiftOptions/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,8 @@ extension Option {
public static let experimentalSpiImports: Option = Option("-experimental-spi-imports", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable experimental support for SPI imports")
public static let experimentalSpiOnlyImports: Option = Option("-experimental-spi-only-imports", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable use of @_spiOnly imports")
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")
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.")
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.")
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.")
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'.")
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'")
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")
Expand Down Expand Up @@ -1371,6 +1372,7 @@ extension Option {
Option.experimentalSpiImports,
Option.experimentalSpiOnlyImports,
Option.enableExperimentalSwiftBasedClosureSpecialization,
Option.explainModuleDependencyDetailed,
Option.explainModuleDependency,
Option.explicitAutoLinking,
Option.explicitDependencyGraphFormat,
Expand Down
75 changes: 53 additions & 22 deletions Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2248,30 +2248,61 @@ final class ExplicitModuleBuildTests: XCTestCase {
try testInputsPath.appending(component: "ExplicitModuleBuilds")
.appending(component: "Swift")
let sdkArgumentsForTesting = (try? Driver.sdkArgumentsForTesting()) ?? []
var driver = try Driver(args: ["swiftc",
"-I", cHeadersPath.nativePathString(escaped: true),
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
"-explicit-module-build", "-v",
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
"-working-directory", path.nativePathString(escaped: true),
"-explain-module-dependency", "A",
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
env: ProcessEnv.vars)
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})

for diag in driver.diagnosticEngine.diagnostics {
print(diag.behavior)
print(diag.message)
// Detailed explain (all possible paths)
do {
var driver = try Driver(args: ["swiftc",
"-I", cHeadersPath.nativePathString(escaped: true),
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
"-explicit-module-build", "-v",
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
"-working-directory", path.nativePathString(escaped: true),
"-explain-module-dependency-detailed", "A",
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
env: ProcessEnv.vars)
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})

for diag in driver.diagnosticEngine.diagnostics {
print(diag.behavior)
print(diag.message)
}
XCTAssertEqual(driver.diagnosticEngine.diagnostics.filter { $0.behavior == .note}.count, 2)
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
$0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)"})
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)"})
}

// Simple explain (first available path)
do {
var driver = try Driver(args: ["swiftc",
"-I", cHeadersPath.nativePathString(escaped: true),
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
"-explicit-module-build", "-v",
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
"-working-directory", path.nativePathString(escaped: true),
"-explain-module-dependency", "A",
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
env: ProcessEnv.vars)
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})

for diag in driver.diagnosticEngine.diagnostics {
print(diag.behavior)
print(diag.message)
}
XCTAssertEqual(driver.diagnosticEngine.diagnostics.filter { $0.behavior == .note}.count, 1)
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
($0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)" ||
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)")})
}
XCTAssertEqual(driver.diagnosticEngine.diagnostics.filter { $0.behavior == .note}.count, 2)
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
$0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)"})
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)"})
}
}

Expand Down