diff --git a/Sources/XcodeProj/Extensions/String+md5.swift b/Sources/XcodeProj/Extensions/String+md5.swift index 04be85b4c..35be65b39 100644 --- a/Sources/XcodeProj/Extensions/String+md5.swift +++ b/Sources/XcodeProj/Extensions/String+md5.swift @@ -27,22 +27,34 @@ import Foundation extension String { var md5: String { - guard let data = data(using: .utf8, allowLossyConversion: true) else { - return self - } #if canImport(CryptoKit) if #available(OSX 10.15, iOS 13.0, *) { - return Insecure.MD5.hash(data: data) - .withUnsafeBytes { Array($0) }.hexString - } else { - return data.slowMD5 + // Hashes the UTF-8 bytes in place and writes the hex digits straight into the result, without the + // intermediate `Data`, byte array, and hex buffer. + var string = self + let digest = string.withUTF8 { Insecure.MD5.hash(data: UnsafeRawBufferPointer($0)) } + return digest.withUnsafeBytes { bytes in + String(unsafeUninitializedCapacity: bytes.count * 2) { hex in + for (offset, byte) in bytes.enumerated() { + hex[offset * 2] = hexDigit(byte >> 4) + hex[offset * 2 + 1] = hexDigit(byte & 0xF) + } + return bytes.count * 2 + } + } } - #else - return data.slowMD5 #endif + guard let data = data(using: .utf8, allowLossyConversion: true) else { + return self + } + return data.slowMD5 } } +private func hexDigit(_ value: UInt8) -> UInt8 { + (value > 9) ? (charA + value - 10) : (char0 + value) +} + private let charA = UInt8(UnicodeScalar("a").value) private let char0 = UInt8(UnicodeScalar("0").value) @@ -54,18 +66,14 @@ private extension DataProtocol { for _ in regions { for i in self { - hexChars[Int(offset * 2)] = itoh((i >> 4) & 0xF) - hexChars[Int(offset * 2 + 1)] = itoh(i & 0xF) + hexChars[Int(offset * 2)] = hexDigit((i >> 4) & 0xF) + hexChars[Int(offset * 2 + 1)] = hexDigit(i & 0xF) offset += 1 } } return String(bytes: hexChars, encoding: .utf8)! } - - func itoh(_ value: UInt8) -> UInt8 { - (value > 9) ? (charA + value - 10) : (char0 + value) - } } private extension Data { diff --git a/Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift b/Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift index 094132d79..830029c7c 100644 --- a/Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift +++ b/Sources/XcodeProj/Objects/BuildPhase/PBXBuildFile.swift @@ -186,7 +186,8 @@ final class PBXBuildPhaseFile: PlistSerializable, Equatable { } func plistKeyAndValue(proj _: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) { - var dictionary: [CommentedString: PlistValue] = [:] + // Room for every key below, so the dictionary isn't regrown while it's filled. + var dictionary = [CommentedString: PlistValue](minimumCapacity: 6) dictionary["isa"] = .string(CommentedString(PBXBuildFile.isa)) if let fileReference = buildFile.fileReference { let fileElement: PBXFileElement? = fileReference.getObject() diff --git a/Sources/XcodeProj/Objects/Project/PBXObject.swift b/Sources/XcodeProj/Objects/Project/PBXObject.swift index 6f205084a..bff10cda5 100644 --- a/Sources/XcodeProj/Objects/Project/PBXObject.swift +++ b/Sources/XcodeProj/Objects/Project/PBXObject.swift @@ -57,7 +57,8 @@ public class PBXObject: Hashable, Decodable, Equatable, AutoEquatable { /// Object isa (type id) public static var isa: String { - String(describing: self) + // What `String(describing: self)` prints for a class, without the reflection it goes through first. + _typeName(self, qualified: false) } public static func == (lhs: PBXObject, diff --git a/Sources/XcodeProj/Objects/Project/PBXObjectReference.swift b/Sources/XcodeProj/Objects/Project/PBXObjectReference.swift index 3f544af26..8bfa39bc8 100644 --- a/Sources/XcodeProj/Objects/Project/PBXObjectReference.swift +++ b/Sources/XcodeProj/Objects/Project/PBXObjectReference.swift @@ -7,7 +7,12 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { private(set) var temporary: Bool /// String reference. - private(set) var value: String + private(set) var value: String { + didSet { valueHash = value.hashValue } + } + + /// The hash of `value`, which dictionaries keyed by references ask for again every time they move an entry. + private var valueHash: Int /// Weak reference to the objects instance that contains the project objects. weak var objects: PBXObjects? @@ -20,6 +25,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { /// - Parameter reference: reference. init(_ reference: String, objects: PBXObjects) { value = reference + valueHash = reference.hashValue temporary = false self.objects = objects } @@ -27,6 +33,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { /// Initializes a temporary reference init(objects: PBXObjects? = nil) { value = "TEMP_\(UUID().uuidString)" + valueHash = value.hashValue temporary = true self.objects = objects } @@ -36,6 +43,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { /// - Parameter reference: reference. init(_ reference: String) { value = reference + valueHash = reference.hashValue temporary = false } @@ -44,6 +52,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { /// - Parameter objectReference: object reference to be initialized from. required init(_ objectReference: PBXObjectReference) { value = objectReference.value + valueHash = objectReference.valueHash temporary = objectReference.temporary } @@ -51,7 +60,8 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { /// /// - Parameter value: value. func fix(_ value: String) { - let object = objects?.delete(reference: self) + // The referenced object's type says which collection holds it, which saves probing every collection. + let object = object.flatMap { objects?.remove(reference: self, typeOf: $0) } ?? objects?.delete(reference: self) self.value = value temporary = false if let object { @@ -71,7 +81,7 @@ class PBXObjectReference: NSObject, Comparable, NSCopying { /// Hash value. override var hash: Int { - value.hashValue + valueHash } func copy(with _: NSZone? = nil) -> Any { diff --git a/Sources/XcodeProj/Objects/Project/PBXObjects.swift b/Sources/XcodeProj/Objects/Project/PBXObjects.swift index 9b7b13691..7575d6bf9 100644 --- a/Sources/XcodeProj/Objects/Project/PBXObjects.swift +++ b/Sources/XcodeProj/Objects/Project/PBXObjects.swift @@ -208,40 +208,70 @@ class PBXObjects: Equatable { } let objectReference: PBXObjectReference = object.reference objectReference.objects = self + update(object, reference: objectReference, inserting: true) + } + + /// Removes the object stored under `reference` from the collection that objects of `object`'s type belong to. + /// Unlike `delete(reference:)`, it doesn't probe every collection. + /// + /// - Returns: the removed object, or nil when that collection has nothing under `reference`. + func remove(reference: PBXObjectReference, typeOf object: PBXObject) -> PBXObject? { + lock.lock() + defer { lock.unlock() } + return update(object, reference: reference, inserting: false) + } + + /// Inserts `object` under `reference` into the collection its type belongs to, or removes what that collection + /// stores under `reference`, so `add` and `remove` always agree on the collection. + @discardableResult private func update( + _ object: PBXObject, + reference: PBXObjectReference, + inserting: Bool + ) -> PBXObject? { + func update(_ objects: inout [PBXObjectReference: T], _ object: T) -> PBXObject? { + if inserting { + objects[reference] = object + return object + } + return objects.removeValue(forKey: reference) + } switch object { // subclasses of PBXGroup; must be tested before PBXGroup - case let object as PBXVariantGroup: _variantGroups[objectReference] = object - case let object as XCVersionGroup: _versionGroups[objectReference] = object + case let object as PBXVariantGroup: return update(&_variantGroups, object) + case let object as XCVersionGroup: return update(&_versionGroups, object) // everything else - case let object as PBXBuildFile: _buildFiles[objectReference] = object - case let object as PBXAggregateTarget: _aggregateTargets[objectReference] = object - case let object as PBXLegacyTarget: _legacyTargets[objectReference] = object - case let object as PBXContainerItemProxy: _containerItemProxies[objectReference] = object - case let object as PBXCopyFilesBuildPhase: _copyFilesBuildPhases[objectReference] = object - case let object as PBXGroup: _groups[objectReference] = object - case let object as XCConfigurationList: _configurationLists[objectReference] = object - case let object as XCBuildConfiguration: _buildConfigurations[objectReference] = object - case let object as PBXTargetDependency: _targetDependencies[objectReference] = object - case let object as PBXSourcesBuildPhase: _sourcesBuildPhases[objectReference] = object - case let object as PBXShellScriptBuildPhase: _shellScriptBuildPhases[objectReference] = object - case let object as PBXResourcesBuildPhase: _resourcesBuildPhases[objectReference] = object - case let object as PBXFrameworksBuildPhase: _frameworksBuildPhases[objectReference] = object - case let object as PBXHeadersBuildPhase: _headersBuildPhases[objectReference] = object - case let object as PBXNativeTarget: _nativeTargets[objectReference] = object - case let object as PBXFileReference: _fileReferences[objectReference] = object - case let object as PBXProject: _projects[objectReference] = object - case let object as PBXReferenceProxy: _referenceProxies[objectReference] = object - case let object as PBXRezBuildPhase: _carbonResourcesBuildPhases[objectReference] = object - case let object as PBXBuildRule: _buildRules[objectReference] = object - case let object as XCRemoteSwiftPackageReference: _remoteSwiftPackageReferences[objectReference] = object - case let object as XCLocalSwiftPackageReference: _localSwiftPackageReferences[objectReference] = object - case let object as XCSwiftPackageProductDependency: _swiftPackageProductDependencies[objectReference] = object - case let object as PBXFileSystemSynchronizedRootGroup: _fileSystemSynchronizedRootGroups[objectReference] = object - case let object as PBXFileSystemSynchronizedBuildFileExceptionSet: _fileSystemSynchronizedBuildFileExceptionSets[objectReference] = object + case let object as PBXBuildFile: return update(&_buildFiles, object) + case let object as PBXAggregateTarget: return update(&_aggregateTargets, object) + case let object as PBXLegacyTarget: return update(&_legacyTargets, object) + case let object as PBXContainerItemProxy: return update(&_containerItemProxies, object) + case let object as PBXCopyFilesBuildPhase: return update(&_copyFilesBuildPhases, object) + case let object as PBXGroup: return update(&_groups, object) + case let object as XCConfigurationList: return update(&_configurationLists, object) + case let object as XCBuildConfiguration: return update(&_buildConfigurations, object) + case let object as PBXTargetDependency: return update(&_targetDependencies, object) + case let object as PBXSourcesBuildPhase: return update(&_sourcesBuildPhases, object) + case let object as PBXShellScriptBuildPhase: return update(&_shellScriptBuildPhases, object) + case let object as PBXResourcesBuildPhase: return update(&_resourcesBuildPhases, object) + case let object as PBXFrameworksBuildPhase: return update(&_frameworksBuildPhases, object) + case let object as PBXHeadersBuildPhase: return update(&_headersBuildPhases, object) + case let object as PBXNativeTarget: return update(&_nativeTargets, object) + case let object as PBXFileReference: return update(&_fileReferences, object) + case let object as PBXProject: return update(&_projects, object) + case let object as PBXReferenceProxy: return update(&_referenceProxies, object) + case let object as PBXRezBuildPhase: return update(&_carbonResourcesBuildPhases, object) + case let object as PBXBuildRule: return update(&_buildRules, object) + case let object as XCRemoteSwiftPackageReference: return update(&_remoteSwiftPackageReferences, object) + case let object as XCLocalSwiftPackageReference: return update(&_localSwiftPackageReferences, object) + case let object as XCSwiftPackageProductDependency: return update(&_swiftPackageProductDependencies, object) + case let object as PBXFileSystemSynchronizedRootGroup: return update(&_fileSystemSynchronizedRootGroups, object) + case let object as PBXFileSystemSynchronizedBuildFileExceptionSet: return update(&_fileSystemSynchronizedBuildFileExceptionSets, object) case let object as PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: - _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet[objectReference] = object - default: fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo") + return update(&_fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet, object) + default: + // Objects of these types are never added, so there is nothing to remove. + guard inserting else { return nil } + fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo") } } @@ -430,17 +460,15 @@ extension PBXObjects { // This is used to decode build files. (we need the name of the build phase) // Otherwise, we would have to go through all the build phases for each file. var buildPhaseFile: [PBXObjectReference: PBXBuildPhaseFile] { - let values: [[PBXBuildPhaseFile]] = buildPhases.values.map { buildPhase in - let files = buildPhase.files - let buildPhaseFile: [PBXBuildPhaseFile] = files?.compactMap { (file: PBXBuildFile) -> PBXBuildPhaseFile in - PBXBuildPhaseFile( - buildFile: file, - buildPhase: buildPhase - ) - } ?? [] - return buildPhaseFile + // A build file listed by several phases maps to the first of them. Visiting the phases and files backwards lets + // each entry be overwritten without checking it first, so the first occurrence is the one that stays. + var buildPhaseFile = [PBXObjectReference: PBXBuildPhaseFile](minimumCapacity: buildFiles.count) + for buildPhase in buildPhases.values.reversed() { + for file in (buildPhase.files ?? []).reversed() { + buildPhaseFile[file.reference] = PBXBuildPhaseFile(buildFile: file, buildPhase: buildPhase) + } } - return Dictionary(values.flatMap { $0 }.map { ($0.buildFile.reference, $0) }, uniquingKeysWith: { first, _ in first }) + return buildPhaseFile } /// Runs the given closure for each of the objects that are part of the project. diff --git a/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift b/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift index fa8710ffc..311bad70a 100644 --- a/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift +++ b/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift @@ -374,9 +374,11 @@ final class PBXProjEncoder { } } - let joinedArray: [String] = resultArray.flatMap { $0 } // should check multiline - output.append(contentsOf: joinedArray) + // Appended element by element, which saves flattening them into a temporary array first. + for lines in resultArray { + output.append(contentsOf: lines) + } write(string: "/* End \(section) section */", to: &output) writeNewLine(stateHolder: &stateHolder, to: &output) } diff --git a/Sources/XcodeProj/Utils/ReferenceGenerator.swift b/Sources/XcodeProj/Utils/ReferenceGenerator.swift index b197c55f4..221352355 100644 --- a/Sources/XcodeProj/Utils/ReferenceGenerator.swift +++ b/Sources/XcodeProj/Utils/ReferenceGenerator.swift @@ -16,6 +16,8 @@ protocol ReferenceGenerating: AnyObject { final class ReferenceGenerator: ReferenceGenerating { let outputSettings: PBXOutputSettings var references: Set = [] + /// The type name and reference acronym of each object type, which only depend on the type. + private var typeNamesAndAcronyms: [ObjectIdentifier: (typeName: String, acronym: String)] = [:] init(outputSettings: PBXOutputSettings) { self.outputSettings = outputSettings @@ -386,14 +388,7 @@ extension ReferenceGenerator { if let context = object.context { identifiers.append(context) } - let typeName = String(describing: type(of: object)) - - // Get acronym to be used as prefix for the reference. - // PBXFileReference is turned to FR. - let acronym = typeName - .replacingOccurrences(of: "PBX", with: "") - .replacingOccurrences(of: "XC", with: "") - .filter { String($0).lowercased() != String($0) } + let (typeName, acronym) = typeNameAndAcronym(of: object) var reference = "" var counter = 0 @@ -410,6 +405,21 @@ extension ReferenceGenerator { } } + private func typeNameAndAcronym(of object: some PBXObject) -> (typeName: String, acronym: String) { + let objectType = type(of: object) + if let cached = typeNamesAndAcronyms[ObjectIdentifier(objectType)] { return cached } + let typeName = String(describing: objectType) + + // Get acronym to be used as prefix for the reference. + // PBXFileReference is turned to FR. + let acronym = typeName + .replacingOccurrences(of: "PBX", with: "") + .replacingOccurrences(of: "XC", with: "") + .filter { String($0).lowercased() != String($0) } + typeNamesAndAcronyms[ObjectIdentifier(objectType)] = (typeName, acronym) + return (typeName, acronym) + } + private func generateReferenceFrom(acronym: String, typeName: String, identifiers: [String], diff --git a/Tests/XcodeProjTests/Utils/ReferenceFixingTests.swift b/Tests/XcodeProjTests/Utils/ReferenceFixingTests.swift new file mode 100644 index 000000000..1d365c82f --- /dev/null +++ b/Tests/XcodeProjTests/Utils/ReferenceFixingTests.swift @@ -0,0 +1,195 @@ +import Foundation +import PathKit +import Testing +@testable import XcodeProj + +/// Covers the bookkeeping that runs when temporary references are made permanent before a project is written: the +/// collection each object lives in, the generated reference values, and the digests and type names they are made of. +struct ReferenceFixingTests { + @Test func md5_matchesKnownDigests() { + // RFC 1321 test suite, plus multi-byte UTF-8 strings checked with `md5 -s`. + #expect("".md5 == "d41d8cd98f00b204e9800998ecf8427e") + #expect("abc".md5 == "900150983cd24fb0d6963f7d28e17f72") + #expect("message digest".md5 == "f96b697d7cb7938d525a2f31aaf161d0") + #expect("é".md5 == "66ddcd97cfdeabb2f6fb8a999b4bc76f") + #expect("PBXBuildFile-Target-Ünïcödé-🚀".md5 == "74d6591936a96f0ff81738e1b8e80f6d") + } + + @Test func md5_hashesBridgedStringsByTheirUTF8Bytes() { + let bridged = NSMutableString(string: "PBXBuildFile-Target-") + bridged.append("Ünïcödé-🚀") + + #expect((bridged as String).md5 == "74d6591936a96f0ff81738e1b8e80f6d") + } + + @Test func isa_isTheUnqualifiedClassName() { + #expect(PBXObject.isa == "PBXObject") + #expect(PBXBuildFile.isa == "PBXBuildFile") + #expect(PBXVariantGroup.isa == "PBXVariantGroup") + #expect(XCVersionGroup.isa == "XCVersionGroup") + #expect(PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.isa == "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet") + let groupType: PBXFileElement.Type = PBXVariantGroup.self + #expect(groupType.isa == "PBXVariantGroup") + } + + #if os(macOS) || (os(Linux) && compiler(>=6.1)) + /// Writing a project whose references are all temporary moves every object it decodes to a permanent reference. + @Test(arguments: [ + "iOS/Project.xcodeproj", + "iOS/AppWithExtensions/AppWithExtensions.xcodeproj", + "iOS/ProjectWithXCLocalSwiftPackageReferences.xcodeproj", + "SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj", + "TargetWithCustomBuildRules/TargetWithCustomBuildRules.xcodeproj", + "Xcode16ProjectReferenceOrder/Test.xcodeproj", + ]) + func writingAProjectWithTemporaryReferences_keepsEveryObject(fixture: String) throws { + let pbxproj = try PBXProj(path: fixturesPath() + fixture + "project.pbxproj") + let objects = objectCountsByType(pbxproj.objects) + pbxproj.objects.invalidateReferences() + + let data = try #require(try pbxproj.dataRepresentation()) + let written = try PBXProj(data: data) + + #expect(objectCountsByType(written.objects) == objects) + #expect(!String(decoding: data, as: UTF8.self).contains("TEMP_")) + } + #endif + + @Test func fixingReferences_movesEachObjectToItsNewReference() throws { + let pbxproj = PBXProj() + let objects = makeOneObjectOfEachType() + objects.forEach { pbxproj.add(object: $0.object) } + // One generator for every type, so the type name and prefix it caches for one type can't leak into another. + let generator = ReferenceGenerator(outputSettings: PBXOutputSettings(projReferenceFormat: .withPrefixAndSuffix)) + + for (object, typeName, acronym) in objects { + let temporaryValue = object.reference.value + generator.fixReference(for: object, identifiers: ["id"]) + + #expect(object.reference.value == "\(acronym)_\("\(typeName)-id".md5.uppercased())") + #expect(pbxproj.objects.get(reference: PBXObjectReference(object.reference.value)) === object) + #expect(pbxproj.objects.get(reference: PBXObjectReference(temporaryValue)) == nil) + } + #expect(objectCount(pbxproj.objects) == objects.count) + } + + @Test func fixingAReference_whoseObjectTypeHasNoCollection_leavesTheCollectionsUnchanged() { + let pbxproj = PBXProj() + pbxproj.add(object: PBXFileReference(name: "A.swift")) + let element = PBXFileElement(name: "Element") + element.reference.objects = pbxproj.objects + + element.reference.fix("ELEMENT") + + #expect(element.reference.value == "ELEMENT") + #expect(pbxproj.objects.get(reference: PBXObjectReference("ELEMENT")) == nil) + #expect(objectCount(pbxproj.objects) == 1) + } + + @Test func referenceHash_followsItsValue() throws { + let reference = PBXObjectReference() + + reference.fix("A") + #expect(reference.hash == PBXObjectReference("A").hash) + #expect(Set([PBXObjectReference("A")]).contains(reference)) + + reference.invalidate() + #expect(reference.hash == PBXObjectReference(reference.value).hash) + #expect(!Set([PBXObjectReference("A")]).contains(reference)) + + let copy = try #require(reference.copy() as? PBXObjectReference) + #expect(copy == reference) + #expect(copy.hash == reference.hash) + } + + @Test func buildPhaseFile_hasOneEntryPerBuildFile_inAPhaseThatListsIt() throws { + let pbxproj = PBXProj() + let fileA = PBXFileReference(name: "A.swift") + let fileB = PBXFileReference(name: "B.png") + let buildFileA = PBXBuildFile(file: fileA) + let buildFileB = PBXBuildFile(file: fileB) + let sources = PBXSourcesBuildPhase(files: [buildFileA, buildFileB, buildFileA]) + let resources = PBXResourcesBuildPhase(files: [buildFileB]) + [fileA, fileB, buildFileA, buildFileB, sources, resources].forEach { pbxproj.add(object: $0) } + + let buildPhaseFile = pbxproj.objects.buildPhaseFile + + #expect(buildPhaseFile.count == 2) + #expect(buildPhaseFile[buildFileA.reference]?.buildFile === buildFileA) + #expect(buildPhaseFile[buildFileA.reference]?.buildPhase === sources) + let phaseOfB = try #require(buildPhaseFile[buildFileB.reference]?.buildPhase) + #expect(phaseOfB === sources || phaseOfB === resources) + } + + // MARK: - Helpers + + private func objectCountsByType(_ objects: PBXObjects) -> [String: Int] { + var counts: [String: Int] = [:] + objects.forEach { counts[String(describing: type(of: $0)), default: 0] += 1 } + return counts + } + + private func objectCount(_ objects: PBXObjects) -> Int { + var count = 0 + objects.forEach { _ in count += 1 } + return count + } + + /// One object of every type `PBXObjects` stores, with the type name and reference prefix the generator should use. + private func makeOneObjectOfEachType() -> [(object: PBXObject, typeName: String, acronym: String)] { + let fileReference = PBXFileReference(name: "A.swift") + let target = PBXNativeTarget(name: "App") + let sourcesBuildPhase = PBXSourcesBuildPhase() + let configurationList = XCConfigurationList() + let remotePackage = XCRemoteSwiftPackageReference(repositoryURL: "https://example.com/package") + let containerItemProxy = PBXContainerItemProxy(containerPortal: .fileReference(fileReference)) + return [ + (fileReference, "PBXFileReference", "FR"), + (target, "PBXNativeTarget", "NT"), + (sourcesBuildPhase, "PBXSourcesBuildPhase", "SBP"), + (configurationList, "XCConfigurationList", "CL"), + (remotePackage, "XCRemoteSwiftPackageReference", "RSPR"), + (containerItemProxy, "PBXContainerItemProxy", "CIP"), + (PBXBuildFile(file: fileReference), "PBXBuildFile", "BF"), + (PBXAggregateTarget(name: "Aggregate"), "PBXAggregateTarget", "AT"), + (PBXLegacyTarget(name: "Legacy"), "PBXLegacyTarget", "LT"), + (PBXCopyFilesBuildPhase(), "PBXCopyFilesBuildPhase", "CFBP"), + (PBXGroup(), "PBXGroup", "G"), + (PBXVariantGroup(), "PBXVariantGroup", "VG"), + (XCVersionGroup(), "XCVersionGroup", "VG"), + (XCBuildConfiguration(name: "Debug"), "XCBuildConfiguration", "BC"), + (PBXTargetDependency(target: target), "PBXTargetDependency", "TD"), + (PBXShellScriptBuildPhase(), "PBXShellScriptBuildPhase", "SSBP"), + (PBXResourcesBuildPhase(), "PBXResourcesBuildPhase", "RBP"), + (PBXFrameworksBuildPhase(), "PBXFrameworksBuildPhase", "FBP"), + (PBXHeadersBuildPhase(), "PBXHeadersBuildPhase", "HBP"), + (PBXRezBuildPhase(), "PBXRezBuildPhase", "RBP"), + (PBXProject( + name: "Project", + buildConfigurationList: configurationList, + compatibilityVersion: nil, + preferredProjectObjectVersion: nil, + minimizedProjectReferenceProxies: nil, + mainGroup: PBXGroup() + ), "PBXProject", "P"), + (PBXReferenceProxy(remote: containerItemProxy), "PBXReferenceProxy", "RP"), + (PBXBuildRule(compilerSpec: "spec", fileType: "type"), "PBXBuildRule", "BR"), + (XCLocalSwiftPackageReference(relativePath: "Package"), "XCLocalSwiftPackageReference", "LSPR"), + (XCSwiftPackageProductDependency(productName: "Product", package: remotePackage), "XCSwiftPackageProductDependency", "SPPD"), + (PBXFileSystemSynchronizedRootGroup(path: "Sources"), "PBXFileSystemSynchronizedRootGroup", "FSSRG"), + (PBXFileSystemSynchronizedBuildFileExceptionSet( + target: target, + membershipExceptions: nil, + publicHeaders: nil, + privateHeaders: nil, + additionalCompilerFlagsByRelativePath: nil, + attributesByRelativePath: nil + ), "PBXFileSystemSynchronizedBuildFileExceptionSet", "FSSBFES"), + (PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet( + buildPhase: sourcesBuildPhase, + membershipExceptions: nil, + attributesByRelativePath: nil + ), "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet", "FSSGBPMES"), + ] + } +}