Skip to content

Commit 26daecd

Browse files
committed
Add option to explain a given module dependency
Adds support for '-explain-dependency <MODULE_NAME>', which causes the driver to run a dependency scan, and find all dependency paths that cause the main module to depend, directly or transitively, on a given module dependency.
1 parent 8f4e72d commit 26daecd

File tree

7 files changed

+146
-5
lines changed

7 files changed

+146
-5
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,37 @@ extension Driver {
13321332
return
13331333
}
13341334

1335+
// If we're only supposed to explain a dependency on a given module, do so now.
1336+
if let explainModuleName = parsedOptions.getLastArgument(.explainModuleDependency) {
1337+
guard let dependencyPlanner = explicitDependencyBuildPlanner else {
1338+
fatalError("Cannot explain dependency without Explicit Build Planner")
1339+
}
1340+
guard let dependencyPaths = try dependencyPlanner.explainDependency(explainModuleName.asSingle) else {
1341+
diagnosticEngine.emit(.remark("No such module dependency found: '\(explainModuleName.asSingle)'"))
1342+
return
1343+
}
1344+
diagnosticEngine.emit(.remark("Module '\(moduleOutputInfo.name)' depends on '\(explainModuleName.asSingle)'"))
1345+
for path in dependencyPaths {
1346+
var pathString:String = ""
1347+
for (index, moduleId) in path.enumerated() {
1348+
switch moduleId {
1349+
case .swift(let moduleName):
1350+
pathString = pathString + "[" + moduleName + "]"
1351+
case .swiftPrebuiltExternal(let moduleName):
1352+
pathString = pathString + "[" + moduleName + "]"
1353+
case .clang(let moduleName):
1354+
pathString = pathString + "[" + moduleName + "](ObjC)"
1355+
case .swiftPlaceholder(_):
1356+
fatalError("Unexpected unresolved Placeholder module")
1357+
}
1358+
if index < path.count - 1 {
1359+
pathString = pathString + " -> "
1360+
}
1361+
}
1362+
diagnosticEngine.emit(.note(pathString))
1363+
}
1364+
}
1365+
13351366
if parsedOptions.contains(.driverPrintOutputFileMap) {
13361367
if let outputFileMap = self.outputFileMap {
13371368
stderrStream <<< outputFileMap.description

Sources/SwiftDriver/ExplicitModuleBuilds/ExplicitDependencyBuildPlanner.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,12 @@ public typealias ExternalTargetModuleDetailsMap = [ModuleDependencyId: ExternalT
487487
}
488488
}
489489

490+
internal extension ExplicitDependencyBuildPlanner {
491+
func explainDependency(_ dependencyModuleName: String) throws -> [[ModuleDependencyId]]? {
492+
return try dependencyGraph.explainDependency(dependencyModuleName: dependencyModuleName)
493+
}
494+
}
495+
490496
// InterModuleDependencyGraph printing, useful for debugging
491497
internal extension InterModuleDependencyGraph {
492498
func prettyPrintString() throws -> String {

Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/CommonDependencyOperations.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,48 @@ extension InterModuleDependencyGraph {
235235
details: .clang(combinedModuleDetails))
236236
}
237237
}
238+
239+
internal extension InterModuleDependencyGraph {
240+
func explainDependency(dependencyModuleName: String) throws -> [[ModuleDependencyId]]? {
241+
guard modules.contains(where: { $0.key.moduleName == dependencyModuleName }) else { return nil }
242+
var results = [[ModuleDependencyId]]()
243+
try findAllPaths(source: .swift(mainModuleName),
244+
to: dependencyModuleName,
245+
pathSoFar: [.swift(mainModuleName)],
246+
results: &results)
247+
return Array(results)
248+
}
249+
250+
251+
private func findAllPaths(source: ModuleDependencyId,
252+
to moduleName: String,
253+
pathSoFar: [ModuleDependencyId],
254+
results: inout [[ModuleDependencyId]]) throws {
255+
let sourceInfo = try moduleInfo(of: source)
256+
// If the source is our target, we are done
257+
guard source.moduleName != moduleName else {
258+
// If the source is a target Swift module, also check if it
259+
// depends on a corresponding Clang module with the same name.
260+
// If it does, add it to the path as well.
261+
var completePath = pathSoFar
262+
if let dependencies = sourceInfo.directDependencies,
263+
dependencies.contains(.clang(moduleName)) {
264+
completePath.append(.clang(moduleName))
265+
}
266+
results.append(completePath)
267+
return
268+
}
269+
270+
// If no more dependencies, this is a leaf node, we are done
271+
guard let dependencies = sourceInfo.directDependencies else {
272+
return
273+
}
274+
275+
for dependency in dependencies {
276+
try findAllPaths(source: dependency,
277+
to: moduleName,
278+
pathSoFar: pathSoFar + [dependency],
279+
results: &results)
280+
}
281+
}
282+
}

Sources/SwiftDriver/Jobs/Planning.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ extension Driver {
140140
IncrementalCompilationState.InitialStateForPlanning?)
141141
throws -> InterModuleDependencyGraph? {
142142
let interModuleDependencyGraph: InterModuleDependencyGraph?
143-
if parsedOptions.contains(.driverExplicitModuleBuild) {
143+
if parsedOptions.contains(.driverExplicitModuleBuild) ||
144+
parsedOptions.contains(.explainModuleDependency) {
144145
interModuleDependencyGraph =
145146
try initialIncrementalState?.maybeUpToDatePriorInterModuleDependencyGraph ??
146147
gatherModuleDependencies()
@@ -195,13 +196,15 @@ extension Driver {
195196
dependencyGraph: InterModuleDependencyGraph?,
196197
addJob: (Job) -> Void)
197198
throws {
198-
// If asked, add jobs to precompile module dependencies
199-
guard parsedOptions.contains(.driverExplicitModuleBuild) else { return }
200199
guard let resolvedDependencyGraph = dependencyGraph else {
201-
fatalError("Attempting to plan explicit dependency build without a dependency graph")
200+
return
202201
}
203202
let modulePrebuildJobs =
204203
try generateExplicitModuleDependenciesJobs(dependencyGraph: resolvedDependencyGraph)
204+
// If asked, add jobs to precompile module dependencies. Otherwise exit.
205+
// We may have a dependency graph but not be required to add pre-compile jobs to the build plan,
206+
// for example when `-explain-dependency` is being used.
207+
guard parsedOptions.contains(.driverExplicitModuleBuild) else { return }
205208
modulePrebuildJobs.forEach(addJob)
206209
}
207210

Sources/SwiftOptions/Options.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extension Option {
2020
public static let accessNotesPathEQ: Option = Option("-access-notes-path=", .joined, alias: Option.accessNotesPath, attributes: [.frontend, .argumentIsPath])
2121
public static let accessNotesPath: Option = Option("-access-notes-path", .separate, attributes: [.frontend, .argumentIsPath], helpText: "Specify YAML file to override attributes on Swift declarations in this module")
2222
public static let aliasModuleNamesInModuleInterface: Option = Option("-alias-module-names-in-module-interface", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "When emitting a module interface, disambiguate modules using distinct alias names")
23+
public static let allowUnstableCacheKeyForTesting: Option = Option("-allow-unstable-cache-key-for-testing", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Allow compilation caching with unstable inputs for testing purpose")
2324
public static let allowableClient: Option = Option("-allowable-client", .separate, attributes: [.frontend], metaVar: "<vers>", helpText: "Module names that are allowed to import this module")
2425
public static let analyzeRequirementMachine: Option = Option("-analyze-requirement-machine", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Print out requirement machine statistics at the end of the compilation job")
2526
public static let apiDiffDataDir: Option = Option("-api-diff-data-dir", .separate, attributes: [.frontend, .noInteractive, .doesNotAffectIncrementalBuild, .argumentIsPath], metaVar: "<path>", helpText: "Load platform and version specific API migration data files from <path>. Ignored if -api-diff-data-file is specified.")
@@ -329,6 +330,7 @@ extension Option {
329330
public static let enableBatchMode: Option = Option("-enable-batch-mode", .flag, attributes: [.helpHidden, .frontend, .noInteractive], helpText: "Enable combining frontend jobs into batches")
330331
public static let enableBridgingPch: Option = Option("-enable-bridging-pch", .flag, attributes: [.helpHidden], helpText: "Enable automatic generation of bridging PCH files")
331332
public static let enableBuiltinModule: Option = Option("-enable-builtin-module", .flag, attributes: [.frontend, .moduleInterface], helpText: "Enables the explicit import of the Builtin module")
333+
public static let enableCas: Option = Option("-enable-cas", .flag, attributes: [.frontend, .noDriver], helpText: "Enable CAS for swift-frontend")
332334
public static let enableCollocateMetadataFunctions: Option = Option("-enable-collocate-metadata-functions", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable collocate metadata functions")
333335
public static let enableColocateTypeDescriptors: Option = Option("-enable-colocate-type-descriptors", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable colocate type descriptors")
334336
public static let enableConformanceAvailabilityErrors: Option = Option("-enable-conformance-availability-errors", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Diagnose conformance availability violations as errors")
@@ -418,13 +420,14 @@ extension Option {
418420
public static let driverExperimentalExplicitModuleBuild: Option = Option("-experimental-explicit-module-build", .flag, alias: Option.driverExplicitModuleBuild, attributes: [.helpHidden], helpText: "Prebuild module dependencies to make them explicit")
419421
public static let experimentalHermeticSealAtLink: Option = Option("-experimental-hermetic-seal-at-link", .flag, attributes: [.helpHidden, .frontend], helpText: "Library code can assume that all clients are visible at linktime, and aggressively strip unused code")
420422
public static let experimentalOneWayClosureParams: Option = Option("-experimental-one-way-closure-params", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable experimental support for one-way closure parameters")
421-
public static let ExperimentalPerformanceAnnotations: Option = Option("-experimental-performance-annotations", .flag, attributes: [.helpHidden, .frontend], helpText: "Enable experimental performance annotations")
423+
public static let ExperimentalPerformanceAnnotations: Option = Option("-experimental-performance-annotations", .flag, attributes: [.helpHidden, .frontend], helpText: "Deprecated, has no effect")
422424
public static let experimentalPrintFullConvention: Option = Option("-experimental-print-full-convention", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "When emitting a module interface or SIL, emit additional @convention arguments, regardless of whether they were written in the source. Also requires -use-clang-function-types to be enabled.")
423425
public static let experimentalSkipAllFunctionBodies: Option = Option("-experimental-skip-all-function-bodies", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Skip type-checking function bodies and all SIL generation")
424426
public static let experimentalSkipNonInlinableFunctionBodiesWithoutTypes: Option = Option("-experimental-skip-non-inlinable-function-bodies-without-types", .flag, attributes: [.helpHidden, .frontend], helpText: "Skip work on non-inlinable function bodies that do not declare nested types")
425427
public static let experimentalSkipNonInlinableFunctionBodies: Option = Option("-experimental-skip-non-inlinable-function-bodies", .flag, attributes: [.helpHidden, .frontend], helpText: "Skip type-checking and SIL generation for non-inlinable function bodies")
426428
public static let experimentalSpiImports: Option = Option("-experimental-spi-imports", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable experimental support for SPI imports")
427429
public static let experimentalSpiOnlyImports: Option = Option("-experimental-spi-only-imports", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Enable use of @_spiOnly imports")
430+
public static let explainModuleDependency: Option = Option("-explain-module-dependency", .separate, attributes: [], helpText: "Emit remark/notes describing why compilaiton may depend on a module with a given name.")
428431
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'")
429432
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")
430433
public static let driverExplicitModuleBuild: Option = Option("-explicit-module-build", .flag, attributes: [.helpHidden], helpText: "Prebuild module dependencies to make them explicit")
@@ -792,6 +795,7 @@ extension Option {
792795
Option.accessNotesPathEQ,
793796
Option.accessNotesPath,
794797
Option.aliasModuleNamesInModuleInterface,
798+
Option.allowUnstableCacheKeyForTesting,
795799
Option.allowableClient,
796800
Option.analyzeRequirementMachine,
797801
Option.apiDiffDataDir,
@@ -823,6 +827,8 @@ extension Option {
823827
Option.buildModuleFromParseableInterface,
824828
Option.bypassBatchModeChecks,
825829
Option.candidateModuleFile,
830+
Option.casFs,
831+
Option.casPath,
826832
Option.checkApiAvailabilityOnly,
827833
Option.checkOnoneCompleteness,
828834
Option.clangBuildSessionFile,
@@ -1101,6 +1107,7 @@ extension Option {
11011107
Option.enableBatchMode,
11021108
Option.enableBridgingPch,
11031109
Option.enableBuiltinModule,
1110+
Option.enableCas,
11041111
Option.enableCollocateMetadataFunctions,
11051112
Option.enableColocateTypeDescriptors,
11061113
Option.enableConformanceAvailabilityErrors,
@@ -1197,6 +1204,7 @@ extension Option {
11971204
Option.experimentalSkipNonInlinableFunctionBodies,
11981205
Option.experimentalSpiImports,
11991206
Option.experimentalSpiOnlyImports,
1207+
Option.explainModuleDependency,
12001208
Option.explicitDependencyGraphFormat,
12011209
Option.explicitInterfaceModuleBuild,
12021210
Option.driverExplicitModuleBuild,

Sources/makeOptions/makeOptions.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ enum SwiftFlags {
6464
SwiftAPIDigesterOption = (1 << 17),
6565
NewDriverOnlyOption = (1 << 18),
6666
ModuleInterfaceOptionIgnorable = (1 << 19),
67+
ModuleInterfaceOptionIgnorablePrivate = (1 << 20)
6768
};
6869

6970
static std::set<std::string> swiftKeywords = { "internal", "static" };

Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,53 @@ final class ExplicitModuleBuildTests: XCTestCase {
16131613
XCTAssertEqual(moduleMap[1].sourceInfoPath!.path.description, "B.swiftsourceinfo")
16141614
XCTAssertEqual(moduleMap[1].isFramework, false)
16151615
}
1616+
1617+
1618+
func testTraceDependency() throws {
1619+
try withTemporaryDirectory { path in
1620+
try localFileSystem.changeCurrentWorkingDirectory(to: path)
1621+
let moduleCachePath = path.appending(component: "ModuleCache")
1622+
try localFileSystem.createDirectory(moduleCachePath)
1623+
let main = path.appending(component: "testTraceDependency.swift")
1624+
try localFileSystem.writeFileContents(main) {
1625+
$0 <<< "import C;"
1626+
$0 <<< "import E;"
1627+
$0 <<< "import G;"
1628+
}
1629+
1630+
let cHeadersPath: AbsolutePath =
1631+
testInputsPath.appending(component: "ExplicitModuleBuilds")
1632+
.appending(component: "CHeaders")
1633+
let swiftModuleInterfacesPath: AbsolutePath =
1634+
testInputsPath.appending(component: "ExplicitModuleBuilds")
1635+
.appending(component: "Swift")
1636+
let sdkArgumentsForTesting = (try? Driver.sdkArgumentsForTesting()) ?? []
1637+
var driver = try Driver(args: ["swiftc",
1638+
"-I", cHeadersPath.nativePathString(escaped: true),
1639+
"-I", swiftModuleInterfacesPath.nativePathString(escaped: true),
1640+
"-explicit-module-build", "-v",
1641+
"-module-cache-path", moduleCachePath.nativePathString(escaped: true),
1642+
"-working-directory", path.nativePathString(escaped: true),
1643+
"-explain-module-dependency", "A",
1644+
main.nativePathString(escaped: true)] + sdkArgumentsForTesting,
1645+
env: ProcessEnv.vars)
1646+
let jobs = try driver.planBuild()
1647+
try driver.run(jobs: jobs)
1648+
XCTAssertTrue(!driver.diagnosticEngine.diagnostics.isEmpty)
1649+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .remark &&
1650+
$0.message.text == "Module 'testTraceDependency' depends on 'A'"})
1651+
1652+
for diag in driver.diagnosticEngine.diagnostics {
1653+
print(diag.behavior)
1654+
print(diag.message)
1655+
}
1656+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
1657+
$0.message.text == "[testTraceDependency] -> [A] -> [A](ObjC)"})
1658+
XCTAssertTrue(driver.diagnosticEngine.diagnostics.contains { $0.behavior == .note &&
1659+
$0.message.text == "[testTraceDependency] -> [C](ObjC) -> [B](ObjC) -> [A](ObjC)"})
1660+
}
1661+
}
1662+
16161663
// We only care about prebuilt modules in macOS.
16171664
#if os(macOS)
16181665
func testPrebuiltModuleGenerationJobs() throws {

0 commit comments

Comments
 (0)