blob: 9771d410ea90d3d594eebfbe836c58574cb37d52 [file] [log] [blame]
// Copyright 2016 The Tulsi Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
/// Information about the Xcode file format that should be used to serialize PBXObjects.
// These values can be obtained by inspecting a generated .xcodeproj file (but generally newer
// Xcode versions can parse properly formatted old file versions).
let XcodeVersionInfo = (objectVersion: "46", compatibilityVersion: "Xcode 3.2")
/// Valid values for the sourceTree field on PBXReference-derived classes. These values correspond to
/// the "Location" selector in Xcode's File Inspector and indicate how the path field in the
/// PBXReference should be handled.
enum SourceTree: String {
/// "Relative to Group" indicates that the path is relative to the group enclosing this reference.
case Group = "<group>"
/// "Absolute Path" indicates that the path is absolute.
case Absolute = "<absolute>"
/// "Relative to Build Products" indicates that the path is relative to the BUILT_PRODUCTS_DIR
/// environment variable.
case BuiltProductsDir = "BUILT_PRODUCTS_DIR"
/// "Relative to SDK" indicates that the path is relative to the SDKROOT environment variable.
case SDKRoot = "SDKROOT"
/// "Relative to Project" indicates that the path is relative to the SOURCE_ROOT environment
/// variable (likely the parent of the .pbxcodeproj bundle).
case SourceRoot = "SOURCE_ROOT"
/// "Relative to Developer Directory" indicates that the path is relative to the DEVELOPER_DIR
/// environment variable (likely the Developer directory within the running Xcode.app bundle).
case DeveloperDir = "DEVELOPER_DIR"
}
// Models a path within an Xcode project file.
struct SourceTreePath: Hashable {
/// Indicates the type of "path".
var sourceTree: SourceTree
var path: String
var hashValue: Int {
return sourceTree.hashValue &+ path.hashValue
}
}
func == (lhs: SourceTreePath, rhs: SourceTreePath) -> Bool {
return (lhs.sourceTree == rhs.sourceTree) && (lhs.path == rhs.path)
}
/// Protocol for all serializable project objects.
protocol PBXObjectProtocol: PBXProjSerializable, CustomDebugStringConvertible {
/// Provides a string identifying this object's type.
var isa: String { get }
/// Used in the generation of globally unique IDs.
var hashValue: Int { get }
var globalID: String { get set }
var comment: String? { get }
}
extension PBXObjectProtocol {
var debugDescription: String {
return "\(type(of: self)) \(String(describing: self.comment))"
}
}
/// Models a collection of build settings.
final class XCBuildConfiguration: PBXObjectProtocol {
var globalID: String = ""
let name: String
var buildSettings = [String:String]()
var baseConfigurationReference: PBXFileReference?
var isa: String {
return "XCBuildConfiguration"
}
lazy var hashValue: Int = { [unowned self] in
return self.name.hashValue
}()
var comment: String? {
return name
}
init(name: String) {
self.name = name
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("name", name)
try serializer.addField("buildSettings", buildSettings)
try serializer.addField("baseConfigurationReference", baseConfigurationReference)
}
}
/// Internal base class for file references and groups.
class PBXReference: PBXObjectProtocol {
var globalID: String = ""
let name: String
var path: String?
let sourceTree: SourceTree
var isa: String {
assertionFailure("PBXReference must be subclassed")
return ""
}
lazy var hashValue: Int = { [unowned self] in
return self.name.hashValue &+ (self.path?.hashValue ?? 0)
}()
var comment: String? {
return name
}
lazy var fileExtension: String? = { [unowned self] in
guard let p = self.path else { return nil }
return p.pbPathExtension
}()
lazy var uti: String? = { [unowned self] in
guard let p = self.path else { return nil }
return p.pbPathUTI
}()
weak var parent: PBXReference? {
return _parent
}
fileprivate weak var _parent: PBXReference?
init(name: String, path: String?, sourceTree: SourceTree, parent: PBXReference? = nil) {
self.name = name;
self.path = path
self.sourceTree = sourceTree
self._parent = parent
}
convenience init(name: String, sourceTreePath: SourceTreePath, parent: PBXReference? = nil) {
self.init(name: name,
path: sourceTreePath.path,
sourceTree: sourceTreePath.sourceTree,
parent: parent)
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("name", name)
try serializer.addField("path", path)
try serializer.addField("sourceTree", sourceTree.rawValue)
}
}
/// PBXFileReference instances are used to track each file in the project.
final class PBXFileReference: PBXReference, Hashable {
// A PBXFileReference for an input will have lastKnownFileType set to the file's UTI. An output
// will have explicitFileType set. The types used correspond to the "Type" field shown in Xcode's
// File Inspector.
var explicitFileType: String? {
if !isInputFile {
return fileType
}
return nil
}
var lastKnownFileType: String? {
if isInputFile {
return fileType ?? "text"
}
return nil
}
/// Override for this file reference's UTI.
var fileTypeOverride: String?
/// Whether or not this file reference is for a project input file.
var isInputFile: Bool = false
override var isa: String {
return "PBXFileReference"
}
var fileType: String? {
if fileTypeOverride != nil {
return fileTypeOverride
}
return self._pbPathUTI
}
// memoized copy of the (expensive) pbPathUTI for this PBXFileReference's name.
private lazy var _pbPathUTI: String? = { [unowned self] in
return self.name.pbPathUTI
}()
/// Returns the path to this file reference relative to the source root group.
/// Access time is linear, depending on the number of parent groups.
var sourceRootRelativePath: String {
var parentHierarchy = [path!]
var group = parent
while (group != nil && group!.path != nil) {
parentHierarchy.append(group!.path!)
group = group!.parent
}
let fullPath = parentHierarchy.reversed().joined(separator: "/")
return fullPath
}
init(name: String, path: String?, sourceTree: SourceTree, parent: PBXGroup?) {
super.init(name: name, path: path, sourceTree: sourceTree, parent: parent)
}
convenience init(name: String, sourceTreePath: SourceTreePath, parent: PBXGroup?) {
self.init(name: name, path: sourceTreePath.path, sourceTree: sourceTreePath.sourceTree, parent: parent)
}
override func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try super.serializeInto(serializer)
if let uti = lastKnownFileType {
try serializer.addField("lastKnownFileType", uti)
} else if let uti = explicitFileType {
try serializer.addField("explicitFileType", uti)
}
}
}
func == (lhs: PBXFileReference, rhs: PBXFileReference) -> Bool {
return lhs.isInputFile == rhs.isInputFile &&
lhs.fileType == rhs.fileType &&
lhs.sourceTree == rhs.sourceTree &&
lhs.name == rhs.name &&
lhs.path == rhs.path
}
/// PBXGroups are simple containers for other PBXReference instances.
class PBXGroup: PBXReference, Hashable {
/// Array of reference objects contained by this group.
var children = [PBXReference]()
// Indexes for typed access of children.
var childGroupsByName = [String: PBXGroup]()
var childVariantGroupsByName = [String: PBXVariantGroup]()
var childVersionGroupsByName = [String: XCVersionGroup]()
var fileReferencesBySourceTreePath = [SourceTreePath: PBXFileReference]()
override var isa: String {
return "PBXGroup"
}
// TODO(dmaclach): Passing back file references is pretty useless for groups.
// Probably want to change to paths or maybe just walk the fileReferencesBySourceTreePath.
var allSources: [PBXFileReference] {
var refs: [PBXFileReference] = []
for reference in children {
if let fileReference = reference as? PBXFileReference {
refs.append(fileReference)
} else if let groupReference = reference as? PBXGroup {
refs.append(contentsOf: groupReference.allSources)
}
}
return refs
}
init(name: String, path: String?, sourceTree: SourceTree, parent: PBXGroup?) {
super.init(name: name, path: path, sourceTree: sourceTree, parent: parent)
}
convenience init(name: String, sourceTreePath: SourceTreePath, parent: PBXGroup?) {
self.init(name: name, path: sourceTreePath.path, sourceTree: sourceTreePath.sourceTree, parent: parent)
}
func getOrCreateChildGroupByName(_ name: String,
path: String?,
sourceTree: SourceTree = .Group) -> PBXGroup {
if let value = childGroupsByName[name] {
return value
}
let value = PBXGroup(name: name, path: path, sourceTree: sourceTree, parent: self)
childGroupsByName[name] = value
children.append(value)
return value
}
func getOrCreateChildVariantGroupByName(_ name: String,
sourceTree: SourceTree = .Group) -> PBXVariantGroup {
if let value = childVariantGroupsByName[name] {
return value
}
let value = PBXVariantGroup(name: name, path: nil, sourceTree: sourceTree, parent: self)
childVariantGroupsByName[name] = value
children.append(value)
return value
}
func getOrCreateChildVersionGroupByName(_ name: String,
path: String?,
sourceTree: SourceTree = .Group) -> XCVersionGroup {
if let value = childVersionGroupsByName[name] {
return value
}
let value = XCVersionGroup(name: name, path: path, sourceTree: sourceTree, parent: self)
childVersionGroupsByName[name] = value
children.append(value)
return value
}
func getOrCreateFileReferenceBySourceTree(
_ sourceTree: SourceTree,
path: String,
name: String? = nil
) -> PBXFileReference {
let sourceTreePath = SourceTreePath(sourceTree: sourceTree, path: path)
return getOrCreateFileReferenceBySourceTreePath(sourceTreePath, name: name)
}
func getOrCreateFileReferenceBySourceTreePath(
_ sourceTreePath: SourceTreePath,
name: String? = nil
) -> PBXFileReference {
if let value = fileReferencesBySourceTreePath[sourceTreePath] {
return value
}
let name = name ?? sourceTreePath.path.pbPathLastComponent
let value = PBXFileReference(name: name,
sourceTreePath: sourceTreePath,
parent:self)
fileReferencesBySourceTreePath[sourceTreePath] = value
children.append(value)
return value
}
func removeChild(_ child: PBXReference) {
children = children.filter() { $0 !== child }
if child is XCVersionGroup {
childVersionGroupsByName.removeValue(forKey: child.name)
} else if child is PBXVariantGroup {
childVariantGroupsByName.removeValue(forKey: child.name)
} else if child is PBXGroup {
childGroupsByName.removeValue(forKey: child.name)
} else if child is PBXFileReference {
let sourceTreePath = SourceTreePath(sourceTree: child.sourceTree, path: child.path!)
fileReferencesBySourceTreePath.removeValue(forKey: sourceTreePath)
}
}
// Recursively updates the child's path for a migration to self.
func updatePathForChild(_ child: PBXReference, currentPath: String?) {
// Assume the name of the PBXReference is a component of the path.
let childPath: String
if let currentPath = currentPath {
childPath = currentPath + "/" + child.name
} else {
childPath = child.name
}
if let child = child as? PBXGroup {
for grandchild in child.children {
child.updatePathForChild(grandchild, currentPath: childPath)
}
} else if let child = child as? PBXFileReference {
// Remove old source path reference. All PBXFileReferences should have valid paths as
// non-main/external groups no longer have any paths, meaning a PBXFileReference without a
// path shouldn't exist as it doesn't point to anything.
var sourceTreePath = SourceTreePath(sourceTree: child.sourceTree, path: child.path!)
fileReferencesBySourceTreePath.removeValue(forKey: sourceTreePath)
// Add in new source path reference.
sourceTreePath = SourceTreePath(sourceTree: child.sourceTree, path: childPath)
fileReferencesBySourceTreePath[sourceTreePath] = child
child.path = childPath
}
}
/// Takes ownership of the children of the given group. Note that this leaves the "other" group in
/// an invalid state and it should be discarded (for example, via removeChild).
func migrateChildrenOfGroup(_ other: PBXGroup) {
for child in other.children {
child._parent = self
children.append(child)
// Update path for the child and its entire tree.
updatePathForChild(child, currentPath: nil)
if let child = child as? XCVersionGroup {
childVersionGroupsByName[child.name] = child
} else if let child = child as? PBXVariantGroup {
childVariantGroupsByName[child.name] = child
} else if let child = child as? PBXGroup {
childGroupsByName[child.name] = child
} else if let child = child as? PBXFileReference {
let sourceTreePath = SourceTreePath(sourceTree: child.sourceTree, path: child.path!)
fileReferencesBySourceTreePath[sourceTreePath] = child
}
}
}
/// Returns the (first) child file reference with the given name, if any.
func childFileReference(withName name: String) -> PBXFileReference? {
return fileReferencesBySourceTreePath.values.first { $0.name == name }
}
override func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try super.serializeInto(serializer)
try serializer.addField("children", children.sorted(by: {$0.name < $1.name}))
}
}
func == (lhs: PBXGroup, rhs: PBXGroup) -> Bool {
// NOTE(abaire): This isn't technically correct as it's possible to create two groups with
// identical paths but different contents. For this to be reusable, everything should really be
// made Hashable and the children should be compared as well.
return lhs.name == rhs.name &&
lhs.isa == rhs.isa &&
lhs.path == rhs.path
}
/// Models a localized resource group.
class PBXVariantGroup: PBXGroup {
override var isa: String {
return "PBXVariantGroup"
}
}
/// Models a versioned group (e.g., a Core Data xcdatamodeld).
final class XCVersionGroup: PBXGroup {
/// The active child reference.
var currentVersion: PBXReference? = nil
var versionGroupType: String = ""
override var isa: String {
return "XCVersionGroup"
}
init(name: String,
path: String?,
sourceTree: SourceTree,
parent: PBXGroup?,
versionGroupType: String = "") {
super.init(name: name, path: path, sourceTree: sourceTree, parent: parent)
}
convenience init(name: String,
sourceTreePath: SourceTreePath,
parent: PBXGroup?,
versionGroupType: String = "") {
self.init(name: name,
path: sourceTreePath.path,
sourceTree: sourceTreePath.sourceTree,
parent: parent,
versionGroupType: versionGroupType)
}
func setCurrentVersionByName(_ name: String) -> Bool {
// We have to go by name instead of path as PBXFileReferences' paths are relative to the
// mainGroup while the name is just the name of the file.
guard let value = childFileReference(withName: name) else {
return false
}
currentVersion = value
return true
}
override func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try super.serializeInto(serializer)
if let currentVersion = currentVersion {
try serializer.addField("currentVersion", currentVersion)
}
try serializer.addField("versionGroupType", versionGroupType)
}
}
/// Models the set of XCBuildConfiguration instances for a given target or project.
final class XCConfigurationList: PBXObjectProtocol {
var globalID: String = ""
var buildConfigurations = [String: XCBuildConfiguration]()
var defaultConfigurationIsVisible = false
var defaultConfigurationName: String? = nil
var isa: String {
return "XCConfigurationList"
}
lazy var hashValue: Int = { [unowned self] in
return self.comment?.hashValue ?? 0
}()
let comment: String?
init(forType: String? = nil, named: String? = nil) {
if let ownerType = forType, let name = named {
self.comment = "Build configuration list for \(ownerType) \"\(name)\""
} else {
self.comment = nil
}
}
func getOrCreateBuildConfiguration(_ name: String) -> XCBuildConfiguration {
if let value = buildConfigurations[name] {
return value
}
let value = XCBuildConfiguration(name: name)
buildConfigurations[name] = value
return value
}
func getBuildConfiguration(_ name: String) -> XCBuildConfiguration? {
return buildConfigurations[name]
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("buildConfigurations", buildConfigurations.values.sorted(by: {$0.name < $1.name}))
try serializer.addField("defaultConfigurationIsVisible", defaultConfigurationIsVisible)
if let defaultConfigurationName = defaultConfigurationName {
try serializer.addField("defaultConfigurationName", defaultConfigurationName)
}
}
}
/// Internal base class for concrete build phases (each of which capture a set of files that will be
/// used as inputs to that phase).
class PBXBuildPhase: PBXObjectProtocol {
// Used to identify different phases of the same type (i.e. isa).
var mnemonic: String = ""
var globalID: String = ""
var files = [PBXBuildFile]()
let buildActionMask: Int
let runOnlyForDeploymentPostprocessing: Bool
init(buildActionMask: Int = 0, runOnlyForDeploymentPostprocessing: Bool = false) {
self.buildActionMask = buildActionMask
self.runOnlyForDeploymentPostprocessing = runOnlyForDeploymentPostprocessing
}
var isa: String {
assertionFailure("PBXBuildPhase must be subclassed")
return ""
}
var hashValue: Int {
assertionFailure("PBXBuildPhase must be subclassed")
return 0
}
var comment: String? {
assertionFailure("PBXBuildPhase must be subclassed")
return nil
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("buildActionMask", buildActionMask)
try serializer.addField("files", files)
try serializer.addField("runOnlyForDeploymentPostprocessing", runOnlyForDeploymentPostprocessing)
}
}
/// Encapsulates a source file compilation phase.
final class PBXSourcesBuildPhase: PBXBuildPhase {
override var isa: String {
return "PBXSourcesBuildPhase"
}
override var hashValue: Int {
return 0
}
override var comment: String? {
return "Sources"
}
override init(buildActionMask: Int = 0, runOnlyForDeploymentPostprocessing: Bool = false) {
super.init(buildActionMask: buildActionMask,
runOnlyForDeploymentPostprocessing: runOnlyForDeploymentPostprocessing)
self.mnemonic = "CompileSources"
}
}
/// Encapsulates a shell script execution phase.
final class PBXShellScriptBuildPhase: PBXBuildPhase {
let inputPaths: [String]
let outputPaths: [String]
let shellPath: String
let shellScript: String
var showEnvVarsInLog = false
override var isa: String {
return "PBXShellScriptBuildPhase"
}
override var hashValue: Int { return _hashValue }
private let _hashValue: Int
override var comment: String? {
return "ShellScript"
}
init(shellScript: String,
shellPath: String = "/bin/sh",
inputPaths: [String] = [String](),
outputPaths: [String] = [String](),
buildActionMask: Int = 0,
runOnlyForDeploymentPostprocessing: Bool = false) {
self.shellScript = shellScript
self.shellPath = shellPath
self.inputPaths = inputPaths
self.outputPaths = outputPaths
self._hashValue = shellPath.hashValue &+ shellScript.hashValue
super.init(buildActionMask: buildActionMask,
runOnlyForDeploymentPostprocessing: runOnlyForDeploymentPostprocessing)
}
override func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try super.serializeInto(serializer)
try serializer.addField("inputPaths", inputPaths)
try serializer.addField("outputPaths", outputPaths)
try serializer.addField("shellPath", shellPath)
try serializer.addField("shellScript", shellScript)
try serializer.addField("showEnvVarsInLog", showEnvVarsInLog)
}
}
/// File reference with associated build flags (set in Xcode via the Compile Sources phase). This
/// extra level of indirection allows any given PBXReference to be included in multiple targets
/// with different COMPILER_FLAGS settings for each. (e.g., a file could have a preprocessor define
/// set while compiling a test target that is not set when building the main target).
final class PBXBuildFile: PBXObjectProtocol {
var globalID: String = ""
let fileRef: PBXReference
let settings: [String: String]?
init(fileRef: PBXReference, settings: [String: String]? = nil) {
self.fileRef = fileRef
self.settings = settings
}
lazy var hashValue: Int = { [unowned self] in
var val = self.fileRef.hashValue
if let settings = self.settings {
for (key, value) in settings {
val = val &+ key.hashValue &+ value.hashValue
}
}
return val
}()
var isa: String {
return "PBXBuildFile"
}
var comment: String? {
if let parent = fileRef.parent {
return "\(fileRef.comment!) in \(parent.comment!)"
}
return fileRef.comment
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("fileRef", fileRef)
if let concreteSettings = settings {
try serializer.addField("settings", concreteSettings)
}
}
}
/// Base class for concrete build targets.
class PBXTarget: PBXObjectProtocol, Hashable {
enum ProductType: String {
case StaticLibrary = "com.apple.product-type.library.static"
case DynamicLibrary = "com.apple.product-type.library.dynamic"
case Tool = "com.apple.product-type.tool"
case Bundle = "com.apple.product-type.bundle"
case Framework = "com.apple.product-type.framework"
case StaticFramework = "com.apple.product-type.framework.static"
case Application = "com.apple.product-type.application"
case MessagesApplication = "com.apple.product-type.application.messages"
case MessagesExtension = "com.apple.product-type.app-extension.messages"
case MessagesStickerPackExtension = "com.apple.product-type.app-extension.messages-sticker-pack"
case UnitTest = "com.apple.product-type.bundle.unit-test"
case UIUnitTest = "com.apple.product-type.bundle.ui-testing"
case InAppPurchaseContent = "com.apple.product-type.in-app-purchase-content"
case AppExtension = "com.apple.product-type.app-extension"
case XPCService = "com.apple.product-type.xpc-service"
case Watch1App = "com.apple.product-type.application.watchapp"
case Watch2App = "com.apple.product-type.application.watchapp2"
case Watch1Extension = "com.apple.product-type.watchkit-extension"
case Watch2Extension = "com.apple.product-type.watchkit2-extension"
case TVAppExtension = "com.apple.product-type.tv-app-extension"
/// Whether or not this ProductType denotes a watch application.
var isWatchApp: Bool {
return self == .Watch1App || self == .Watch2App
}
/// Whether or not this ProductType is an iOS app extension or variant.
var isiOSAppExtension: Bool {
return self == .AppExtension || self == .MessagesExtension
|| self == .MessagesStickerPackExtension
}
/// Whether or not this ProductType denotes a test bundle.
var isTest: Bool {
return self == .UnitTest || self == .UIUnitTest
}
/// Whether or not this ProductTypes denotes a library target.
var isLibrary: Bool {
return self == .StaticLibrary || self == .DynamicLibrary
}
/// Returns the extension type associated with this watch app type (or nil if this ProductType
/// is not a WatchApp type).
var watchAppExtensionType: ProductType? {
switch self {
case .Watch1App:
return .Watch1Extension
case .Watch2App:
return .Watch2Extension
default:
return nil
}
}
var explicitFileType: String {
switch self {
case .StaticLibrary:
return "archive.ar"
case .DynamicLibrary:
return "compiled.mach-o.dylib"
case .Tool:
return "compiled.mach-o.executable"
case .Bundle:
return "wrapper.bundle"
case .Framework:
return "wrapper.framework"
case .StaticFramework:
return "wrapper.framework.static"
case .Watch1App:
fallthrough
case .Watch2App:
fallthrough
case .MessagesApplication:
fallthrough
case .Application:
return "wrapper.application"
case .UnitTest:
fallthrough
case .UIUnitTest:
return "wrapper.cfbundle"
case .InAppPurchaseContent:
return "folder"
case .Watch1Extension:
fallthrough
case .Watch2Extension:
fallthrough
case .TVAppExtension:
fallthrough
case .MessagesExtension:
fallthrough
case .MessagesStickerPackExtension:
fallthrough
case .AppExtension:
return "wrapper.app-extension"
case .XPCService:
return "wrapper.xpc-service"
}
}
func productName(_ name: String) -> String {
switch self {
case .StaticLibrary:
return "lib\(name).a"
case .DynamicLibrary:
return "lib\(name).dylib"
case .Tool:
return name
case .Bundle:
return "\(name).bundle"
case .Framework:
return "\(name).framework"
case .StaticFramework:
return "\(name).framework"
case .Watch2App:
fallthrough
case .MessagesApplication:
fallthrough
case .Application:
return "\(name).app"
case .UnitTest:
fallthrough
case .UIUnitTest:
return "\(name).xctest"
case .InAppPurchaseContent:
return name
case .Watch1App:
// watchOS1 apps are packaged as extensions.
fallthrough
case .Watch1Extension:
fallthrough
case .Watch2Extension:
fallthrough
case .TVAppExtension:
fallthrough
case .MessagesExtension:
fallthrough
case .MessagesStickerPackExtension:
fallthrough
case .AppExtension:
return "\(name).appex"
case .XPCService:
return "\(name).xpc"
}
}
}
var globalID: String = ""
let name: String
var productName: String? { return name }
// The primary artifact generated by building this target. Generally this will be productName with
// a file/bundle extension.
var buildableName: String { return name }
lazy var buildConfigurationList: XCConfigurationList = { [unowned self] in
XCConfigurationList(forType: self.isa, named: self.name)
}()
/// The targets on which this target depends.
var dependencies = [PBXTargetDependency]()
/// Any targets which must be built by XCSchemes generated for this target.
var buildActionDependencies = Set<PBXTarget>()
/// The build phases to be executed to generate this target.
var buildPhases = [PBXBuildPhase]()
/// Deployment target for this target, if available.
let deploymentTarget: DeploymentTarget?
var isa: String {
assertionFailure("PBXTarget must be subclassed")
return ""
}
lazy var hashValue: Int = { [unowned self] in
return self.name.hashValue &+ (self.comment?.hashValue ?? 0)
}()
var comment: String? {
return name
}
init(name: String, deploymentTarget: DeploymentTarget?) {
self.name = name
self.deploymentTarget = deploymentTarget
}
/// Creates a dependency on the given target.
/// If first is true, the dependency will be prepended instead of appended.
func createDependencyOn(_ target: PBXTarget,
proxyType: PBXContainerItemProxy.ProxyType,
inProject project: PBXProject,
first: Bool = false) {
if target === self {
assertionFailure("Targets may not be dependent on themselves. (\(target.name))")
return
}
let dependency = project.createTargetDependency(target, proxyType: proxyType)
if first {
dependencies.insert(dependency, at: 0)
} else {
dependencies.append(dependency)
}
}
/// Creates a BuildAction-only dependency on the given target. Unlike a true dependency, this
/// linkage is only intended to affect generated XCSchemes.
func createBuildActionDependencyOn(_ target: PBXTarget) {
buildActionDependencies.insert(target)
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("buildPhases", buildPhases)
try serializer.addField("buildConfigurationList", buildConfigurationList)
try serializer.addField("dependencies", dependencies)
try serializer.addField("name", name)
try serializer.addField("productName", productName)
}
}
func == (lhs: PBXTarget, rhs: PBXTarget) -> Bool {
return lhs.name == rhs.name
}
/// Models a target that produces a binary.
final class PBXNativeTarget: PBXTarget {
let productType: ProductType
/// Reference to the output of this target.
var productReference: PBXFileReference?
override var buildableName: String {
return productType.productName(name)
}
override var isa: String {
return "PBXNativeTarget"
}
init(name: String, deploymentTarget: DeploymentTarget?, productType: ProductType) {
self.productType = productType
super.init(name: name, deploymentTarget: deploymentTarget)
}
override func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try super.serializeInto(serializer)
// An empty buildRules array is emitted to match Xcode's serialization.
try serializer.addField("buildRules", [String]())
try serializer.addField("productReference", productReference)
try serializer.addField("productType", productType.rawValue)
}
}
/// Models a target that executes an arbitrary binary.
final class PBXLegacyTarget: PBXTarget {
let buildArgumentsString: String
let buildToolPath: String
let buildWorkingDirectory: String
var passBuildSettingsInEnvironment: Bool = true
override var isa: String {
return "PBXLegacyTarget"
}
init(name: String,
deploymentTarget: DeploymentTarget?,
buildToolPath: String,
buildArguments: String,
buildWorkingDirectory: String) {
self.buildToolPath = buildToolPath
self.buildArgumentsString = buildArguments
self.buildWorkingDirectory = buildWorkingDirectory
super.init(name: name, deploymentTarget: deploymentTarget)
}
override func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try super.serializeInto(serializer)
try serializer.addField("buildArgumentsString", buildArgumentsString)
try serializer.addField("buildToolPath", buildToolPath)
try serializer.addField("buildWorkingDirectory", buildWorkingDirectory)
try serializer.addField("passBuildSettingsInEnvironment", passBuildSettingsInEnvironment)
}
}
/// Models a link to a target or output file which may be in a different project.
final class PBXContainerItemProxy: PBXObjectProtocol, Hashable {
/// The type of the item being referenced.
enum ProxyType: Int {
/// Refers to a PBXTarget in some project file.
case targetReference = 1
/// Refers to a PBXFileReference in some other project file (an output of another project's
/// target).
case fileReference = 2
}
var globalID: String = ""
/// The project containing the referenced item.
var containerPortal: PBXProject {
return _ContainerPortal!
}
private weak var _ContainerPortal: PBXProject?
/// The target being tracked by this proxy.
var target: PBXObjectProtocol {
return _target!
}
fileprivate weak var _target: PBXObjectProtocol?
let proxyType: ProxyType
var isa: String {
return "PBXContainerItemProxy"
}
var hashValue: Int {
return _target!.hashValue &+ proxyType.rawValue
}
var comment: String? {
return "PBXContainerItemProxy"
}
init(containerPortal: PBXProject, target: PBXObjectProtocol, proxyType: ProxyType) {
self._ContainerPortal = containerPortal
self._target = target
self.proxyType = proxyType
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
try serializer.addField("containerPortal", _ContainerPortal)
try serializer.addField("proxyType", proxyType.rawValue)
try serializer.addField("remoteGlobalIDString", _target, rawID: true)
}
}
func == (lhs: PBXContainerItemProxy, rhs: PBXContainerItemProxy) -> Bool {
if lhs.proxyType != rhs.proxyType { return false }
switch lhs.proxyType {
case .targetReference:
return lhs._target as? PBXTarget == rhs._target as? PBXTarget
case .fileReference:
return lhs._target as? PBXFileReference == rhs._target as? PBXFileReference
}
}
/// Models a dependent relationship between a build target and some other build target.
final class PBXTargetDependency: PBXObjectProtocol {
var globalID: String = ""
let targetProxy: PBXContainerItemProxy
init(targetProxy: PBXContainerItemProxy) {
self.targetProxy = targetProxy
}
var isa: String {
return "PBXTargetDependency"
}
var hashValue: Int {
return targetProxy.hashValue
}
var comment: String? {
return "PBXTargetDependency"
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
// Note(abaire): Xcode also generates a "target" field.
try serializer.addField("targetProxy", targetProxy)
}
}
/// Models a project container.
final class PBXProject: PBXObjectProtocol {
// Name of the group in which target file references are stored.
static let ProductsGroupName = "Products"
// Name of the group inside ProductsGroupName to store indexer target file references.
static let IndexerProductsGroupName = "Indexer"
var globalID: String = ""
let name: String
/// The root group for this project.
let mainGroup: PBXGroup
/// Map of target name to target instance.
var targetByName = [String: PBXTarget]()
/// List of all targets.
var allTargets: Dictionary<String, PBXTarget>.Values {
return targetByName.values
}
let compatibilityVersion = XcodeVersionInfo.compatibilityVersion
let lastUpgradeCheck = "1000"
/// May be set to an Xcode version string indicating the last time a Swift upgrade check was
/// performed (e.g., 0710).
var lastSwiftUpdateCheck: String? = nil
/// List of (testTarget, hostTarget) pairs linking test targets to their host applications.
var testTargetLinkages = [(PBXTarget, PBXTarget)]()
// Maps container proxies to PBXTargetDependency instances to facilitate reuse of target
// dependency instances.
var targetDependencies = [PBXContainerItemProxy: PBXTargetDependency]()
lazy var buildConfigurationList: XCConfigurationList = { [unowned self] in
XCConfigurationList(forType: self.isa, named: self.name)
}()
var isa: String {
return "PBXProject"
}
var hashValue: Int {
return name.hashValue
}
var comment: String? {
return "Project object"
}
init(name: String, mainGroup: PBXGroup? = nil) {
if mainGroup != nil {
self.mainGroup = mainGroup!
} else {
self.mainGroup = PBXGroup(name: "mainGroup", path: nil, sourceTree: .SourceRoot, parent: nil)
}
self.name = name
}
func createNativeTarget(_ name: String,
deploymentTarget: DeploymentTarget?,
targetType: PBXTarget.ProductType,
isIndexerTarget: Bool = false) -> PBXNativeTarget {
let value = PBXNativeTarget(name: name,
deploymentTarget: deploymentTarget,
productType: targetType)
targetByName[name] = value
var productsGroup = mainGroup.getOrCreateChildGroupByName(PBXProject.ProductsGroupName,
path: nil)
if isIndexerTarget {
productsGroup = productsGroup.getOrCreateChildGroupByName(PBXProject.IndexerProductsGroupName,
path: nil)
}
let productName = targetType.productName(name)
let productReference = productsGroup.getOrCreateFileReferenceBySourceTree(.BuiltProductsDir,
path: productName)
productReference.fileTypeOverride = targetType.explicitFileType
productReference.isInputFile = false
value.productReference = productReference
return value
}
func createLegacyTarget(_ name: String,
deploymentTarget: DeploymentTarget?,
buildToolPath: String,
buildArguments: String,
buildWorkingDirectory: String) -> PBXLegacyTarget {
let value = PBXLegacyTarget(name: name,
deploymentTarget: deploymentTarget,
buildToolPath: buildToolPath,
buildArguments: buildArguments,
buildWorkingDirectory: buildWorkingDirectory)
targetByName[name] = value
return value
}
/// Creates subgroups and a file reference to the given path under the special Products directory
/// rather than as a normal component of the build process. This should very rarely be used, but
/// is useful if it is necessary to add references to byproducts of the build process that are
/// not the direct output of any PBXTarget.
@discardableResult
func createProductReference(_ path: String) -> (Set<PBXGroup>, PBXFileReference) {
let productsGroup = mainGroup.getOrCreateChildGroupByName(PBXProject.ProductsGroupName,
path: nil)
return createGroupsAndFileReferenceForPath(path, underGroup: productsGroup)
}
func linkTestTarget(_ testTarget: PBXTarget, toHostTarget hostTarget: PBXTarget) {
testTargetLinkages.append((testTarget, hostTarget))
testTarget.createDependencyOn(hostTarget,
proxyType:PBXContainerItemProxy.ProxyType.targetReference,
inProject: self)
}
func linkedTestTargetsForHost(_ host: PBXTarget) -> [PBXTarget] {
let targetHostPairs = testTargetLinkages.filter() {
(testTarget: PBXTarget, testHostTarget: PBXTarget) -> Bool in
testHostTarget == host
}
return targetHostPairs.map() { $0.0 }
}
func linkedHostForTestTarget(_ target: PBXTarget) -> PBXTarget? {
for (testTarget, testHostTarget) in testTargetLinkages {
if testTarget == target {
return testHostTarget
}
}
return nil
}
func createTargetDependency(_ target: PBXTarget, proxyType: PBXContainerItemProxy.ProxyType) -> PBXTargetDependency {
let targetProxy = PBXContainerItemProxy(containerPortal: self, target: target, proxyType: proxyType)
if let existingDependency = targetDependencies[targetProxy] {
return existingDependency
}
let dependency = PBXTargetDependency(targetProxy: targetProxy)
targetDependencies[targetProxy] = dependency
return dependency
}
func targetByName(_ name: String) -> PBXTarget? {
return targetByName[name]
}
/// Creates subgroups and file references for the given set of paths. Path directory components
/// will be expanded into nested PBXGroup instances, but will not have individual path components.
/// The full path is made into a PBXFileReference under the inner most PBXGroup.
/// Returns a tuple containing the PBXGroup and PBXFileReference instances that were touched while
/// processing the set of paths.
@discardableResult
func getOrCreateGroupsAndFileReferencesForPaths(_ paths: [String]) -> (Set<PBXGroup>, [PBXFileReference]) {
var accessedGroups = Set<PBXGroup>()
var accessedFileReferences = [PBXFileReference]()
for path in paths {
let (groups, ref) = createGroupsAndFileReferenceForPath(path, underGroup: mainGroup)
accessedGroups.formUnion(groups)
ref.isInputFile = true
accessedFileReferences.append(ref)
}
return (accessedGroups, accessedFileReferences)
}
func getOrCreateGroupForPath(_ path: String) -> PBXGroup {
guard !path.isEmpty else {
// Rather than creating an empty subpath, return the mainGroup itself.
return mainGroup
}
var group = mainGroup
for component in path.components(separatedBy: "/") {
let groupName = component.isEmpty ? "/" : component
group = group.getOrCreateChildGroupByName(groupName, path: nil)
}
return group
}
func getOrCreateVersionGroupForPath(_ path: String, versionGroupType: String) -> XCVersionGroup {
let parentPath = (path as NSString).deletingLastPathComponent
let group = getOrCreateGroupForPath(parentPath)
let versionedGroupName = (path as NSString).lastPathComponent
let versionedGroup = group.getOrCreateChildVersionGroupByName(versionedGroupName,
path: nil)
versionedGroup.versionGroupType = versionGroupType
return versionedGroup
}
func serializeInto(_ serializer: PBXProjFieldSerializer) throws {
var attributes: [String: AnyObject] = ["LastUpgradeCheck": lastUpgradeCheck as AnyObject]
if lastSwiftUpdateCheck != nil {
attributes["LastSwiftUpdateCheck"] = lastSwiftUpdateCheck! as AnyObject?
}
// Link test targets to their host applications.
var testLinkages = [String: Any]()
for (testTarget, hostTarget) in testTargetLinkages {
let testTargetID = try serializer.serializeObject(testTarget, returnRawID: true)
let hostTargetID = try serializer.serializeObject(hostTarget, returnRawID: true)
testLinkages[testTargetID] = ["TestTargetID": hostTargetID]
}
if !testLinkages.isEmpty {
attributes["TargetAttributes"] = testLinkages as AnyObject?
}
try serializer.addField("attributes", attributes)
try serializer.addField("buildConfigurationList", buildConfigurationList)
try serializer.addField("compatibilityVersion", compatibilityVersion)
try serializer.addField("mainGroup", mainGroup);
try serializer.addField("targets", targetByName.values.sorted(by: {$0.name < $1.name}));
// Hardcoded defaults to match Xcode behavior.
try serializer.addField("developmentRegion", "English")
try serializer.addField("hasScannedForEncodings", false)
try serializer.addField("knownRegions", ["en"])
}
func createGroupsAndFileReferenceForPath(
_ path: String,
underGroup parent: PBXGroup
) -> (Set<PBXGroup>, PBXFileReference) {
var group = parent
var accessedGroups = Set<PBXGroup>()
// Traverse the directory components of the path, converting them to Xcode
// PBXGroups without a path component.
let components = path.components(separatedBy: "/")
for i in 0 ..< components.count - 1 {
// Check to see if this component is actually a bundle that should be treated as a file
// reference by Xcode (e.g., .xcassets bundles) instead of as a PBXGroup.
let currentComponent = components[i]
// Support per-locale files (lprojs), which are represented in PBXVariantGroups.
if currentComponent.pbPathExtension == "lproj" {
let lprojName = currentComponent.replacingOccurrences(of: ".lproj", with: "")
let name = components[i + 1] // Assume it's the next (and last).
let variantGroup = group.getOrCreateChildVariantGroupByName(name)
accessedGroups.insert(variantGroup)
let fileRef = variantGroup.getOrCreateFileReferenceBySourceTree(.Group,
path: path,
name: lprojName)
if let ext = name.pbPathExtension, let uti = DirExtensionToUTI[ext] {
fileRef.fileTypeOverride = uti
}
return (accessedGroups, fileRef)
}
// This will naively create a bundle grouping rather than including the per-locale strings.
if let ext = currentComponent.pbPathExtension, let uti = DirExtensionToUTI[ext] {
let partialPath = components[0...i].joined(separator: "/")
let fileRef = group.getOrCreateFileReferenceBySourceTree(.Group, path: partialPath)
fileRef.fileTypeOverride = uti
// Contents of bundles should never be referenced directly so this path
// entry is now fully parsed.
return (accessedGroups, fileRef)
}
// Create a subgroup for this simple path component.
let groupName = currentComponent.isEmpty ? "/" : currentComponent
group = group.getOrCreateChildGroupByName(groupName, path: nil)
accessedGroups.insert(group)
}
let fileRef = group.getOrCreateFileReferenceBySourceTree(.Group, path: path)
return (accessedGroups, fileRef)
}
}