//===----------------------------------------------------------------------===// // // 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 // //===----------------------------------------------------------------------===// /* An extemely simple rendition of the Xcode project model into a plist. There is only enough functionality to allow serialization of Xcode projects. */ extension Xcode.Project: PropertyListSerializable { /// Generates and returns the contents of a `project.pbxproj` plist. Does /// not generate any ancillary files, such as a set of schemes. /// /// Many complexities of the Xcode project model are not represented; we /// should not add functionality to this model unless it's needed, since /// implementation of the full Xcode project model would be unnecessarily /// complex. public func generatePlist() throws -> PropertyList { // The project plist is a bit special in that it's the archive for the // whole file. We create a plist serializer and serialize the entire // object graph to it, and then return an archive dictionary containing // the serialized object dictionaries. let serializer = PropertyListSerializer() try serializer.serialize(object: self) return .dictionary([ "archiveVersion": .string("1"), "objectVersion": .string("46"), // Xcode 8.0 "rootObject": .identifier(serializer.id(of: self)), "objects": .dictionary(serializer.idsToDicts), ]) } /// Called by the Serializer to serialize the Project. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXProject` plist dictionary. // Note: we skip things like the `Products` group; they get autocreated // by Xcode when it opens the project and notices that they are missing. // Note: we also skip schemes, since they are not in the project plist. var dict = [String: PropertyList]() dict["isa"] = .string("PBXProject") // Since the project file is generated, we opt out of upgrade-checking. // FIXME: Should we really? Why would we not want to get upgraded? dict["attributes"] = .dictionary(["LastUpgradeCheck": .string("9999"), "LastSwiftMigration": .string("9999")]) dict["compatibilityVersion"] = .string("Xcode 3.2") dict["developmentRegion"] = .string("en") // Build settings are a bit tricky; in Xcode, each is stored in a named // XCBuildConfiguration object, and the list of build configurations is // in turn stored in an XCConfigurationList. In our simplified model, // we have a BuildSettingsTable, with three sets of settings: one for // the common settings, and one each for the Debug and Release overlays. // So we consider the BuildSettingsTable to be the configuration list. dict["buildConfigurationList"] = try .identifier(serializer.serialize(object: buildSettings)) dict["mainGroup"] = try .identifier(serializer.serialize(object: mainGroup)) dict["hasScannedForEncodings"] = .string("0") dict["knownRegions"] = .array([.string("en")]) if let productGroup = productGroup { dict["productRefGroup"] = .identifier(serializer.id(of: productGroup)) } dict["projectDirPath"] = .string(projectDir) // Ensure that targets are output in a sorted order. let sortedTargets = targets.sorted(by: { $0.name < $1.name }) dict["targets"] = try .array(sortedTargets.map({ target in try .identifier(serializer.serialize(object: target)) })) return dict } } /// Private helper function that constructs and returns a partial property list /// dictionary for references. The caller can add to the returned dictionary. /// FIXME: It would be nicer to be able to use inheritance to serialize the /// attributes inherited from Reference, but but in Swift 3.0 we get an error /// that "declarations in extensions cannot override yet". fileprivate func makeReferenceDict( reference: Xcode.Reference, serializer: PropertyListSerializer, xcodeClassName: String ) -> [String: PropertyList] { var dict = [String: PropertyList]() dict["isa"] = .string(xcodeClassName) dict["path"] = .string(reference.path) if let name = reference.name { dict["name"] = .string(name) } dict["sourceTree"] = .string(reference.pathBase.rawValue) return dict } extension Xcode.Group: PropertyListSerializable { /// Called by the Serializer to serialize the Group. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXGroup` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from Reference, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". var dict = makeReferenceDict(reference: self, serializer: serializer, xcodeClassName: "PBXGroup") dict["children"] = try .array(subitems.map({ reference in // For the same reason, we have to cast as `PropertyListSerializable` // here; as soon as we try to make Reference conform to the protocol, // we get the problem of not being able to override `serialize(to:)`. try .identifier(serializer.serialize(object: reference as! PropertyListSerializable)) })) return dict } } extension Xcode.FileReference: PropertyListSerializable { /// Called by the Serializer to serialize the FileReference. fileprivate func serialize(to serializer: PropertyListSerializer) -> [String: PropertyList] { // Create a `PBXFileReference` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from Reference, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". var dict = makeReferenceDict(reference: self, serializer: serializer, xcodeClassName: "PBXFileReference") if let fileType = fileType { dict["explicitFileType"] = .string(fileType) } // FileReferences don't need to store a name if it's the same as the path. if name == path { dict["name"] = nil } return dict } } extension Xcode.Target: PropertyListSerializable { /// Called by the Serializer to serialize the Target. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create either a `PBXNativeTarget` or an `PBXAggregateTarget` plist // dictionary (depending on whether or not we have a product type). var dict = [String: PropertyList]() dict["isa"] = .string(productType == nil ? "PBXAggregateTarget" : "PBXNativeTarget") dict["name"] = .string(name) // Build settings are a bit tricky; in Xcode, each is stored in a named // XCBuildConfiguration object, and the list of build configurations is // in turn stored in an XCConfigurationList. In our simplified model, // we have a BuildSettingsTable, with three sets of settings: one for // the common settings, and one each for the Debug and Release overlays. // So we consider the BuildSettingsTable to be the configuration list. // This is the same situation as for Project. dict["buildConfigurationList"] = try .identifier(serializer.serialize(object: buildSettings)) dict["buildPhases"] = try .array(buildPhases.map({ phase in // Here we have the same problem as for Reference; we cannot inherit // functionality since we're in an extension. try .identifier(serializer.serialize(object: phase as! PropertyListSerializable)) })) /// Private wrapper class for a target dependency relation. This is /// glue between our value-based settings structures and the Xcode /// project model's identity-based TargetDependency objects. class TargetDependency: PropertyListSerializable { var target: Xcode.Target init(target: Xcode.Target) { self.target = target } func serialize(to serializer: PropertyListSerializer) -> [String: PropertyList] { // Create a `PBXTargetDependency` plist dictionary. var dict = [String: PropertyList]() dict["isa"] = .string("PBXTargetDependency") dict["target"] = .identifier(serializer.id(of: target)) return dict } } dict["dependencies"] = try .array(dependencies.map({ dep in // In the Xcode project model, target dependencies are objects, // so we need a helper class here. try .identifier(serializer.serialize(object: TargetDependency(target: dep.target))) })) dict["productName"] = .string(productName) if let productType = productType { dict["productType"] = .string(productType.rawValue) } if let productReference = productReference { dict["productReference"] = .identifier(serializer.id(of: productReference)) } return dict } } /// Private helper function that constructs and returns a partial property list /// dictionary for build phases. The caller can add to the returned dictionary. /// FIXME: It would be nicer to be able to use inheritance to serialize the /// attributes inherited from BuildPhase, but but in Swift 3.0 we get an error /// that "declarations in extensions cannot override yet". fileprivate func makeBuildPhaseDict( buildPhase: Xcode.BuildPhase, serializer: PropertyListSerializer, xcodeClassName: String ) throws -> [String: PropertyList] { var dict = [String: PropertyList]() dict["isa"] = .string(xcodeClassName) dict["files"] = try .array(buildPhase.files.map({ file in try .identifier(serializer.serialize(object: file)) })) return dict } extension Xcode.HeadersBuildPhase: PropertyListSerializable { /// Called by the Serializer to serialize the HeadersBuildPhase. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXHeadersBuildPhase` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from BuildPhase, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". return try makeBuildPhaseDict(buildPhase: self, serializer: serializer, xcodeClassName: "PBXHeadersBuildPhase") } } extension Xcode.SourcesBuildPhase: PropertyListSerializable { /// Called by the Serializer to serialize the SourcesBuildPhase. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXSourcesBuildPhase` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from BuildPhase, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". return try makeBuildPhaseDict(buildPhase: self, serializer: serializer, xcodeClassName: "PBXSourcesBuildPhase") } } extension Xcode.FrameworksBuildPhase: PropertyListSerializable { /// Called by the Serializer to serialize the FrameworksBuildPhase. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXFrameworksBuildPhase` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from BuildPhase, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". return try makeBuildPhaseDict(buildPhase: self, serializer: serializer, xcodeClassName: "PBXFrameworksBuildPhase") } } extension Xcode.CopyFilesBuildPhase: PropertyListSerializable { /// Called by the Serializer to serialize the FrameworksBuildPhase. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXCopyFilesBuildPhase` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from BuildPhase, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". var dict = try makeBuildPhaseDict( buildPhase: self, serializer: serializer, xcodeClassName: "PBXCopyFilesBuildPhase" ) dict["dstPath"] = .string("") // FIXME: needs to be real dict["dstSubfolderSpec"] = .string("") // FIXME: needs to be real return dict } } extension Xcode.ShellScriptBuildPhase: PropertyListSerializable { /// Called by the Serializer to serialize the ShellScriptBuildPhase. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXShellScriptBuildPhase` plist dictionary. // FIXME: It would be nicer to be able to use inheritance for the code // inherited from BuildPhase, but but in Swift 3.0 we get an error that // "declarations in extensions cannot override yet". var dict = try makeBuildPhaseDict( buildPhase: self, serializer: serializer, xcodeClassName: "PBXShellScriptBuildPhase") dict["shellPath"] = .string("/bin/sh") // FIXME: should be settable dict["shellScript"] = .string(script) dict["inputPaths"] = .array(inputs.map { .string($0) }) dict["outputPaths"] = .array(outputs.map { .string($0) }) dict["alwaysOutOfDate"] = .string(alwaysRun ? "1" : "0") return dict } } extension Xcode.BuildFile: PropertyListSerializable { /// Called by the Serializer to serialize the BuildFile. fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `PBXBuildFile` plist dictionary. var dict = [String: PropertyList]() dict["isa"] = .string("PBXBuildFile") if let fileRef = fileRef { dict["fileRef"] = .identifier(serializer.id(of: fileRef)) } let settingsDict = try PropertyList.encode(settings) if !settingsDict.isEmpty { dict["settings"] = settingsDict } return dict } } extension Xcode.BuildSettingsTable: PropertyListSerializable { /// Called by the Serializer to serialize the BuildFile. It is serialized /// as an XCBuildConfigurationList and two additional XCBuildConfiguration /// objects (one for debug and one for release). fileprivate func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { /// Private wrapper class for BuildSettings structures. This is glue /// between our value-based settings structures and the Xcode project /// model's identity-based XCBuildConfiguration objects. class BuildSettingsDictWrapper: PropertyListSerializable { let name: String var baseSettings: BuildSettings var overlaySettings: BuildSettings let xcconfigFileRef: Xcode.FileReference? init( name: String, baseSettings: BuildSettings, overlaySettings: BuildSettings, xcconfigFileRef: Xcode.FileReference? ) { self.name = name self.baseSettings = baseSettings self.overlaySettings = overlaySettings self.xcconfigFileRef = xcconfigFileRef } func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] { // Create a `XCBuildConfiguration` plist dictionary. var dict = [String: PropertyList]() dict["isa"] = .string("XCBuildConfiguration") dict["name"] = .string(name) // Combine the base settings and the overlay settings. dict["buildSettings"] = try combineBuildSettingsPropertyLists( baseSettings: .encode(baseSettings), overlaySettings: .encode(overlaySettings) ) // Add a reference to the base configuration, if there is one. if let xcconfigFileRef = xcconfigFileRef { dict["baseConfigurationReference"] = .identifier(serializer.id(of: xcconfigFileRef)) } return dict } } // Create a `XCConfigurationList` plist dictionary. var dict = [String: PropertyList]() dict["isa"] = .string("XCConfigurationList") dict["buildConfigurations"] = .array([ // We use a private wrapper to "objectify" our two build settings // structures (which, being structs, are value types). try .identifier(serializer.serialize(object: BuildSettingsDictWrapper( name: "Debug", baseSettings: common, overlaySettings: debug, xcconfigFileRef: xcconfigFileRef))), try .identifier(serializer.serialize(object: BuildSettingsDictWrapper( name: "Release", baseSettings: common, overlaySettings: release, xcconfigFileRef: xcconfigFileRef))), ]) // FIXME: What is this, and why are we setting it? dict["defaultConfigurationIsVisible"] = .string("0") // FIXME: Should we allow this to be set in the model? dict["defaultConfigurationName"] = .string("Release") return dict } } /// Private helper function that combines a base property list and an overlay /// property list, respecting the semantics of `$(inherited)` as we go. fileprivate func combineBuildSettingsPropertyLists( baseSettings: PropertyList, overlaySettings: PropertyList ) throws -> PropertyList { // Extract the base and overlay dictionaries. guard case let .dictionary(baseDict) = baseSettings else { throw InternalError("base settings plist must be a dictionary") } guard case let .dictionary(overlayDict) = overlaySettings else { throw InternalError("overlay settings plist must be a dictionary") } // Iterate over the overlay values and apply them to the base. var resultDict = baseDict for (name, value) in overlayDict { if let array = baseDict[name]?.array, let overlayArray = value.array, overlayArray.first?.string == "$(inherited)" { resultDict[name] = .array(array + overlayArray.dropFirst()) } else { resultDict[name] = value } } return .dictionary(resultDict) } /// A simple property list serializer with the same semantics as the Xcode /// property list serializer. Not generally reusable at this point, but only /// because of implementation details (architecturally it isn't tied to Xcode). fileprivate class PropertyListSerializer { /// Private struct that represents a strong reference to a serializable /// object. This prevents any temporary objects from being deallocated /// during the serialization and replaced with other objects having the /// same object identifier (a violation of our assumptions) struct SerializedObjectRef: Hashable, Equatable { let object: PropertyListSerializable init(_ object: PropertyListSerializable) { self.object = object } func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(object)) } static func == (lhs: SerializedObjectRef, rhs: SerializedObjectRef) -> Bool { return lhs.object === rhs.object } } /// Maps objects to the identifiers that have been assigned to them. The /// next identifier to be assigned is always one greater than the number /// of entries in the mapping. var objsToIds = [SerializedObjectRef: String]() /// Maps serialized objects ids to dictionaries. This may contain fewer /// entries than `objsToIds`, since ids are assigned upon reference, but /// plist dictionaries are created only upon actual serialization. This /// dictionary is what gets written to the property list. var idsToDicts = [String: PropertyList]() /// Returns the quoted identifier for the object, assigning one if needed. func id(of object: PropertyListSerializable) -> String { // We need a "serialized object ref" wrapper for the `objsToIds` map. let serObjRef = SerializedObjectRef(object) if let id = objsToIds[serObjRef] { return "\"\(id)\"" } // We currently always assign identifiers starting at 1 and going up. // FIXME: This is a suboptimal format for object identifier strings; // for debugging purposes they should at least sort in numeric order. let id = object.objectID ?? "OBJ_\(objsToIds.count + 1)" objsToIds[serObjRef] = id return "\"\(id)\"" } /// Serializes `object` by asking it to construct a plist dictionary and /// then adding that dictionary to the serializer. This may in turn cause /// recursive invocations of `serialize(object:)`; the closure of these /// invocations end up serializing the whole object graph. @discardableResult func serialize(object: PropertyListSerializable) throws -> String { // Assign an id for the object, if it doesn't already have one. let id = self.id(of: object) // If that id is already in `idsToDicts`, we've detected recursion or // repeated serialization. guard idsToDicts[id] == nil else { throw InternalError("tried to serialize \(object) twice") } // Set a sentinel value in the `idsToDicts` mapping to detect recursion. idsToDicts[id] = .dictionary([:]) // Now recursively serialize the object, and store the result (replacing // the sentinel). idsToDicts[id] = try .dictionary(object.serialize(to: self)) // Finally, return the identifier so the caller can store it (usually in // an attribute in its own serialization dictionary). return id } } fileprivate protocol PropertyListSerializable: AnyObject { /// Called by the Serializer to construct and return a dictionary for a /// serializable object. The entries in the dictionary should represent /// the receiver's attributes and relationships, as PropertyList values. /// /// Every object that is written to the Serializer is assigned an id (an /// arbitrary but unique string). Forward references can use `id(of:)` /// of the Serializer to assign and access the id before the object is /// actually written. /// /// Implementations can use the Serializer's `serialize(object:)` method /// to serialize owned objects (getting an id to the serialized object, /// which can be stored in one of the attributes) or can use the `id(of:)` /// method to store a reference to an unowned object. /// /// The implementation of this method for each serializable objects looks /// something like this: /// /// // Create a `PBXSomeClassOrOther` plist dictionary. /// var dict = [String: PropertyList]() /// dict["isa"] = .string("PBXSomeClassOrOther") /// dict["name"] = .string(name) /// if let path = path { dict["path"] = .string(path) } /// dict["mainGroup"] = .identifier(serializer.serialize(object: mainGroup)) /// dict["subitems"] = .array(subitems.map({ .string($0.id) })) /// dict["cross-ref"] = .identifier(serializer.id(of: unownedObject)) /// return dict /// /// FIXME: I'm not totally happy with how this looks. It's far too clunky /// and could be made more elegant. However, since the Xcode project model /// is static, this is not something that will need to evolve over time. /// What does need to evolve, which is how the project model is constructed /// from the package contents, is where the elegance and simplicity really /// matters. So this is acceptable for now in the interest of getting it /// done. /// A custom ID to use for the instance, if enabled. /// /// This ID must be unique across the entire serialized graph. var objectID: String? { get } /// Should create and return a property list dictionary of the object's /// attributes. This function may also use the serializer's `serialize()` /// function to serialize other objects, and may use `id(of:)` to access /// ids of objects that either have or will be serialized. func serialize(to serializer: PropertyListSerializer) throws -> [String: PropertyList] } extension PropertyListSerializable { var objectID: String? { return nil } } extension PropertyList { var isEmpty: Bool { switch self { case let .identifier(string): return string.isEmpty case let .string(string): return string.isEmpty case let .array(array): return array.isEmpty case let .dictionary(dictionary): return dictionary.isEmpty } } }