-
Notifications
You must be signed in to change notification settings - Fork 10.5k
/
Copy pathXcodeProjectModel.swift
453 lines (408 loc) · 17.6 KB
/
XcodeProjectModel.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
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2014-2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
/*
A very simple rendition of the Xcode project model. There is only sufficient
functionality to allow creation of Xcode projects in a somewhat readable way,
and serialization to .xcodeproj plists. There is no consistency checking to
ensure, for example, that build settings have valid values, dependency cycles
are not created, etc.
Everything here is geared toward supporting project generation. The intended
usage model is for custom logic to build up a project using Xcode terminology
(e.g. "group", "reference", "target", "build phase"), but there is almost no
provision for modifying the model after it has been built up. The intent is
to create it as desired from the start.
Rather than try to represent everything that Xcode's project model supports,
the approach is to start small and to add functionality as needed.
Note that this API represents only the project model — there is no notion of
workspaces, schemes, etc (although schemes are represented individually in a
separate API). The notion of build settings is also somewhat different from
what it is in Xcode: instead of an open-ended mapping of build configuration
names to dictionaries of build settings, here there is a single set of common
build settings plus two overlay sets for debug and release. The generated
project has just the two Debug and Release configurations, created by merging
the common set into the release and debug sets. This allows a more natural
configuration of the settings, since most values are the same between Debug
and Release. Also, the build settings themselves are represented as structs
of named fields, instead of dictionaries with arbitrary name strings as keys.
It is expected that some of these simplifications will need to be lifted over
time, based on need. That should be done carefully, however, to avoid ending
up with an overly complicated model.
Some things that are incomplete in even this first model:
- copy files build phases are incomplete
- shell script build phases are incomplete
- file types in file references are specified using strings; should be enums
so that the client doesn't have to hardcode the mapping to Xcode file type
identifiers
- debug and release settings override common settings; they should be merged
in a way that respects `$(inhertied)` when the same setting is defined in
common and in debug or release
- there is no good way to control the ordering of the `Products` group in the
main group; it needs to be added last in order to appear after the other
references
*/
public struct Xcode {
/// An Xcode project, consisting of a tree of groups and file references,
/// a list of targets, and some additional information. Note that schemes
/// are outside of the project data model.
public class Project {
public let mainGroup: Group
public var buildSettings: BuildSettingsTable
public var productGroup: Group?
public var projectDir: String
public var targets: [Target]
public init() {
self.mainGroup = Group(path: "")
self.buildSettings = BuildSettingsTable()
self.productGroup = nil
self.projectDir = ""
self.targets = []
}
/// Creates and adds a new target (which does not initially have any
/// build phases).
public func addTarget(objectID: String? = nil, productType: Target.ProductType? = nil, name: String) -> Target {
let target = Target(objectID: objectID ?? "TARGET_\(name)", productType: productType, name: name)
targets.append(target)
return target
}
}
/// Abstract base class for all items in the group hierarchy.
public class Reference {
/// Relative path of the reference. It is usually a literal, but may
/// in fact contain build settings.
public var path: String
/// Determines the base path for the reference's relative path.
public var pathBase: RefPathBase
/// Name of the reference, if different from the last path component
/// (if not set, Xcode will use the last path component as the name).
public var name: String?
/// Determines the base path for a reference's relative path (this is
/// what for some reason is called a "source tree" in Xcode).
public enum RefPathBase: String {
/// An absolute path
case absolute = "<absolute>"
/// Indicates that the path is relative to the source root (i.e.
/// the "project directory").
case projectDir = "SOURCE_ROOT"
/// Indicates that the path is relative to the path of the parent
/// group.
case groupDir = "<group>"
/// Indicates that the path is relative to the effective build
/// directory (which varies depending on active scheme, active run
/// destination, or even an overridden build setting.
case buildDir = "BUILT_PRODUCTS_DIR"
}
init(path: String, pathBase: RefPathBase = .groupDir, name: String? = nil) {
self.path = path
self.pathBase = pathBase
self.name = name
}
/// Whether this is either a group or directory reference (blue folder).
public var isDirectoryLike: Bool {
if self is Xcode.Group {
return true
}
if let ref = self as? Xcode.FileReference {
return ref.isDirectory
}
return false
}
}
/// A reference to a file system entity (a file, folder, etc).
public final class FileReference: Reference {
public var objectID: String?
public var fileType: String?
public var isDirectory: Bool
public fileprivate(set) var isBuildableFolder: Bool = false
init(
path: String, isDirectory: Bool, pathBase: RefPathBase = .groupDir,
name: String? = nil, fileType: String? = nil, objectID: String? = nil
) {
self.isDirectory = isDirectory
super.init(path: path, pathBase: pathBase, name: name)
self.objectID = objectID
self.fileType = fileType
}
}
/// A group that can contain References (FileReferences and other Groups).
/// The resolved path of a group is used as the base path for any child
/// references whose source tree type is GroupRelative.
public final class Group: Reference {
public var subitems = [Reference]()
/// Creates and appends a new Group to the list of subitems.
/// The new group is returned so that it can be configured.
@discardableResult
public func addGroup(
path: String,
pathBase: RefPathBase = .groupDir,
name: String? = nil
) -> Group {
let group = Group(path: path, pathBase: pathBase, name: name)
subitems.append(group)
return group
}
/// Creates and appends a new FileReference to the list of subitems.
@discardableResult
public func addFileReference(
path: String,
isDirectory: Bool,
pathBase: RefPathBase = .groupDir,
name: String? = nil,
fileType: String? = nil,
objectID: String? = nil
) -> FileReference {
let fref = FileReference(path: path, isDirectory: isDirectory, pathBase: pathBase, name: name, fileType: fileType, objectID: objectID)
subitems.append(fref)
return fref
}
}
/// An Xcode target, representing a single entity to build.
public final class Target {
public var objectID: String?
public var name: String
public var productName: String
public var productType: ProductType?
public var buildSettings: BuildSettingsTable
public var buildPhases: [BuildPhase]
public var productReference: FileReference?
public var dependencies: [TargetDependency]
public private(set) var buildableFolders: [FileReference]
public enum ProductType: String {
case application = "com.apple.product-type.application"
case staticArchive = "com.apple.product-type.library.static"
case dynamicLibrary = "com.apple.product-type.library.dynamic"
case framework = "com.apple.product-type.framework"
case executable = "com.apple.product-type.tool"
case unitTest = "com.apple.product-type.bundle.unit-test"
}
init(objectID: String?, productType: ProductType?, name: String) {
self.objectID = objectID
self.name = name
self.productType = productType
self.productName = name
self.buildSettings = BuildSettingsTable()
self.buildPhases = []
self.dependencies = []
self.buildableFolders = []
}
// FIXME: There's a lot repetition in these methods; using generics to
// try to avoid that raised other issues in terms of requirements on
// the Reference class, though.
/// Adds a "headers" build phase, i.e. one that copies headers into a
/// directory of the product, after suitable processing.
@discardableResult
public func addHeadersBuildPhase() -> HeadersBuildPhase {
let phase = HeadersBuildPhase()
buildPhases.append(phase)
return phase
}
/// Adds a "sources" build phase, i.e. one that compiles sources and
/// provides them to be linked into the executable code of the product.
@discardableResult
public func addSourcesBuildPhase() -> SourcesBuildPhase {
let phase = SourcesBuildPhase()
buildPhases.append(phase)
return phase
}
/// Adds a "frameworks" build phase, i.e. one that links compiled code
/// and libraries into the executable of the product.
@discardableResult
public func addFrameworksBuildPhase() -> FrameworksBuildPhase {
let phase = FrameworksBuildPhase()
buildPhases.append(phase)
return phase
}
/// Adds a "copy files" build phase, i.e. one that copies files to an
/// arbitrary location relative to the product.
@discardableResult
public func addCopyFilesBuildPhase(dstDir: String) -> CopyFilesBuildPhase {
let phase = CopyFilesBuildPhase(dstDir: dstDir)
buildPhases.append(phase)
return phase
}
/// Adds a "shell script" build phase, i.e. one that runs a custom
/// shell script as part of the build.
@discardableResult
public func addShellScriptBuildPhase(
script: String, inputs: [String], outputs: [String], alwaysRun: Bool
) -> ShellScriptBuildPhase {
let phase = ShellScriptBuildPhase(
script: script, inputs: inputs, outputs: outputs, alwaysRun: alwaysRun
)
buildPhases.append(phase)
return phase
}
/// Adds a dependency on another target.
/// FIXME: We do not check for cycles. Should we? This is an extremely
/// minimal API so it's not clear that we should.
public func addDependency(on target: Target) {
dependencies.append(TargetDependency(target: target))
}
/// Turn a given folder reference into a buildable folder for this target.
public func addBuildableFolder(_ fileRef: FileReference) {
precondition(fileRef.isDirectory)
fileRef.isBuildableFolder = true
buildableFolders.append(fileRef)
}
/// A simple wrapper to prevent ownership cycles in the `dependencies`
/// property.
public struct TargetDependency {
public unowned var target: Target
}
}
/// Abstract base class for all build phases in a target.
public class BuildPhase {
public var files: [BuildFile] = []
/// Adds a new build file that refers to `fileRef`.
@discardableResult
public func addBuildFile(fileRef: FileReference) -> BuildFile {
let buildFile = BuildFile(fileRef: fileRef)
files.append(buildFile)
return buildFile
}
}
/// A "headers" build phase, i.e. one that copies headers into a directory
/// of the product, after suitable processing.
public final class HeadersBuildPhase: BuildPhase {
// Nothing extra yet.
}
/// A "sources" build phase, i.e. one that compiles sources and provides
/// them to be linked into the executable code of the product.
public final class SourcesBuildPhase: BuildPhase {
// Nothing extra yet.
}
/// A "frameworks" build phase, i.e. one that links compiled code and
/// libraries into the executable of the product.
public final class FrameworksBuildPhase: BuildPhase {
// Nothing extra yet.
}
/// A "copy files" build phase, i.e. one that copies files to an arbitrary
/// location relative to the product.
public final class CopyFilesBuildPhase: BuildPhase {
public var dstDir: String
init(dstDir: String) {
self.dstDir = dstDir
}
}
/// A "shell script" build phase, i.e. one that runs a custom shell script.
public final class ShellScriptBuildPhase: BuildPhase {
public var script: String
public var inputs: [String]
public var outputs: [String]
public var alwaysRun: Bool
init(script: String, inputs: [String], outputs: [String], alwaysRun: Bool) {
self.script = script
self.inputs = inputs
self.outputs = outputs
self.alwaysRun = alwaysRun
}
}
/// A build file, representing the membership of a file reference in a
/// build phase of a target.
public final class BuildFile {
public var fileRef: FileReference?
init(fileRef: FileReference) {
self.fileRef = fileRef
}
public var settings = Settings()
/// A set of file settings.
public struct Settings: Encodable {
public var ATTRIBUTES: [String]?
public var COMPILER_FLAGS: String?
public init() {
}
}
}
/// A table of build settings, which for the sake of simplicity consists
/// (in this simplified model) of a set of common settings, and a set of
/// overlay settings for Debug and Release builds. There can also be a
/// file reference to an .xcconfig file on which to base the settings.
public final class BuildSettingsTable {
/// Common build settings are in both generated configurations (Debug
/// and Release).
public var common = BuildSettings()
/// Debug build settings are overlaid over the common settings in the
/// generated Debug configuration.
public var debug = BuildSettings()
/// Release build settings are overlaid over the common settings in the
/// generated Release configuration.
public var release = BuildSettings()
/// An optional file reference to an .xcconfig file.
public var xcconfigFileRef: FileReference?
public init() {
}
/// A set of build settings, which is represented as a struct of optional
/// build settings. This is not optimally efficient, but it is great for
/// code completion and type-checking.
public struct BuildSettings: Encodable {
// Note: although some of these build settings sound like booleans,
// they are all either strings or arrays of strings, because even
// a boolean may be a macro reference expression.
public var BUILT_PRODUCTS_DIR: String?
public var CLANG_CXX_LANGUAGE_STANDARD: String?
public var CLANG_ENABLE_MODULES: String?
public var CLANG_ENABLE_OBJC_ARC: String?
public var COMBINE_HIDPI_IMAGES: String?
public var COPY_PHASE_STRIP: String?
public var CURRENT_PROJECT_VERSION: String?
public var DEBUG_INFORMATION_FORMAT: String?
public var DEFINES_MODULE: String?
public var DYLIB_INSTALL_NAME_BASE: String?
public var EMBEDDED_CONTENT_CONTAINS_SWIFT: String?
public var ENABLE_NS_ASSERTIONS: String?
public var ENABLE_TESTABILITY: String?
public var FRAMEWORK_SEARCH_PATHS: [String]?
public var GCC_C_LANGUAGE_STANDARD: String?
public var GCC_OPTIMIZATION_LEVEL: String?
public var GCC_PREPROCESSOR_DEFINITIONS: [String]?
public var GCC_GENERATE_DEBUGGING_SYMBOLS: String?
public var GCC_WARN_64_TO_32_BIT_CONVERSION: String?
public var HEADER_SEARCH_PATHS: [String]?
public var INFOPLIST_FILE: String?
public var LD_RUNPATH_SEARCH_PATHS: [String]?
public var LIBRARY_SEARCH_PATHS: [String]?
public var MACOSX_DEPLOYMENT_TARGET: String?
public var IPHONEOS_DEPLOYMENT_TARGET: String?
public var TVOS_DEPLOYMENT_TARGET: String?
public var WATCHOS_DEPLOYMENT_TARGET: String?
public var DRIVERKIT_DEPLOYMENT_TARGET: String?
public var MODULEMAP_FILE: String?
public var ONLY_ACTIVE_ARCH: String?
public var OTHER_CFLAGS: [String]?
public var OTHER_CPLUSPLUSFLAGS: [String]?
public var OTHER_LDFLAGS: [String]?
public var OTHER_SWIFT_FLAGS: [String]?
public var PRODUCT_BUNDLE_IDENTIFIER: String?
public var PRODUCT_MODULE_NAME: String?
public var PRODUCT_NAME: String?
public var PROJECT_DIR: String?
public var PROJECT_NAME: String?
public var SDKROOT: String?
public var SKIP_INSTALL: String?
public var SUPPORTED_PLATFORMS: [String]?
public var SUPPORTS_MACCATALYST: String?
public var SWIFT_ACTIVE_COMPILATION_CONDITIONS: [String]?
public var SWIFT_COMPILATION_MODE: String?
public var SWIFT_ENABLE_EXPLICIT_MODULES: String?
public var SWIFT_FORCE_STATIC_LINK_STDLIB: String?
public var SWIFT_FORCE_DYNAMIC_LINK_STDLIB: String?
public var SWIFT_INCLUDE_PATHS: [String]?
public var SWIFT_MODULE_ALIASES: [String: String]?
public var SWIFT_OBJC_INTERFACE_HEADER_NAME: String?
public var SWIFT_OPTIMIZATION_LEVEL: String?
public var SWIFT_VERSION: String?
public var TARGET_NAME: String?
public var TARGET_BUILD_DIR: String?
public var USE_HEADERMAP: String?
public var LD: String?
}
}
}