Skip to content

Commit 93f4293

Browse files
committed
Add external dependency resolution logic.
Resolves `placeholder` dependencies in the dependency graph computed by the scanning action using external dependencies passed in from the client.
1 parent 03230fe commit 93f4293

File tree

7 files changed

+189
-23
lines changed

7 files changed

+189
-23
lines changed

Sources/SwiftDriver/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
add_library(SwiftDriver
1010
"Explicit Module Builds/ClangModuleBuildJobCache.swift"
11-
"Explicit Module Builds/ExplicitModuleBuildHandler.swift"
11+
"Explicit Module Builds/ExplicitModuleBuildHandler.swift"
12+
"Explicit Module Builds/PlaceholderDependencyResolution.swift"
1213
"Explicit Module Builds/InterModuleDependencyGraph.swift"
1314
"Explicit Module Builds/ModuleDependencyScanning.swift"
1415
"Explicit Module Builds/ModuleArtifacts.swift"

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public struct Driver {
3030
case missingPCMArguments(String)
3131
case missingModuleDependency(String)
3232
case dependencyScanningFailure(Int, String)
33+
case missingExternalDependency(String)
3334

3435
public var description: String {
3536
switch self {
@@ -59,6 +60,8 @@ public struct Driver {
5960
return "Module Dependency Scanner returned with non-zero exit status: \(code), \(error)"
6061
case .unableToLoadOutputFileMap(let path):
6162
return "unable to load output file map '\(path)': no such file or directory"
63+
case .missingExternalDependency(let moduleName):
64+
return "Missing External dependency info for module: \(moduleName)"
6265
}
6366
}
6467
}

Sources/SwiftDriver/Explicit Module Builds/ExplicitModuleBuildHandler.swift

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public typealias ExternalDependencyArtifactMap =
3535
private let toolchain: Toolchain
3636

3737
/// A collection of external dependency modules, and their binary module file paths and dependency graph.
38-
private let externalDependencyArtifactMap: ExternalDependencyArtifactMap
38+
internal let externalDependencyArtifactMap: ExternalDependencyArtifactMap
3939

4040
/// The file system which we should interact with.
4141
/// FIXME: Our end goal is to not have any direct filesystem manipulation in here, but that's dependent on getting the
@@ -120,24 +120,17 @@ public typealias ExternalDependencyArtifactMap =
120120
///
121121
mutating public func generateExplicitModuleDependenciesBuildJobs() throws -> [Job] {
122122
// Resolve placeholder dependencies in the dependency graph, if any.
123-
try resolvePlaceholderDependencies()
123+
if (!externalDependencyArtifactMap.isEmpty) {
124+
try resolvePlaceholderDependencies()
125+
}
124126

125127
// Compute jobs for all main module dependencies
126128
var mainModuleInputs: [TypedVirtualPath] = []
127129
var mainModuleCommandLine: [Job.ArgTemplate] = []
128130
try resolveMainModuleDependencies(inputs: &mainModuleInputs,
129131
commandLine: &mainModuleCommandLine)
130-
return Array(swiftModuleBuildCache.values) + clangTargetModuleBuildCache.allJobs
131-
}
132132

133-
/// TODO: Explain
134-
mutating public func resolvePlaceholderDependencies() throws {
135-
print("Hello, World")
136-
for moduleId in dependencyGraph.modules.keys {
137-
if case .swiftPlaceholder(let moduleName) = moduleId {
138-
print(moduleName)
139-
}
140-
}
133+
return Array(swiftModuleBuildCache.values) + clangTargetModuleBuildCache.allJobs
141134
}
142135

143136
/// Resolve all module dependencies of the main module and add them to the lists of
@@ -338,7 +331,7 @@ public typealias ExternalDependencyArtifactMap =
338331
clangDependencyArtifacts: &clangDependencyArtifacts,
339332
swiftDependencyArtifacts: &swiftDependencyArtifacts)
340333
case .swiftPlaceholder:
341-
fatalError("Unresolved placeholder dependencies at planning stage.")
334+
fatalError("Unresolved placeholder dependencies at planning stage: \(dependencyId) of \(moduleId)")
342335
}
343336
}
344337
}
@@ -359,7 +352,7 @@ public typealias ExternalDependencyArtifactMap =
359352

