Skip to content

Commit 65bf915

Browse files
authored
Merge pull request #209 from polac24/add-driver-parsing
Swift-driver integration, Part II: add Swift front-end parsing stage
2 parents ee31a38 + 867bbb6 commit 65bf915

File tree

15 files changed

+1009
-12
lines changed

15 files changed

+1009
-12
lines changed

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ let package = Package(
3232
name: "xcswiftc",
3333
dependencies: ["XCRemoteCache"]
3434
),
35+
.target(
36+
name: "xcswift-frontend",
37+
dependencies: ["XCRemoteCache"]
38+
),
3539
.target(
3640
name: "xclibtoolSupport",
3741
dependencies: ["XCRemoteCache"]
@@ -69,6 +73,7 @@ let package = Package(
6973
dependencies: [
7074
"xcprebuild",
7175
"xcswiftc",
76+
"xcswift-frontend",
7277
"xclibtool",
7378
"xcpostbuild",
7479
"xcprepare",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ Note: This step is not required if at least one of these is true:
359359
| `custom_rewrite_envs` | A list of extra ENVs that should be used as placeholders in the dependency list. ENV rewrite process is optimistic - does nothing if an ENV is not defined in the pre/postbuild process. | `[]` | ⬜️ |
360360
| `irrelevant_dependencies_paths` | Regexes of files that should not be included in a list of dependencies. Warning! Add entries here with caution - excluding dependencies that are relevant might lead to a target overcaching. The regex can match either partially or fully the filepath, e.g. `\\.modulemap$` will exclude all `.modulemap` files. | `[]` | ⬜️ |
361361
| `gracefully_handle_missing_common_sha` | If true, do not fail `prepare` if cannot find the most recent common commits with the primary branch. That might be useful on CI, where a shallow clone is used and cloning depth is not big enough to fetch a commit from a primary branch | `false` | ⬜️ |
362+
| `enable_swift_driver_integration` | Enable experimental integration with swift driver, added in Xcode 14 | `false` | ⬜️ |
362363

363364
## Backend cache server
364365

Sources/XCRemoteCache/Commands/Prepare/Integrate/IntegrateContext.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,36 +27,44 @@ struct IntegrateContext {
2727
let configOverride: URL
2828
let fakeSrcRoot: URL
2929
let output: URL?
30+
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption
3031
}
3132

3233
extension IntegrateContext {
3334
init(
3435
input: String,
35-
repoRootPath: String,
36+
config: XCRemoteCacheConfig,
3637
mode: Mode,
37-
configOverridePath: String,
3838
env: [String: String],
3939
binariesDir: URL,
4040
fakeSrcRoot: String,
4141
outputPath: String?
4242
) throws {
4343
projectPath = URL(fileURLWithPath: input)
4444
let srcRoot = projectPath.deletingLastPathComponent()
45-
repoRoot = URL(fileURLWithPath: repoRootPath, relativeTo: srcRoot)
45+
repoRoot = URL(fileURLWithPath: config.repoRoot, relativeTo: srcRoot)
4646
self.mode = mode
47-
configOverride = URL(fileURLWithPath: configOverridePath, relativeTo: srcRoot)
47+
configOverride = URL(fileURLWithPath: config.extraConfigurationFile, relativeTo: srcRoot)
4848
output = outputPath.flatMap(URL.init(fileURLWithPath:))
4949
self.fakeSrcRoot = URL(fileURLWithPath: fakeSrcRoot)
50+
var swiftcBinaryName = "swiftc"
51+
var buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = []
52+
// Keep the legacy behaviour (supported in Xcode 14 and lower)
53+
if !config.enableSwiftDriverIntegration {
54+
buildSettingsAppenderOptions.insert(.disableSwiftDriverIntegration)
55+
swiftcBinaryName = "xcswiftc"
56+
}
5057
binaries = XCRCBinariesPaths(
5158
prepare: binariesDir.appendingPathComponent("xcprepare"),
5259
cc: binariesDir.appendingPathComponent("xccc"),
53-
swiftc: binariesDir.appendingPathComponent("xcswiftc"),
60+
swiftc: binariesDir.appendingPathComponent(swiftcBinaryName),
5461
libtool: binariesDir.appendingPathComponent("xclibtool"),
5562
lipo: binariesDir.appendingPathComponent("xclipo"),
5663
ld: binariesDir.appendingPathComponent("xcld"),
5764
ldplusplus: binariesDir.appendingPathComponent("xcldplusplus"),
5865
prebuild: binariesDir.appendingPathComponent("xcprebuild"),
5966
postbuild: binariesDir.appendingPathComponent("xcpostbuild")
6067
)
68+
self.buildSettingsAppenderOptions = buildSettingsAppenderOptions
6169
}
6270
}

Sources/XCRemoteCache/Commands/Prepare/Integrate/XCIntegrate.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,8 @@ public class XCIntegrate {
8282

8383
let context = try IntegrateContext(
8484
input: projectPath,
85-
repoRootPath: config.repoRoot,
85+
config: config,
8686
mode: mode,
87-
configOverridePath: config.extraConfigurationFile,
8887
env: env,
8988
binariesDir: binariesDir,
9089
fakeSrcRoot: fakeSrcRoot,
@@ -98,15 +97,12 @@ public class XCIntegrate {
9897
excludes: targetsExclude.integrateArrayArguments,
9998
includes: targetsInclude.integrateArrayArguments
10099
)
101-
let buildSettingsAppenderOptions: BuildSettingsIntegrateAppenderOption = [
102-
.disableSwiftDriverIntegration,
103-
]
104100
let buildSettingsAppender = XcodeProjBuildSettingsIntegrateAppender(
105101
mode: context.mode,
106102
repoRoot: context.repoRoot,
107103
fakeSrcRoot: context.fakeSrcRoot,
108104
sdksExclude: sdksExclude.integrateArrayArguments,
109-
options: buildSettingsAppenderOptions
105+
options: context.buildSettingsAppenderOptions
110106
)
111107
let lldbPatcher: LLDBInitPatcher
112108
switch lldbMode {
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Copyright (c) 2023 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import Foundation
21+
22+
enum SwiftFrontendArgInputError: Error, Equatable {
23+
// swift-frontend should either be compling or emiting a module
24+
case bothCompilationAndEmitAction
25+
// no .swift files have been passed as input files
26+
case noCompilationInputs
27+
// no -primary-file .swift files have been passed as input files
28+
case noPrimaryFileCompilationInputs
29+
// number of -emit-dependencies-path doesn't match compilation inputs
30+
case dependenciesOuputCountDoesntMatch(expected: Int, parsed: Int)
31+
// number of -serialize-diagnostics-path doesn't match compilation inputs
32+
case diagnosticsOuputCountDoesntMatch(expected: Int, parsed: Int)
33+
// number of -o doesn't match compilation inputs
34+
case outputsOuputCountDoesntMatch(expected: Int, parsed: Int)
35+
// number of -o for emit-module can be only 1
36+
case emitModulOuputCountIsNot1(parsed: Int)
37+
// number of -emit-dependencies-path for emit-module can be 0 or 1 (generate or not)
38+
case emitModuleDependenciesOuputCountIsHigherThan1(parsed: Int)
39+
// number of -serialize-diagnostics-path for emit-module can be 0 or 1 (generate or not)
40+
case emitModuleDiagnosticsOuputCountIsHigherThan1(parsed: Int)
41+
// emit-module requires -emit-objc-header-path
42+
case emitModuleMissingObjcHeaderPath
43+
// -target is required
44+
case emitMissingTarget
45+
// -moduleName is required
46+
case emitMissingModuleName
47+
}
48+
49+
public struct SwiftFrontendArgInput {
50+
let compile: Bool
51+
let emitModule: Bool
52+
let objcHeaderOutput: String?
53+
let moduleName: String?
54+
let target: String?
55+
let primaryInputPaths: [String]
56+
let inputPaths: [String]
57+
var outputPaths: [String]
58+
var dependenciesPaths: [String]
59+
// Extra params
60+
// Diagnostics are not supported yet in the XCRemoteCache (cached artifacts assumes no warnings)
61+
var diagnosticsPaths: [String]
62+
// Unsed for now:
63+
// .swiftsourceinfo and .swiftdoc will be placed next to the .swiftmodule
64+
let sourceInfoPath: String?
65+
let docPath: String?
66+
// Passed as -supplementary-output-file-map
67+
let supplementaryOutputFileMap: String?
68+
69+
/// Manual initializer implementation required to be public
70+
public init(
71+
compile: Bool,
72+
emitModule: Bool,
73+
objcHeaderOutput: String?,
74+
moduleName: String?,
75+
target: String?,
76+
primaryInputPaths: [String],
77+
inputPaths: [String],
78+
outputPaths: [String],
79+
dependenciesPaths: [String],
80+
diagnosticsPaths: [String],
81+
sourceInfoPath: String?,
82+
docPath: String?,
83+
supplementaryOutputFileMap: String?
84+
) {
85+
self.compile = compile
86+
self.emitModule = emitModule
87+
self.objcHeaderOutput = objcHeaderOutput
88+
self.moduleName = moduleName
89+
self.target = target
90+
self.primaryInputPaths = primaryInputPaths
91+
self.inputPaths = inputPaths
92+
self.outputPaths = outputPaths
93+
self.dependenciesPaths = dependenciesPaths
94+
self.diagnosticsPaths = diagnosticsPaths
95+
self.sourceInfoPath = sourceInfoPath
96+
self.docPath = docPath
97+
self.supplementaryOutputFileMap = supplementaryOutputFileMap
98+
}
99+
100+
private func generateForCompilation(
101+
config: XCRemoteCacheConfig,
102+
target: String,
103+
moduleName: String
104+
) throws -> SwiftcContext {
105+
let primaryInputsCount = primaryInputPaths.count
106+
107+
guard primaryInputsCount > 0 else {
108+
throw SwiftFrontendArgInputError.noPrimaryFileCompilationInputs
109+
}
110+
guard [primaryInputsCount, 0].contains(dependenciesPaths.count) else {
111+
throw SwiftFrontendArgInputError.dependenciesOuputCountDoesntMatch(
112+
expected: primaryInputsCount,
113+
parsed: dependenciesPaths.count
114+
)
115+
}
116+
guard [primaryInputsCount, 0].contains(diagnosticsPaths.count) else {
117+
throw SwiftFrontendArgInputError.diagnosticsOuputCountDoesntMatch(
118+
expected: primaryInputsCount,
119+
parsed: diagnosticsPaths.count
120+
)
121+
}
122+
guard outputPaths.count == primaryInputsCount else {
123+
throw SwiftFrontendArgInputError.outputsOuputCountDoesntMatch(
124+
expected: primaryInputsCount,
125+
parsed: outputPaths.count
126+
)
127+
}
128+
let primaryInputFilesURLs: [URL] = primaryInputPaths.map(URL.init(fileURLWithPath:))
129+
130+
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
131+
compileFilesScope: .subset(primaryInputFilesURLs),
132+
emitModule: nil
133+
)
134+
135+
let compilationFilesInputs = buildCompilationFilesInputs(
136+
primaryInputsCount: primaryInputsCount,
137+
primaryInputFilesURLs: primaryInputFilesURLs
138+
)
139+
140+
return try .init(
141+
config: config,
142+
moduleName: moduleName,
143+
steps: steps,
144+
inputs: compilationFilesInputs,
145+
target: target,
146+
compilationFiles: .list(inputPaths),
147+
exampleWorkspaceFilePath: outputPaths[0]
148+
)
149+
}
150+
151+
private func buildCompilationFilesInputs(
152+
primaryInputsCount: Int,
153+
primaryInputFilesURLs: [URL]
154+
) -> SwiftcContext.CompilationFilesInputs {
155+
if let compimentaryFileMa = supplementaryOutputFileMap {
156+
return .supplementaryFileMap(compimentaryFileMa)
157+
} else {
158+
return .map((0..<primaryInputsCount).reduce(
159+
[String: SwiftFileCompilationInfo]()
160+
) { prev, i in
161+
var new = prev
162+
new[primaryInputPaths[i]] = SwiftFileCompilationInfo(
163+
file: primaryInputFilesURLs[i],
164+
dependencies: dependenciesPaths.get(i).map(URL.init(fileURLWithPath:)),
165+
object: outputPaths.get(i).map(URL.init(fileURLWithPath:)),
166+
// for now - swift-dependencies are not requested in the driver compilation mode
167+
swiftDependencies: nil
168+
)
169+
return new
170+
})
171+
}
172+
}
173+
174+
private func generateForEmitModule(
175+
config: XCRemoteCacheConfig,
176+
target: String,
177+
moduleName: String
178+
) throws -> SwiftcContext {
179+
guard outputPaths.count == 1 else {
180+
throw SwiftFrontendArgInputError.emitModulOuputCountIsNot1(parsed: outputPaths.count)
181+
}
182+
guard let objcHeaderOutput = objcHeaderOutput else {
183+
throw SwiftFrontendArgInputError.emitModuleMissingObjcHeaderPath
184+
}
185+
guard diagnosticsPaths.count <= 1 else {
186+
throw SwiftFrontendArgInputError.emitModuleDiagnosticsOuputCountIsHigherThan1(
187+
parsed: diagnosticsPaths.count
188+
)
189+
}
190+
guard dependenciesPaths.count <= 1 else {
191+
throw SwiftFrontendArgInputError.emitModuleDependenciesOuputCountIsHigherThan1(
192+
parsed: dependenciesPaths.count
193+
)
194+
}
195+
196+
let steps: SwiftcContext.SwiftcSteps = SwiftcContext.SwiftcSteps(
197+
compileFilesScope: .none,
198+
emitModule: SwiftcContext.SwiftcStepEmitModule(
199+
objcHeaderOutput: URL(fileURLWithPath: objcHeaderOutput),
200+
modulePathOutput: URL(fileURLWithPath: outputPaths[0]),
201+
dependencies: dependenciesPaths.first.map(URL.init(fileURLWithPath:))
202+
)
203+
)
204+
return try .init(
205+
config: config,
206+
moduleName: moduleName,
207+
steps: steps,
208+
inputs: .map([:]),
209+
target: target,
210+
compilationFiles: .list(inputPaths),
211+
exampleWorkspaceFilePath: objcHeaderOutput
212+
)
213+
}
214+
215+
func generateSwiftcContext(config: XCRemoteCacheConfig) throws -> SwiftcContext {
216+
guard compile != emitModule else {
217+
throw SwiftFrontendArgInputError.bothCompilationAndEmitAction
218+
}
219+
let inputPathsCount = inputPaths.count
220+
guard inputPathsCount > 0 else {
221+
throw SwiftFrontendArgInputError.noCompilationInputs
222+
}
223+
guard let target = target else {
224+
throw SwiftFrontendArgInputError.emitMissingTarget
225+
}
226+
guard let moduleName = moduleName else {
227+
throw SwiftFrontendArgInputError.emitMissingModuleName
228+
}
229+
230+
if compile {
231+
return try generateForCompilation(
232+
config: config,
233+
target: target,
234+
moduleName: moduleName
235+
)
236+
} else {
237+
return try generateForEmitModule(
238+
config: config,
239+
target: target,
240+
moduleName: moduleName
241+
)
242+
}
243+
}
244+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) 2023 Spotify AB.
2+
//
3+
// Licensed to the Apache Software Foundation (ASF) under one
4+
// or more contributor license agreements. See the NOTICE file
5+
// distributed with this work for additional information
6+
// regarding copyright ownership. The ASF licenses this file
7+
// to you under the Apache License, Version 2.0 (the
8+
// "License"); you may not use this file except in compliance
9+
// with the License. You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing,
14+
// software distributed under the License is distributed on an
15+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
// KIND, either express or implied. See the License for the
17+
// specific language governing permissions and limitations
18+
// under the License.
19+
20+
import Foundation
21+
22+
/// Manages the `swift-frontend` logic
23+
protocol SwiftFrontendOrchestrator {
24+
/// Executes the criticial secion according to the required order
25+
/// - Parameter criticalSection: the block that should be synchronized
26+
func run(criticalSection: () -> Void ) throws
27+
}
28+
29+
/// The default orchestrator that manages the order or swift-frontend invocations
30+
/// For emit-module (the "first" process) action, it locks a shared file between all swift-frontend invcations,
31+
/// verifies that the mocking can be done and continues the mocking/fallbacking along the lock release
32+
/// For the compilation action, tries to ackquire a lock and waits until the "emit-module" makes a decision
33+
/// if the compilation should be skipped and a "mocking" should used instead
34+
class CommonSwiftFrontendOrchestrator {
35+
private let mode: SwiftcContext.SwiftcMode
36+
37+
init(mode: SwiftcContext.SwiftcMode) {
38+
self.mode = mode
39+
}
40+
41+
func run(criticalSection: () throws -> Void) throws {
42+
// TODO: implement synchronization in a separate PR
43+
try criticalSection()
44+
}
45+
}

0 commit comments

Comments
 (0)