Skip to content

Commit 48326b9

Browse files
committed
Teach Driver to generate swift-frontend invocations to build prebuilt module cache
We should be able to generate prebuilt module cache by reusing the infrastructure libSwiftDriver already provides. More specifically, we could (1) write a dummy Swift file imports every frameworks/libraries in an SDK, (2) use module dependency scanner in the driver to generate module building commands, and (3) use MultiJobExecutor to execute those commands topologically. This PR is for (2).
1 parent 1430a1f commit 48326b9

File tree

16 files changed

+417
-37
lines changed

16 files changed

+417
-37
lines changed

Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ let package = Package(
2121
.executable(
2222
name: "swift-help",
2323
targets: ["swift-help"]),
24+
.executable(
25+
name: "swift-build-sdk-interfaces",
26+
targets: ["swift-build-sdk-interfaces"]),
2427
.library(
2528
name: "SwiftDriver",
2629
targets: ["SwiftDriver"]),
@@ -93,6 +96,11 @@ let package = Package(
9396
name: "swift-help",
9497
dependencies: ["SwiftOptions", "ArgumentParser", "SwiftToolsSupport-auto"]),
9598

99+
/// The help executable.
100+
.target(
101+
name: "swift-build-sdk-interfaces",
102+
dependencies: ["SwiftDriver", "SwiftDriverExecution"]),
103+
96104
/// The `makeOptions` utility (for importing option definitions).
97105
.target(
98106
name: "makeOptions",

Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,6 @@ extension Driver {
7979
commandLine.appendFlag(Triple(variant, normalizing: true).triple)
8080
}
8181

82-
// Enable address top-byte ignored in the ARM64 backend.
83-
if targetTriple.arch == .aarch64 {
84-
commandLine.appendFlag(.Xllvm)
85-
commandLine.appendFlag("-aarch64-use-tbi")
86-
}
87-
8882
// Enable or disable ObjC interop appropriately for the platform
8983
if targetTriple.isDarwin {
9084
commandLine.appendFlag(.enableObjcInterop)

Sources/SwiftDriver/Jobs/Planning.swift

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,110 @@ extension Driver {
496496
return try explicitDependencyBuildPlanner!.generateExplicitModuleDependenciesBuildJobs()
497497
}
498498

499+
500+
public mutating func generatePrebuitModuleGenerationJobs(_ inputMap: [String: [TypedVirtualPath]],
501+
_ prebuiltModuleDir: AbsolutePath) throws -> [Job] {
502+
guard let rawSdkPath = frontendTargetInfo.sdkPath?.path else {
503+
// cannot proceed without SDK path
504+
return []
505+
}
506+
let sdkPath = VirtualPath.lookup(rawSdkPath)
507+
// Run the dependency scanner and update the dependency oracle with the results
508+
let dependencyGraph = try gatherModuleDependencies()
509+
var jobs: [Job] = []
510+
511+
// Create directories for each Swift module
512+
try inputMap.forEach {
513+
assert(!$0.value.isEmpty)
514+
try localFileSystem.createDirectory(prebuiltModuleDir
515+
.appending(RelativePath($0.key + ".swiftmodule")))
516+
}
517+
518+
let outputMap: [String: [TypedVirtualPath]] =
519+
Dictionary.init(uniqueKeysWithValues: inputMap.map { key, value in
520+
let outputPaths: [TypedVirtualPath] = value.map {
521+
let path = prebuiltModuleDir.appending(RelativePath(key + ".swiftmodule"))
522+
.appending(RelativePath($0.file.basenameWithoutExt + ".swiftmodule"))
523+
return TypedVirtualPath(file: VirtualPath.absolute(path).intern(),
524+
type: .swiftModule)
525+
}
526+
return (key, outputPaths)
527+
})
528+
529+
func isIosMac(_ path: TypedVirtualPath) -> Bool {
530+
return path.file.basenameWithoutExt.contains("macabi")
531+
}
532+
533+
func getDependenciesPaths(_ module: String, _ iosMac: Bool) throws -> [TypedVirtualPath] {
534+
var results: [TypedVirtualPath] = []
535+
let info = dependencyGraph.modules[.swift(module)]!
536+
guard let dependencies = info.directDependencies else {
537+
return results
538+
}
539+
540+
for dep in dependencies {
541+
if case let .swift(moduleName) = dep {
542+
if let outputs = outputMap[moduleName] {
543+
results.append(contentsOf: outputs.filter { isIosMac($0) == iosMac })
544+
}
545+
}
546+
}
547+
return results
548+
}
549+
550+
let iosMacFrameworks = sdkPath
551+
.appending(component: "System")
552+
.appending(component: "iOSSupport")
553+
.appending(component: "System")
554+
.appending(component: "Library")
555+
.appending(component: "Frameworks")
556+
let moduleInfo = dependencyGraph.mainModule
557+
if let dependencies = moduleInfo.directDependencies {
558+
for dep in dependencies {
559+
let moduleName = dep.moduleName
560+
if let inputPaths = inputMap[moduleName] {
561+
let outputPaths = outputMap[moduleName]!
562+
assert(inputPaths.count == outputPaths.count)
563+
assert(!inputPaths.isEmpty)
564+
for i in 0..<inputPaths.count {
565+
let inputPath = inputPaths[i]
566+
let outputPath = outputPaths[i]
567+
var commandLine: [Job.ArgTemplate] = []
568+
commandLine.appendFlag(.compileModuleFromInterface)
569+
commandLine.appendFlag(.sdk)
570+
commandLine.append(.path(sdkPath))
571+
commandLine.appendFlag(.prebuiltModuleCachePath)
572+
commandLine.appendPath(prebuiltModuleDir)
573+
commandLine.appendFlag(.moduleName)
574+
commandLine.appendFlag(moduleName)
575+
commandLine.appendFlag(.o)
576+
commandLine.appendPath(outputPath.file)
577+
commandLine.appendPath(inputPath.file)
578+
if moduleName == "Swift" {
579+
commandLine.appendFlag(.parseStdlib)
580+
}
581+
if isIosMac(inputPath) {
582+
commandLine.appendFlag(.Fsystem)
583+
commandLine.append(.path(iosMacFrameworks))
584+
}
585+
commandLine.appendFlag(.serializeParseableModuleInterfaceDependencyHashes)
586+
jobs.append(Job(
587+
moduleName: moduleName,
588+
kind: .compile,
589+
tool: .absolute(try toolchain.getToolPath(.swiftCompiler)),
590+
commandLine: commandLine,
591+
inputs: try getDependenciesPaths(moduleName, isIosMac(inputPath)),
592+
primaryInputs: [],
593+
outputs: [outputPath]
594+
))
595+
}
596+
}
597+
}
598+
}
599+
600+
return jobs
601+
}
602+
499603
private mutating func gatherModuleDependencies()
500604
throws -> InterModuleDependencyGraph {
501605
var dependencyGraph = try performDependencyScan()
@@ -551,6 +655,99 @@ extension Driver {
551655

552656
}
553657

658+
public struct SDKPrebuiltModuleInputsCollector {
659+
let sdkPath: AbsolutePath
660+
let nonFrameworkDirs = [RelativePath("usr/lib/swift"),
661+
RelativePath("System/iOSSupport/usr/lib/swift")]
662+
let frameworkDirs = [RelativePath("System/Library/Frameworks"),
663+
RelativePath("System/iOSSupport/System/Library/Frameworks")]
664+
let sdkInfo: DarwinToolchain.DarwinSDKInfo
665+
let diagEngine: DiagnosticsEngine
666+
public init(_ sdkPath: AbsolutePath, _ diagEngine: DiagnosticsEngine) {
667+
self.sdkPath = sdkPath
668+
self.sdkInfo = DarwinToolchain.readSDKInfo(localFileSystem,
669+
VirtualPath.absolute(sdkPath).intern())!
670+
self.diagEngine = diagEngine
671+
}
672+
673+
public var targetTriple: String {
674+
let canonicalName = sdkInfo.canonicalName
675+
func extractVersion(_ platform: String) -> Substring? {
676+
if canonicalName.starts(with: platform) {
677+
return canonicalName.suffix(from: canonicalName.index(canonicalName.startIndex,
678+
offsetBy: platform.count))
679+
}
680+
return nil
681+
}
682+
683+
if let version = extractVersion("macosx") {
684+
return "x86_64-apple-macosx\(version)"
685+
} else if let version = extractVersion("iphoneos") {
686+
return "arm64-apple-ios\(version)"
687+
} else {
688+
diagEngine.emit(error: "unhandled platform name: \(canonicalName)")
689+
return ""
690+
}
691+
}
692+
693+
public func collectSwiftInterfaceMap() throws -> [String: [TypedVirtualPath]] {
694+
var results: [String: [TypedVirtualPath]] = [:]
695+
696+
func updateResults(_ dir: AbsolutePath) throws {
697+
if !localFileSystem.exists(dir) {
698+
return
699+
}
700+
let moduleName = dir.basenameWithoutExt
701+
if results[moduleName] == nil {
702+
results[moduleName] = []
703+
}
704+
705+
try localFileSystem.getDirectoryContents(dir).forEach {
706+
let currentFile = AbsolutePath(dir, try VirtualPath(path: $0).relativePath!)
707+
if currentFile.extension == "swiftinterface" {
708+
let currentBaseName = currentFile.basenameWithoutExt
709+
let interfacePath = TypedVirtualPath(file: VirtualPath.absolute(currentFile).intern(),
710+
type: .swiftInterface)
711+
if !results[moduleName]!.contains(where: { $0.file.basenameWithoutExt == currentBaseName }) {
712+
results[moduleName]!.append(interfacePath)
713+
}
714+
}
715+
}
716+
}
717+
for dir in frameworkDirs {
718+
let frameDir = AbsolutePath(sdkPath, dir)
719+
if !localFileSystem.exists(frameDir) {
720+
continue
721+
}
722+
try localFileSystem.getDirectoryContents(frameDir).forEach {
723+
let frameworkPath = try VirtualPath(path: $0)
724+
if frameworkPath.extension != "framework" {
725+
return
726+
}
727+
let moduleName = frameworkPath.basenameWithoutExt
728+
let swiftModulePath = frameworkPath
729+
.appending(component: "Modules")
730+
.appending(component: moduleName + ".swiftmodule").relativePath!
731+
try updateResults(AbsolutePath(frameDir, swiftModulePath))
732+
}
733+
}
734+
for dir in nonFrameworkDirs {
735+
let swiftModuleDir = AbsolutePath(sdkPath, dir)
736+
if !localFileSystem.exists(swiftModuleDir) {
737+
continue
738+
}
739+
try localFileSystem.getDirectoryContents(swiftModuleDir).forEach {
740+
let swiftModulePath = try VirtualPath(path: $0).relativePath!
741+
if swiftModulePath.extension != "swiftmodule" {
742+
return
743+
}
744+
try updateResults(AbsolutePath(swiftModuleDir, swiftModulePath))
745+
}
746+
}
747+
return results
748+
}
749+
}
750+
554751
/// MARK: Planning
555752
extension Driver {
556753
/// Create a job if needed for simple requests that can be immediately

Sources/SwiftDriver/Toolchains/DarwinToolchain.swift

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,17 @@ import SwiftOptions
227227
private enum CodingKeys: String, CodingKey {
228228
case version = "Version"
229229
case versionMap = "VersionMap"
230+
case canonicalName = "CanonicalName"
230231
}
231232

232233
struct VersionMap: Decodable {
233234
private enum CodingKeys: String, CodingKey {
234235
case macOSToCatalystMapping = "macOS_iOSMac"
235236
}
236237

237-
var macOSToCatalystMapping: [Version: Version]
238+
var macOSToCatalystMapping: [Version: Version] = [:]
238239

240+
init() {}
239241
init(from decoder: Decoder) throws {
240242
let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
241243

@@ -259,18 +261,23 @@ import SwiftOptions
259261

260262
private var version: Version
261263
private var versionMap: VersionMap
262-
264+
let canonicalName: String
263265
init(from decoder: Decoder) throws {
264266
let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
265267

266268
let versionString = try keyedContainer.decode(String.self, forKey: .version)
269+
self.canonicalName = try keyedContainer.decode(String.self, forKey: .canonicalName)
267270
guard let version = Version(potentiallyIncompleteVersionString: versionString) else {
268271
throw DecodingError.dataCorruptedError(forKey: .version,
269272
in: keyedContainer,
270273
debugDescription: "Malformed version string")
271274
}
272275
self.version = version
273-
self.versionMap = try keyedContainer.decode(VersionMap.self, forKey: .versionMap)
276+
if self.canonicalName.hasPrefix("macosx") {
277+
self.versionMap = try keyedContainer.decode(VersionMap.self, forKey: .versionMap)
278+
} else {
279+
self.versionMap = VersionMap()
280+
}
274281
}
275282

276283
func sdkVersion(for triple: Triple) -> Version {
@@ -287,16 +294,20 @@ import SwiftOptions
287294
// SDK info is computed lazily. This should not generally be accessed directly.
288295
private var _sdkInfo: DarwinSDKInfo? = nil
289296

297+
static func readSDKInfo(_ fileSystem: FileSystem, _ sdkPath: VirtualPath.Handle) -> DarwinSDKInfo? {
298+
let sdkSettingsPath = VirtualPath.lookup(sdkPath).appending(component: "SDKSettings.json")
299+
guard let contents = try? fileSystem.readFileContents(sdkSettingsPath) else { return nil }
300+
guard let sdkInfo = try? JSONDecoder().decode(DarwinSDKInfo.self,
301+
from: Data(contents.contents)) else { return nil }
302+
return sdkInfo
303+
}
304+
290305
func getTargetSDKInfo(sdkPath: VirtualPath.Handle) -> DarwinSDKInfo? {
291306
if let info = _sdkInfo {
292307
return info
293308
} else {
294-
let sdkSettingsPath = VirtualPath.lookup(sdkPath).appending(component: "SDKSettings.json")
295-
guard let contents = try? fileSystem.readFileContents(sdkSettingsPath) else { return nil }
296-
guard let sdkInfo = try? JSONDecoder().decode(DarwinSDKInfo.self,
297-
from: Data(contents.contents)) else { return nil }
298-
self._sdkInfo = sdkInfo
299-
return sdkInfo
309+
self._sdkInfo = DarwinToolchain.readSDKInfo(fileSystem, sdkPath)
310+
return self._sdkInfo
300311
}
301312
}
302313

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import SwiftDriverExecution
2+
import SwiftDriver
3+
import TSCLibc
4+
import TSCBasic
5+
import TSCUtility
6+
7+
let diagnosticsEngine = DiagnosticsEngine(handlers: [Driver.stderrDiagnosticsHandler])
8+
9+
guard let sdkPath = ProcessEnv.vars["SDKROOT"] else {
10+
diagnosticsEngine.emit(.error("need to set SDKROOT"))
11+
exit(1)
12+
}
13+
guard let swiftcPath = ProcessEnv.vars["SWIFT_EXEC"] else {
14+
diagnosticsEngine.emit(.error("need to set SWIFT_EXEC"))
15+
exit(1)
16+
}
17+
class PrebuitGenDelegate: JobExecutionDelegate {
18+
var commandMap: [Int: String] = [:]
19+
func jobStarted(job: Job, arguments: [String], pid: Int) {
20+
commandMap[pid] = arguments.reduce("") { return $0 + " " + $1 }
21+
}
22+
23+
func jobFinished(job: Job, result: ProcessResult, pid: Int) {
24+
switch result.exitStatus {
25+
case .terminated(code: let code):
26+
if code != 0 {
27+
Driver.stdErrQueue.sync {
28+
stderrStream <<< "failed: " <<< commandMap[pid]! <<< "\n"
29+
stderrStream.flush()
30+
}
31+
}
32+
case .signalled:
33+
diagnosticsEngine.emit(.remark("\(job.moduleName) interrupted"))
34+
}
35+
}
36+
37+
func jobSkipped(job: Job) {
38+
diagnosticsEngine.emit(.error("\(job.moduleName) skipped"))
39+
}
40+
}
41+
do {
42+
let processSet = ProcessSet()
43+
let collector = try SDKPrebuiltModuleInputsCollector(VirtualPath(path: sdkPath).absolutePath!, diagnosticsEngine)
44+
let inputMap = try collector.collectSwiftInterfaceMap()
45+
let allModules = inputMap.keys
46+
try withTemporaryFile(suffix: ".swift") {
47+
let tempPath = $0.path
48+
try localFileSystem.writeFileContents(tempPath, body: {
49+
for module in allModules {
50+
if module == "MediaPlayer" || module == "PhotosUI" {
51+
continue
52+
}
53+
$0 <<< "import " <<< module <<< "\n"
54+
}
55+
})
56+
print(try localFileSystem.readFileContents(tempPath))
57+
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
58+
processSet: processSet,
59+
fileSystem: localFileSystem,
60+
env: ProcessEnv.vars)
61+
print(collector.targetTriple)
62+
var driver = try Driver(args: ["swiftc",
63+
"-target", collector.targetTriple,
64+
tempPath.description,
65+
"-sdk", sdkPath],
66+
diagnosticsEngine: diagnosticsEngine,
67+
executor: executor,
68+
compilerExecutableDir: VirtualPath.init(path: swiftcPath).parentDirectory.absolutePath!)
69+
let jobs = try driver.generatePrebuitModuleGenerationJobs(inputMap,
70+
VirtualPath(path: "/tmp/t").absolutePath!)
71+
72+
print(jobs.count)
73+
try executor.execute(workload: DriverExecutorWorkload.init(jobs, nil, continueBuildingAfterErrors: true), delegate: PrebuitGenDelegate(), numParallelJobs: 10)
74+
}
75+
} catch {
76+
print("error: \(error)")
77+
exit(1)
78+
}

0 commit comments

Comments
 (0)