360353
let swiftModulePath: TypedVirtualPath
361354
if case .swift(let details) = dependencyInfo.details,
362-
let compiledModulePath = details.compiledModulePath {
355+
let compiledModulePath = details.explicitCompiledModulePath {
363356
// If an already-compiled module is available, use it.
364357
swiftModulePath = .init(file: try VirtualPath(path: compiledModulePath),
365358
type: .swiftModule)

Sources/SwiftDriver/Explicit Module Builds/InterModuleDependencyGraph.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public enum ModuleDependencyId: Hashable {
1717
case swiftPlaceholder(String)
1818
case clang(String)
1919

20-
var moduleName: String {
20+
public var moduleName: String {
2121
switch self {
2222
case .swift(let name): return name
2323
case .swiftPlaceholder(let name): return name
@@ -77,8 +77,9 @@ public struct SwiftModuleDetails: Codable {
7777
/// The paths of potentially ready-to-use compiled modules for the interface.
7878
public var compiledModuleCandidates: [String]?
7979

80-
/// The path to the already-compiled module.
81-
public var compiledModulePath: String?
80+
/// The path to the already-compiled module that must be used instead of
81+
/// generating a job to build this module.
82+
public var explicitCompiledModulePath: String?
8283

8384
/// The bridging header, if any.
8485
public var bridgingHeaderPath: String?
@@ -92,7 +93,7 @@ public struct SwiftModuleDetails: Codable {
9293
/// To build a PCM to be used by this Swift module, we need to append these
9394
/// arguments to the generic PCM build arguments reported from the dependency
9495
/// graph.
95-
var extraPcmArgs: [String]?
96+
public var extraPcmArgs: [String]?
9697
}
9798

9899
/// Details specific to Swift external modules.

Sources/SwiftDriver/Explicit Module Builds/ModuleDependencyScanning.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ extension Driver {
5454
supportsResponseFiles: true)
5555
}
5656

57-
/// Serialize a map of placeholder
57+
/// Serialize a map of placeholder (external) dependencies for the dependency scanner.
5858
func serializeExternalDependencyArtifacts(externalDependencyArtifactMap: ExternalDependencyArtifactMap)
5959
throws -> AbsolutePath {
6060
let temporaryDirectory = try determineTempDirectory()
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
//===--------------- PlaceholderDependencyResolution.swift ----------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import TSCBasic
13+
import TSCUtility
14+
import Foundation
15+
16+
extension ExplicitModuleBuildHandler {
17+
// Building a Swift module in Explicit Module Build mode requires passing all of its module
18+
// dependencies as explicit arguments to the build command.
19+
//
20+
// When the driver's clients (build systems) are planning a build that involves multiple
21+
// Swift modules, planning for each individual module may take place before its dependencies
22+
// have been built. This means that the dependency scanning action will not be able to
23+
// discover such modules. In such cases, the clients must provide the driver with information
24+
// about such external dependencies, including the path to where their compiled .swiftmodule
25+
// will be located, once built, and a full inter-module dependency graph for each such dependence.
26+
//
27+
// The driver will pass down the information about such external dependencies to the scanning
28+
// action, which will generate `placeholder` swift modules for them in the resulting dependency
29+
// graph. The driver will then use the complete dependency graph provided by
30+
// the client for each external dependency and use it to "resolve" the dependency's "placeholder"
31+
// module.
32+
//
33+
// Consider an example SwiftPM package with two targets: target B, and target A, where A
34+
// depends on B:
35+
// SwiftPM will process targets in a topological order and “bubble-up” each target’s
36+
// inter-module dependency graph to its dependees. First, SwiftPM will process B, and be
37+
// able to plan its full build because it does not have any target dependencies. Then the
38+
// driver is tasked with planning a build for A. SwiftPM will pass as input to the driver
39+
// the module dependency graph of its target’s dependencies, in this case, just the
40+
// dependency graph of B. The scanning action for module A will contain a placeholder module B,
41+
// which the driver will then resolve using B's full dependency graph provided by the client.
42+
43+
/// Resolve all placeholder dependencies using external dependency information provided by the client
44+
mutating public func resolvePlaceholderDependencies() throws {
45+
let placeholderModules = dependencyGraph.modules.keys.filter {
46+
if case .swiftPlaceholder(_) = $0 {
47+
return true
48+
}
49+
return false
50+
}
51+
52+
// Resolve all placeholder modules
53+
for moduleId in placeholderModules {
54+
guard let (placeholderModulePath, placeholderDependencyGraph) =
55+
externalDependencyArtifactMap[moduleId] else {
56+
throw Driver.Error.missingExternalDependency(moduleId.moduleName)
57+
}
58+
try resolvePlaceholderDependency(placeholderModulePath: placeholderModulePath,
59+
placeholderDependencyGraph: placeholderDependencyGraph)
60+
}
61+
}
62+
63+
/// Merge a given external module's dependency graph in place of a placeholder dependency
64+
mutating public func resolvePlaceholderDependency(placeholderModulePath: AbsolutePath,
65+
placeholderDependencyGraph: InterModuleDependencyGraph)
66+
throws {
67+
// For every other module in the placeholder dependency graph, generate a new module info
68+
// containing only the pre-compiled module path, and insert it into the current module's
69+
// dependency graph, replacing equivalent (non pre-built) modules, if necessary.
70+
for (moduleId, moduleInfo) in placeholderDependencyGraph.modules {
71+
switch moduleId {
72+
case .swift(_):
73+
// Compute the compiled module path for this module.
74+
// If this module is the placeholder itself, this information was passed from SwiftPM
75+
// If this module is any other swift module, then the compiled module path is
76+
// a part of the details field.
77+
// Otherwise (for most other dependencies), it is the modulePath of the moduleInfo node.
78+
// FIXME: Will the `else` case ever happen?
79+
let compiledModulePath : String
80+
if moduleId.moduleName == placeholderDependencyGraph.mainModuleName {
81+
compiledModulePath = placeholderModulePath.description
82+
} else if case .swift(let details) = moduleInfo.details,
83+
let explicitModulePath = details.explicitCompiledModulePath {
84+
compiledModulePath = explicitModulePath
85+
} else {
86+
compiledModulePath = moduleInfo.modulePath.description
87+
}
88+
89+
let swiftDetails =
90+
SwiftModuleDetails(compiledModulePath: compiledModulePath)
91+
print("For module: \(moduleId.moduleName) set the path to:\n\(compiledModulePath)")
92+
let newInfo = ModuleInfo(modulePath: moduleInfo.modulePath.description,
93+
sourceFiles: nil,
94+
directDependencies: moduleInfo.directDependencies,
95+
details: ModuleInfo.Details.swift(swiftDetails))
96+
try insertOrReplaceModule(moduleId: moduleId, moduleInfo: newInfo)
97+
print("New Info: \(newInfo)")
98+
case .clang(_):
99+
// Because PCM modules file names encode the specific pcmArguments of their dependees,
100+
// we cannot use pre-built files here because we do not always know which target
101+
// they corrspond to, nor do we have a way to map from a certain target to a specific
102+
// pcm file. Because of this, all PCM dependencies, direct and transitive, have to be
103+
// built for all modules.
104+
if dependencyGraph.modules[moduleId] == nil {
105+
dependencyGraph.modules[moduleId] = moduleInfo
106+
}
107+
case .swiftPlaceholder(_):
108+
try insertOrReplaceModule(moduleId: moduleId, moduleInfo: moduleInfo)
109+
}
110+
}
111+
}
112+
113+
/// Insert a module into the handler's dependency graph. If a module with this identifier already exists,
114+
/// replace it's module with a moduleInfo that contains a path to an existing prebuilt .swiftmodule
115+
mutating public func insertOrReplaceModule(moduleId: ModuleDependencyId,
116+
moduleInfo: ModuleInfo) throws {
117+
// Check for placeholders to be replaced
118+
if dependencyGraph.modules[ModuleDependencyId.swiftPlaceholder(moduleId.moduleName)] != nil {
119+
try replaceModule(originalId: .swiftPlaceholder(moduleId.moduleName), replacementId: moduleId,
120+
replacementInfo: moduleInfo)
121+
}
122+
// Check for modules with the same Identifier, and replace if found
123+
else if dependencyGraph.modules[moduleId] != nil {
124+
try replaceModule(originalId: moduleId, replacementId: moduleId, replacementInfo: moduleInfo)
125+
// This module is new to the current dependency graph
126+
} else {
127+
dependencyGraph.modules[moduleId] = moduleInfo
128+
}
129+
}
130+
131+
/// Replace a module with a new one. Replace all references to the original module in other modules' dependencies
132+
/// with the new module.
133+
mutating public func replaceModule(originalId: ModuleDependencyId,
134+
replacementId: ModuleDependencyId,
135+
replacementInfo: ModuleInfo) throws {
136+
dependencyGraph.modules.removeValue(forKey: originalId)
137+
dependencyGraph.modules[replacementId] = replacementInfo
138+
for moduleId in dependencyGraph.modules.keys {
139+
var moduleInfo = dependencyGraph.modules[moduleId]!
140+
// Skip over other placeholders, they do not have dependencies
141+
if case .swiftPlaceholder(_) = moduleId {
142+
continue
143+
}
144+
if let originalModuleIndex = moduleInfo.directDependencies?.firstIndex(of: originalId) {
145+
moduleInfo.directDependencies![originalModuleIndex] = replacementId;
146+
}
147+
dependencyGraph.modules[moduleId] = moduleInfo
148+
}
149+
}
150+
}
151+
152+
/// Used for creating new module infos during placeholder dependency resolution
153+
/// Modules created this way only contain a path to a pre-built module file.
154+
extension SwiftModuleDetails {
155+
public init(compiledModulePath: String) {
156+
self.moduleInterfacePath = nil
157+
self.compiledModuleCandidates = nil
158+
self.explicitCompiledModulePath = compiledModulePath
159+
self.bridgingHeaderPath = nil
160+
self.bridgingSourceFiles = nil
161+
self.commandLine = nil
162+
self.extraPcmArgs = nil
163+
}
164+
}

Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ private func checkExplicitModuleBuildJob(job: Job,
5050
type: .clangModuleMap)
5151
XCTAssertEqual(job.kind, .generatePCM)
5252
XCTAssertTrue(job.inputs.contains(moduleMapPath))
53+
case .swiftPlaceholder(_):
54+
XCTFail("Placeholder dependency found.")
5355
}
5456
// Ensure the frontend was prohibited from doing implicit module builds
5557
XCTAssertTrue(job.commandLine.contains(.flag(String("-disable-implicit-swift-modules"))))
@@ -65,7 +67,7 @@ private func checkExplicitModuleBuildJobDependencies(job: Job,
6567
moduleInfo : ModuleInfo,
6668
moduleDependencyGraph: InterModuleDependencyGraph
6769
) throws {
68-
for dependencyId in moduleInfo.directDependencies {
70+
for dependencyId in moduleInfo.directDependencies! {
6971
let dependencyInfo = moduleDependencyGraph.modules[dependencyId]!
7072
switch dependencyInfo.details {
7173
case .swift(let swiftDetails):
@@ -85,7 +87,7 @@ private func checkExplicitModuleBuildJobDependencies(job: Job,
8587
from: Data(contents.contents))
8688
let dependencyArtifacts =
8789
dependencyInfoList.first(where:{ $0.moduleName == dependencyId.moduleName })
88-
XCTAssertEqual(dependencyArtifacts!.modulePath, swiftDetails.compiledModulePath ?? dependencyInfo.modulePath)
90+
XCTAssertEqual(dependencyArtifacts!.modulePath, swiftDetails.explicitCompiledModulePath ?? dependencyInfo.modulePath)
8991
case .clang(let clangDependencyDetails):
9092
let clangDependencyModulePathString =
9193
try ExplicitModuleBuildHandler.targetEncodedClangModuleFilePath(
@@ -101,10 +103,12 @@ private func checkExplicitModuleBuildJobDependencies(job: Job,
101103
.flag(String("-fmodule-file=\(clangDependencyModulePathString)"))))
102104
XCTAssertTrue(job.commandLine.contains(
103105
.flag(String("-fmodule-map-file=\(clangDependencyDetails.moduleMapPath)"))))
106+
case .swiftPlaceholder(_):
107+
XCTFail("Placeholder dependency found.")
104108
}
105109

106110
// Ensure all transitive dependencies got added as well.
107-
for transitiveDependencyId in dependencyInfo.directDependencies {
111+
for transitiveDependencyId in dependencyInfo.directDependencies! {
108112
try checkExplicitModuleBuildJobDependencies(job: job, pcmArgs: pcmArgs,
109113
moduleInfo: moduleDependencyGraph.modules[transitiveDependencyId]!,
110114
moduleDependencyGraph: moduleDependencyGraph)

0 commit comments

Comments
 (0)