Skip to content

Commit 903ae6a

Browse files
authored
Merge pull request #579 from nkcsgexi/prebuilt-module-gen
Implement swift_build_sdk_interfaces.py using libSwiftDriver
2 parents 0606137 + 037aede commit 903ae6a

25 files changed

+659
-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/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ add_library(SwiftDriver
8080
Jobs/VerifyDebugInfoJob.swift
8181
Jobs/VerifyModuleInterfaceJob.swift
8282
Jobs/WebAssemblyToolchain+LinkerSupport.swift
83+
Jobs/PrebuiltModulesJob.swift
8384

8485
Toolchains/DarwinToolchain.swift
8586
Toolchains/GenericUnixToolchain.swift

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,23 @@ public struct Driver {
312312
/// A global queue for emitting non-interrupted messages into stderr
313313
public static let stdErrQueue = DispatchQueue(label: "org.swift.driver.emit-to-stderr")
314314

315+
316+
lazy var sdkPath: VirtualPath? = {
317+
guard let rawSdkPath = frontendTargetInfo.sdkPath?.path else {
318+
return nil
319+
}
320+
return VirtualPath.lookup(rawSdkPath)
321+
} ()
322+
323+
lazy var iosMacFrameworksSearchPath: VirtualPath = {
324+
sdkPath!
325+
.appending(component: "System")
326+
.appending(component: "iOSSupport")
327+
.appending(component: "System")
328+
.appending(component: "Library")
329+
.appending(component: "Frameworks")
330+
} ()
331+
315332
/// Handler for emitting diagnostics to stderr.
316333
public static let stderrDiagnosticsHandler: DiagnosticsEngine.DiagnosticsHandler = { diagnostic in
317334
stdErrQueue.sync {

Sources/SwiftDriver/Jobs/Planning.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ extension Driver {
496496
return try explicitDependencyBuildPlanner!.generateExplicitModuleDependenciesBuildJobs()
497497
}
498498

499-
private mutating func gatherModuleDependencies()
499+
mutating func gatherModuleDependencies()
500500
throws -> InterModuleDependencyGraph {
501501
var dependencyGraph = try performDependencyScan()
502502

@@ -551,6 +551,7 @@ extension Driver {
551551

552552
}
553553

554+
554555
/// MARK: Planning
555556
extension Driver {
556557
/// Create a job if needed for simple requests that can be immediately
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
//===----- PrebuiltModulesJob.swift - Swit prebuilt module Planning -------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 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 SwiftOptions
14+
15+
public struct PrebuiltModuleInput {
16+
// The path to the input/output of the a module building task.
17+
let path: TypedVirtualPath
18+
// The arch infered from the file name.
19+
let arch: Triple.Arch
20+
init(_ path: TypedVirtualPath) {
21+
let baseName = path.file.basename
22+
let arch = baseName.prefix(upTo: baseName.firstIndex(where: { $0 == "-" || $0 == "." })!)
23+
self.init(path, Triple.Arch.parse(arch)!)
24+
}
25+
init(_ path: TypedVirtualPath, _ arch: Triple.Arch) {
26+
self.path = path
27+
self.arch = arch
28+
}
29+
}
30+
31+
typealias PrebuiltModuleOutput = PrebuiltModuleInput
32+
33+
public struct SDKPrebuiltModuleInputsCollector {
34+
let sdkPath: AbsolutePath
35+
let nonFrameworkDirs = [RelativePath("usr/lib/swift"),
36+
RelativePath("System/iOSSupport/usr/lib/swift")]
37+
let frameworkDirs = [RelativePath("System/Library/Frameworks"),
38+
RelativePath("System/iOSSupport/System/Library/Frameworks")]
39+
let sdkInfo: DarwinToolchain.DarwinSDKInfo
40+
let diagEngine: DiagnosticsEngine
41+
public init(_ sdkPath: AbsolutePath, _ diagEngine: DiagnosticsEngine) {
42+
self.sdkPath = sdkPath
43+
self.sdkInfo = DarwinToolchain.readSDKInfo(localFileSystem,
44+
VirtualPath.absolute(sdkPath).intern())!
45+
self.diagEngine = diagEngine
46+
}
47+
48+
public var versionString: String {
49+
return sdkInfo.versionString
50+
}
51+
52+
// Returns a target triple that's proper to use with the given SDK path.
53+
public var targetTriple: String {
54+
let canonicalName = sdkInfo.canonicalName
55+
func extractVersion(_ platform: String) -> Substring? {
56+
if canonicalName.starts(with: platform) {
57+
return canonicalName.suffix(from: canonicalName.index(canonicalName.startIndex,
58+
offsetBy: platform.count))
59+
}
60+
return nil
61+
}
62+
63+
if let version = extractVersion("macosx") {
64+
return "arm64-apple-macosx\(version)"
65+
} else if let version = extractVersion("iphoneos") {
66+
return "arm64-apple-ios\(version)"
67+
} else if let version = extractVersion("iphonesimulator") {
68+
return "arm64-apple-ios\(version)-simulator"
69+
} else if let version = extractVersion("watchos") {
70+
return "armv7k-apple-watchos\(version)"
71+
} else if let version = extractVersion("watchsimulator") {
72+
return "arm64-apple-watchos\(version)-simulator"
73+
} else if let version = extractVersion("appletvos") {
74+
return "arm64-apple-tvos\(version)"
75+
} else if let version = extractVersion("appletvsimulator") {
76+
return "arm64-apple-tvos\(version)-simulator"
77+
} else {
78+
diagEngine.emit(error: "unhandled platform name: \(canonicalName)")
79+
return ""
80+
}
81+
}
82+
83+
private func sanitizeInterfaceMap(_ map: [String: [PrebuiltModuleInput]]) -> [String: [PrebuiltModuleInput]] {
84+
return map.filter {
85+
// Remove modules without associated .swiftinterface files and diagnose.
86+
if $0.value.isEmpty {
87+
diagEngine.emit(warning: "\($0.key) has no associated .swiftinterface files")
88+
return false
89+
}
90+
return true
91+
}
92+
}
93+
94+
public func collectSwiftInterfaceMap() throws -> [String: [PrebuiltModuleInput]] {
95+
var results: [String: [PrebuiltModuleInput]] = [:]
96+
97+
func updateResults(_ dir: AbsolutePath) throws {
98+
if !localFileSystem.exists(dir) {
99+
return
100+
}
101+
let moduleName = dir.basenameWithoutExt
102+
if results[moduleName] == nil {
103+
results[moduleName] = []
104+
}
105+
106+
// Search inside a .swiftmodule directory for any .swiftinterface file, and
107+
// add the files into the dictionary.
108+
// Duplicate entries are discarded, otherwise llbuild will complain.
109+
try localFileSystem.getDirectoryContents(dir).forEach {
110+
let currentFile = AbsolutePath(dir, try VirtualPath(path: $0).relativePath!)
111+
if currentFile.extension == "swiftinterface" {
112+
let currentBaseName = currentFile.basenameWithoutExt
113+
let interfacePath = TypedVirtualPath(file: VirtualPath.absolute(currentFile).intern(),
114+
type: .swiftInterface)
115+
if !results[moduleName]!.contains(where: { $0.path.file.basenameWithoutExt == currentBaseName }) {
116+
results[moduleName]!.append(PrebuiltModuleInput(interfacePath))
117+
}
118+
}
119+
if currentFile.extension == "swiftmodule" {
120+
diagEngine.emit(warning: "found \(currentFile)")
121+
}
122+
}
123+
}
124+
// Search inside framework dirs in an SDK to find .swiftmodule directories.
125+
for dir in frameworkDirs {
126+
let frameDir = AbsolutePath(sdkPath, dir)
127+
if !localFileSystem.exists(frameDir) {
128+
continue
129+
}
130+
try localFileSystem.getDirectoryContents(frameDir).forEach {
131+
let frameworkPath = try VirtualPath(path: $0)
132+
if frameworkPath.extension != "framework" {
133+
return
134+
}
135+
let moduleName = frameworkPath.basenameWithoutExt
136+
let swiftModulePath = frameworkPath
137+
.appending(component: "Modules")
138+
.appending(component: moduleName + ".swiftmodule").relativePath!
139+
try updateResults(AbsolutePath(frameDir, swiftModulePath))
140+
}
141+
}
142+
// Search inside lib dirs in an SDK to find .swiftmodule directories.
143+
for dir in nonFrameworkDirs {
144+
let swiftModuleDir = AbsolutePath(sdkPath, dir)
145+
if !localFileSystem.exists(swiftModuleDir) {
146+
continue
147+
}
148+
try localFileSystem.getDirectoryContents(swiftModuleDir).forEach {
149+
if $0.hasSuffix(".swiftmodule") {
150+
try updateResults(AbsolutePath(swiftModuleDir, $0))
151+
}
152+
}
153+
}
154+
return sanitizeInterfaceMap(results)
155+
}
156+
}
157+
158+
extension Driver {
159+
160+
private mutating func generateSingleModuleBuildingJob(_ moduleName: String, _ prebuiltModuleDir: AbsolutePath,
161+
_ inputPath: PrebuiltModuleInput, _ outputPath: PrebuiltModuleOutput,
162+
_ dependencies: [TypedVirtualPath]) throws -> Job {
163+
assert(inputPath.path.file.basenameWithoutExt == outputPath.path.file.basenameWithoutExt)
164+
func isIosMac(_ path: TypedVirtualPath) -> Bool {
165+
// Infer macabi interfaces by the file name.
166+
// FIXME: more robust way to do this.
167+
return path.file.basenameWithoutExt.contains("macabi")
168+
}
169+
var commandLine: [Job.ArgTemplate] = []
170+
commandLine.appendFlag(.compileModuleFromInterface)
171+
commandLine.appendFlag(.sdk)
172+
commandLine.append(.path(sdkPath!))
173+
commandLine.appendFlag(.prebuiltModuleCachePath)
174+
commandLine.appendPath(prebuiltModuleDir)
175+
commandLine.appendFlag(.moduleName)
176+
commandLine.appendFlag(moduleName)
177+
commandLine.appendFlag(.o)
178+
commandLine.appendPath(outputPath.path.file)
179+
commandLine.appendPath(inputPath.path.file)
180+
if moduleName == "Swift" {
181+
commandLine.appendFlag(.parseStdlib)
182+
}
183+
// Add macabi-specific search path.
184+
if isIosMac(inputPath.path) {
185+
commandLine.appendFlag(.Fsystem)
186+
commandLine.append(.path(iosMacFrameworksSearchPath))
187+
}
188+
commandLine.appendFlag(.serializeParseableModuleInterfaceDependencyHashes)
189+
return Job(
190+
moduleName: moduleName,
191+
kind: .compile,
192+
tool: .absolute(try toolchain.getToolPath(.swiftCompiler)),
193+
commandLine: commandLine,
194+
inputs: dependencies,
195+
primaryInputs: [],
196+
outputs: [outputPath.path]
197+
)
198+
}
199+
200+
public mutating func generatePrebuitModuleGenerationJobs(_ inputMap: [String: [PrebuiltModuleInput]],
201+
_ prebuiltModuleDir: AbsolutePath) throws -> ([Job], [Job]) {
202+
assert(sdkPath != nil)
203+
// Run the dependency scanner and update the dependency oracle with the results
204+
let dependencyGraph = try gatherModuleDependencies()
205+
var jobs: [Job] = []
206+
var danglingJobs: [Job] = []
207+
var inputCount = 0
208+
// Create directories for each Swift module
209+
try inputMap.forEach {
210+
assert(!$0.value.isEmpty)
211+
try localFileSystem.createDirectory(prebuiltModuleDir
212+
.appending(RelativePath($0.key + ".swiftmodule")))
213+
}
214+
215+
// Generate an outputMap from the inputMap for easy reference.
216+
let outputMap: [String: [PrebuiltModuleOutput]] =
217+
Dictionary.init(uniqueKeysWithValues: inputMap.map { key, value in
218+
let outputPaths: [PrebuiltModuleInput] = value.map {
219+
let path = prebuiltModuleDir.appending(RelativePath(key + ".swiftmodule"))
220+
.appending(RelativePath($0.path.file.basenameWithoutExt + ".swiftmodule"))
221+
return PrebuiltModuleOutput(TypedVirtualPath(file: VirtualPath.absolute(path).intern(),
222+
type: .swiftModule), $0.arch)
223+
}
224+
inputCount += outputPaths.count
225+
return (key, outputPaths)
226+
})
227+
228+
func getDependenciesPaths(_ module: String, _ arch: Triple.Arch) throws -> [TypedVirtualPath] {
229+
var results: [TypedVirtualPath] = []
230+
let info = dependencyGraph.modules[.swift(module)]!
231+
guard let dependencies = info.directDependencies else {
232+
return results
233+
}
234+
235+
for dep in dependencies {
236+
if case let .swift(moduleName) = dep {
237+
if let outputs = outputMap[moduleName] {
238+
// Depending only those .swiftmodule files with the same arch kind.
239+
// FIXME: handling arm64 and arm64e specifically.
240+
let selectOutputs = outputs.filter ({ $0.arch == arch }).map { $0.path }
241+
results.append(contentsOf: selectOutputs)
242+
}
243+
}
244+
}
245+
return results
246+
}
247+
248+
func forEachInputOutputPair(_ moduleName: String,
249+
_ action: (PrebuiltModuleInput, PrebuiltModuleOutput) throws -> ()) throws {
250+
if let inputPaths = inputMap[moduleName] {
251+
let outputPaths = outputMap[moduleName]!
252+
assert(inputPaths.count == outputPaths.count)
253+
assert(!inputPaths.isEmpty)
254+
for i in 0..<inputPaths.count {
255+
try action(inputPaths[i], outputPaths[i])
256+
}
257+
}
258+
}
259+
// Keep track of modules that are not handled.
260+
var unhandledModules = Set<String>(inputMap.keys)
261+
let moduleInfo = dependencyGraph.mainModule
262+
if let dependencies = moduleInfo.directDependencies {
263+
for dep in dependencies {
264+
switch dep {
265+
case .swift(let moduleName):
266+
// Removed moduleName from the list.
267+
unhandledModules.remove(moduleName)
268+
try forEachInputOutputPair(moduleName) {
269+
jobs.append(try generateSingleModuleBuildingJob(moduleName,
270+
prebuiltModuleDir, $0, $1,
271+
try getDependenciesPaths(moduleName, $0.arch)))
272+
}
273+
default:
274+
continue
275+
}
276+
}
277+
}
278+
279+
// For each unhandled module, generate dangling jobs for each associated
280+
// interfaces.
281+
// The only known usage of this so for is in macosx SDK where some collected
282+
// modules are only for macabi. The file under scanning is using a target triple
283+
// of mac native so those macabi-only modules cannot be found by the scanner.
284+
// We have to handle those modules separately without any dependency info.
285+
try unhandledModules.forEach { moduleName in
286+
diagnosticEngine.emit(warning: "handle \(moduleName) as dangling jobs")
287+
try forEachInputOutputPair(moduleName) { input, output in
288+
danglingJobs.append(try generateSingleModuleBuildingJob(moduleName,
289+
prebuiltModuleDir, input, output, []))
290+
}
291+
}
292+
293+
// check we've generated jobs for all inputs
294+
assert(inputCount == jobs.count + danglingJobs.count)
295+
return (jobs, danglingJobs)
296+
}
297+
}

0 commit comments

Comments
 (0)