forked from tuist/tuist
-
Notifications
You must be signed in to change notification settings - Fork 0
/
SchemesGenerator.swift
483 lines (412 loc) 路 23.3 KB
/
SchemesGenerator.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
import Basic
import Foundation
import TuistSupport
import XcodeProj
/// Protocol that defines the interface of the schemes generation.
protocol SchemesGenerating {
/// Generates the schemes for the project targets.
///
/// - Parameters:
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Throws: A FatalError if the generation of the schemes fails.
func generateTargetSchemes(project: Project,
generatedProject: GeneratedProject) throws
}
// swiftlint:disable:next type_body_length
final class SchemesGenerator: SchemesGenerating {
/// Default last upgrade version for generated schemes.
private static let defaultLastUpgradeVersion = "1010"
/// Default version for generated schemes.
private static let defaultVersion = "1.3"
/// Generates the schemes for the project manifest.
///
/// - Parameters:
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Throws: A FatalError if the generation of the schemes fails.
func generateTargetSchemes(project: Project, generatedProject: GeneratedProject) throws {
/// Generate scheme from manifest
try project.schemes.forEach { scheme in
try generateScheme(scheme: scheme, project: project, generatedProject: generatedProject)
}
/// Generate scheme for every targets in Project that is not defined in Manifest
let buildConfiguration = defaultDebugBuildConfigurationName(in: project)
try project.targets.forEach { target in
if !project.schemes.contains(where: { $0.name == target.name }) {
let scheme = Scheme(name: target.name,
shared: true,
buildAction: BuildAction(targets: [target.name]),
testAction: TestAction(targets: [target.name], configurationName: buildConfiguration),
runAction: RunAction(configurationName: buildConfiguration,
executable: target.productName,
arguments: Arguments(environment: target.environment)))
try generateScheme(scheme: scheme,
project: project,
generatedProject: generatedProject)
}
}
}
/// Generates the scheme.
///
/// - Parameters:
/// - scheme: Scheme manifest.
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Throws: An error if the generation fails.
func generateScheme(scheme: Scheme,
project: Project,
generatedProject: GeneratedProject) throws {
let schemesDirectory = try createSchemesDirectory(projectPath: generatedProject.path, shared: scheme.shared)
let schemePath = schemesDirectory.appending(component: "\(scheme.name).xcscheme")
let generatedBuildAction = schemeBuildAction(scheme: scheme, project: project, generatedProject: generatedProject)
let generatedTestAction = schemeTestAction(scheme: scheme, project: project, generatedProject: generatedProject)
let generatedLaunchAction = schemeLaunchAction(scheme: scheme, project: project, generatedProject: generatedProject)
let generatedProfileAction = schemeProfileAction(scheme: scheme, project: project, generatedProject: generatedProject)
let scheme = XCScheme(name: scheme.name,
lastUpgradeVersion: SchemesGenerator.defaultLastUpgradeVersion,
version: SchemesGenerator.defaultVersion,
buildAction: generatedBuildAction,
testAction: generatedTestAction,
launchAction: generatedLaunchAction,
profileAction: generatedProfileAction,
analyzeAction: schemeAnalyzeAction(for: project),
archiveAction: schemeArchiveAction(for: project))
try scheme.write(path: schemePath.path, override: true)
}
/// Returns the build action for the project scheme.
///
/// - Parameters:
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - graph: Dependencies graph.
/// - Returns: Scheme build action.
func projectBuildAction(project: Project,
generatedProject: GeneratedProject,
graph: Graphing) -> XCScheme.BuildAction {
let targets = project.sortedTargetsForProjectScheme(graph: graph)
let entries: [XCScheme.BuildAction.Entry] = targets.map { (target) -> XCScheme.BuildAction.Entry in
let pbxTarget = generatedProject.targets[target.name]!
let buildableReference = targetBuildableReference(target: target,
pbxTarget: pbxTarget,
projectName: generatedProject.name)
var buildFor: [XCScheme.BuildAction.Entry.BuildFor] = []
if target.product.testsBundle {
buildFor.append(.testing)
} else {
buildFor.append(contentsOf: [.analyzing, .archiving, .profiling, .running, .testing])
}
return XCScheme.BuildAction.Entry(buildableReference: buildableReference,
buildFor: buildFor)
}
return XCScheme.BuildAction(buildActionEntries: entries,
parallelizeBuild: true,
buildImplicitDependencies: true)
}
/// Generates the test action for the project scheme.
///
/// - Parameters:
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Returns: Scheme test action.
func projectTestAction(project: Project,
generatedProject: GeneratedProject) -> XCScheme.TestAction {
var testables: [XCScheme.TestableReference] = []
let testTargets = project.targets.filter { $0.product.testsBundle }
testTargets.forEach { target in
let pbxTarget = generatedProject.targets[target.name]!
let reference = targetBuildableReference(target: target,
pbxTarget: pbxTarget,
projectName: generatedProject.name)
let testable = XCScheme.TestableReference(skipped: false,
buildableReference: reference)
testables.append(testable)
}
let buildConfiguration = defaultDebugBuildConfigurationName(in: project)
return XCScheme.TestAction(buildConfiguration: buildConfiguration,
macroExpansion: nil,
testables: testables)
}
/// Generates the array of BuildableReference for targets that the
/// coverage report should be generated for them.
///
/// - Parameters:
/// - testAction: test actions.
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Returns: Array of buildable references.
private func testCoverageTargetReferences(testAction: TestAction, project: Project, generatedProject: GeneratedProject) -> [XCScheme.BuildableReference] {
var codeCoverageTargets: [XCScheme.BuildableReference] = []
testAction.codeCoverageTargets.forEach { name in
guard let target = project.targets.first(where: { $0.name == name }) else { return }
guard let pbxTarget = generatedProject.targets[name] else { return }
let reference = self.targetBuildableReference(target: target,
pbxTarget: pbxTarget,
projectName: generatedProject.name)
codeCoverageTargets.append(reference)
}
return codeCoverageTargets
}
/// Generates the scheme test action.
///
/// - Parameters:
/// - scheme: Scheme manifest.
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Returns: Scheme test action.
func schemeTestAction(scheme: Scheme,
project: Project,
generatedProject: GeneratedProject) -> XCScheme.TestAction? {
guard let testAction = scheme.testAction else { return nil }
var testables: [XCScheme.TestableReference] = []
var preActions: [XCScheme.ExecutionAction] = []
var postActions: [XCScheme.ExecutionAction] = []
testAction.targets.forEach { name in
guard let target = project.targets.first(where: { $0.name == name }), target.product.testsBundle else { return }
guard let pbxTarget = generatedProject.targets[name] else { return }
let reference = self.targetBuildableReference(target: target,
pbxTarget: pbxTarget,
projectName: generatedProject.name)
let testable = XCScheme.TestableReference(skipped: false, buildableReference: reference)
testables.append(testable)
}
preActions = schemeExecutionActions(actions: testAction.preActions,
project: project,
generatedProject: generatedProject)
postActions = schemeExecutionActions(actions: testAction.postActions,
project: project,
generatedProject: generatedProject)
var args: XCScheme.CommandLineArguments?
var environments: [XCScheme.EnvironmentVariable]?
if let arguments = testAction.arguments {
args = XCScheme.CommandLineArguments(arguments: commandlineArgruments(arguments.launch))
environments = environmentVariables(arguments.environment)
}
let codeCoverageTargets = self.testCoverageTargetReferences(testAction: testAction, project: project, generatedProject: generatedProject)
let onlyGenerateCoverageForSpecifiedTargets = codeCoverageTargets.count > 0 ? true : nil
let shouldUseLaunchSchemeArgsEnv: Bool = args == nil && environments == nil
return XCScheme.TestAction(buildConfiguration: testAction.configurationName,
macroExpansion: nil,
testables: testables,
preActions: preActions,
postActions: postActions,
shouldUseLaunchSchemeArgsEnv: shouldUseLaunchSchemeArgsEnv,
codeCoverageEnabled: testAction.coverage,
codeCoverageTargets: codeCoverageTargets,
onlyGenerateCoverageForSpecifiedTargets: onlyGenerateCoverageForSpecifiedTargets,
commandlineArguments: args,
environmentVariables: environments)
}
/// Generates the scheme build action.
///
/// - Parameters:
/// - scheme: Scheme manifest.
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Returns: Scheme build action.
func schemeBuildAction(scheme: Scheme,
project: Project,
generatedProject: GeneratedProject) -> XCScheme.BuildAction? {
guard let buildAction = scheme.buildAction else { return nil }
let buildFor: [XCScheme.BuildAction.Entry.BuildFor] = [
.analyzing, .archiving, .profiling, .running, .testing,
]
var entries: [XCScheme.BuildAction.Entry] = []
var preActions: [XCScheme.ExecutionAction] = []
var postActions: [XCScheme.ExecutionAction] = []
buildAction.targets.forEach { name in
guard let target = project.targets.first(where: { $0.name == name }) else { return }
guard let pbxTarget = generatedProject.targets[name] else { return }
let buildableReference = self.targetBuildableReference(target: target,
pbxTarget: pbxTarget,
projectName: generatedProject.name)
entries.append(XCScheme.BuildAction.Entry(buildableReference: buildableReference, buildFor: buildFor))
}
preActions = schemeExecutionActions(actions: buildAction.preActions,
project: project,
generatedProject: generatedProject)
postActions = schemeExecutionActions(actions: buildAction.postActions,
project: project,
generatedProject: generatedProject)
return XCScheme.BuildAction(buildActionEntries: entries,
preActions: preActions,
postActions: postActions,
parallelizeBuild: true,
buildImplicitDependencies: true)
}
/// Generates the scheme launch action.
///
/// - Parameters:
/// - scheme: Scheme manifest.
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Returns: Scheme launch action.
func schemeLaunchAction(scheme: Scheme,
project: Project,
generatedProject: GeneratedProject) -> XCScheme.LaunchAction? {
guard var target = project.targets.first(where: { $0.name == scheme.buildAction?.targets.first }) else { return nil }
if let executable = scheme.runAction?.executable {
guard let runableTarget = project.targets.first(where: { $0.name == executable }) else { return nil }
target = runableTarget
}
guard let pbxTarget = generatedProject.targets[target.name] else { return nil }
var buildableProductRunnable: XCScheme.BuildableProductRunnable?
var macroExpansion: XCScheme.BuildableReference?
let buildableReference = targetBuildableReference(target: target, pbxTarget: pbxTarget, projectName: generatedProject.name)
if target.product.runnable {
buildableProductRunnable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference, runnableDebuggingMode: "0")
} else {
macroExpansion = buildableReference
}
var commandlineArguments: XCScheme.CommandLineArguments?
var environments: [XCScheme.EnvironmentVariable]?
if let arguments = scheme.runAction?.arguments {
commandlineArguments = XCScheme.CommandLineArguments(arguments: commandlineArgruments(arguments.launch))
environments = environmentVariables(arguments.environment)
}
let buildConfiguration = scheme.runAction?.configurationName ?? defaultDebugBuildConfigurationName(in: project)
return XCScheme.LaunchAction(runnable: buildableProductRunnable,
buildConfiguration: buildConfiguration,
macroExpansion: macroExpansion,
commandlineArguments: commandlineArguments,
environmentVariables: environments)
}
/// Generates the scheme profile action for a given target.
///
/// - Parameters:
/// - target: Target manifest.
/// - pbxTarget: Xcode native target.
/// - projectName: Project name with .xcodeproj extension.
/// - Returns: Scheme profile action.
func schemeProfileAction(scheme: Scheme,
project: Project,
generatedProject: GeneratedProject) -> XCScheme.ProfileAction? {
guard var target = project.targets.first(where: { $0.name == scheme.buildAction?.targets.first }) else { return nil }
if let executable = scheme.runAction?.executable {
guard let runableTarget = project.targets.first(where: { $0.name == executable }) else { return nil }
target = runableTarget
}
guard let pbxTarget = generatedProject.targets[target.name] else { return nil }
var buildableProductRunnable: XCScheme.BuildableProductRunnable?
var macroExpansion: XCScheme.BuildableReference?
let buildableReference = targetBuildableReference(target: target, pbxTarget: pbxTarget, projectName: generatedProject.name)
if target.product.runnable {
buildableProductRunnable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference, runnableDebuggingMode: "0")
} else {
macroExpansion = buildableReference
}
let buildConfiguration = defaultReleaseBuildConfigurationName(in: project)
return XCScheme.ProfileAction(buildableProductRunnable: buildableProductRunnable,
buildConfiguration: buildConfiguration,
macroExpansion: macroExpansion)
}
/// Returns the scheme pre/post actions.
///
/// - Parameters:
/// - actions: pre/post action manifest.
/// - project: Project manifest.
/// - generatedProject: Generated Xcode project.
/// - Returns: Scheme actions.
func schemeExecutionActions(actions: [ExecutionAction],
project: Project,
generatedProject: GeneratedProject) -> [XCScheme.ExecutionAction] {
/// Return Buildable Reference for Scheme Action
func schemeBuildableReference(targetName: String?, project: Project, generatedProject: GeneratedProject) -> XCScheme.BuildableReference? {
guard let targetName = targetName else { return nil }
guard let target = project.targets.first(where: { $0.name == targetName }) else { return nil }
guard let pbxTarget = generatedProject.targets[targetName] else { return nil }
return targetBuildableReference(target: target, pbxTarget: pbxTarget, projectName: generatedProject.name)
}
var schemeActions: [XCScheme.ExecutionAction] = []
actions.forEach { action in
let schemeAction = XCScheme.ExecutionAction(scriptText: action.scriptText,
title: action.title,
environmentBuildable: nil)
schemeAction.environmentBuildable = schemeBuildableReference(targetName: action.target,
project: project,
generatedProject: generatedProject)
schemeActions.append(schemeAction)
}
return schemeActions
}
/// Returns the scheme commandline argument passed on launch
///
/// - Parameters:
/// - environments: commandline argument keys.
/// - Returns: XCScheme.CommandLineArguments.CommandLineArgument.
func commandlineArgruments(_ arguments: [String: Bool]) -> [XCScheme.CommandLineArguments.CommandLineArgument] {
return arguments.map { key, enabled in
XCScheme.CommandLineArguments.CommandLineArgument(name: key, enabled: enabled)
}
}
/// Returns the scheme environment variables
///
/// - Parameters:
/// - environments: environment variables
/// - Returns: XCScheme.EnvironmentVariable.
func environmentVariables(_ environments: [String: String]) -> [XCScheme.EnvironmentVariable] {
return environments.map { key, value in
XCScheme.EnvironmentVariable(variable: key, value: value, enabled: true)
}
}
/// Returns the scheme buildable reference for a given target.
///
/// - Parameters:
/// - target: Target manifest.
/// - pbxTarget: Xcode native target.
/// - projectName: Project name with the .xcodeproj extension.
/// - Returns: Buildable reference.
func targetBuildableReference(target: Target, pbxTarget: PBXNativeTarget, projectName: String) -> XCScheme.BuildableReference {
return XCScheme.BuildableReference(referencedContainer: "container:\(projectName)",
blueprint: pbxTarget,
buildableName: target.productNameWithExtension,
blueprintName: target.name,
buildableIdentifier: "primary")
}
/// Returns the scheme analyze action
///
/// - Returns: Scheme analyze action.
func schemeAnalyzeAction(for project: Project) -> XCScheme.AnalyzeAction {
let buildConfiguration = defaultDebugBuildConfigurationName(in: project)
return XCScheme.AnalyzeAction(buildConfiguration: buildConfiguration)
}
/// Returns the scheme archive action
///
/// - Returns: Scheme archive action.
func schemeArchiveAction(for project: Project) -> XCScheme.ArchiveAction {
let buildConfiguration = defaultReleaseBuildConfigurationName(in: project)
return XCScheme.ArchiveAction(buildConfiguration: buildConfiguration,
revealArchiveInOrganizer: true)
}
/// Creates the directory where the schemes are stored inside the project.
/// If the directory exists it does not re-create it.
///
/// - Parameters:
/// - projectPath: Path to the Xcode project.
/// - shared: Scheme should be shared or not
/// - Returns: Path to the schemes directory.
/// - Throws: A FatalError if the creation of the directory fails.
private func createSchemesDirectory(projectPath: AbsolutePath, shared: Bool = true) throws -> AbsolutePath {
var path: AbsolutePath!
if shared {
path = projectPath.appending(RelativePath("xcshareddata/xcschemes"))
} else {
let username = NSUserName()
path = projectPath.appending(RelativePath("xcuserdata/\(username).xcuserdatad/xcschemes"))
}
if !FileHandler.shared.exists(path) {
try FileHandler.shared.createFolder(path)
}
return path
}
private func defaultDebugBuildConfigurationName(in project: Project) -> String {
let debugConfiguration = project.settings.defaultDebugBuildConfiguration()
let buildConfiguration = debugConfiguration ?? project.settings.configurations.keys.first
return buildConfiguration?.name ?? BuildConfiguration.debug.name
}
private func defaultReleaseBuildConfigurationName(in project: Project) -> String {
let releaseConfiguration = project.settings.defaultReleaseBuildConfiguration()
let buildConfiguration = releaseConfiguration ?? project.settings.configurations.keys.first
return buildConfiguration?.name ?? BuildConfiguration.release.name
}
}