From eb642942e1eaf6c77b1a1cbc0bc577d557a78d0d Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 8 Aug 2024 14:04:52 +0200 Subject: [PATCH] fix: Fix non-determinism with the packageProductDependencies attirbute of PBXTarget --- .../Objects/Project/PBXObjects.swift | 147 ++++++++++-------- .../Objects/Project/PBXProject.swift | 4 +- .../XcodeProj/Objects/Targets/PBXTarget.swift | 28 ++-- .../XcodeProj/Utils/ReferenceGenerator.swift | 2 +- .../Objects/Project/PBXProjectTests.swift | 6 +- ....swift => XcodeProjIntegrationTests.swift} | 9 ++ 6 files changed, 112 insertions(+), 84 deletions(-) rename Tests/XcodeProjTests/Project/{XcodeProjTests.swift => XcodeProjIntegrationTests.swift} (89%) diff --git a/Sources/XcodeProj/Objects/Project/PBXObjects.swift b/Sources/XcodeProj/Objects/Project/PBXObjects.swift index b153b9fc..6e7d4157 100644 --- a/Sources/XcodeProj/Objects/Project/PBXObjects.swift +++ b/Sources/XcodeProj/Objects/Project/PBXObjects.swift @@ -3,133 +3,133 @@ import Foundation // swiftlint:disable type_body_length class PBXObjects: Equatable { // MARK: - Properties - + private let lock = NSRecursiveLock() - + private var _projects: [PBXObjectReference: PBXProject] = [:] var projects: [PBXObjectReference: PBXProject] { lock.whileLocked { _projects } } - + private var _referenceProxies: [PBXObjectReference: PBXReferenceProxy] = [:] var referenceProxies: [PBXObjectReference: PBXReferenceProxy] { lock.whileLocked { _referenceProxies } } - + // File elements private var _fileReferences: [PBXObjectReference: PBXFileReference] = [:] var fileReferences: [PBXObjectReference: PBXFileReference] { lock.whileLocked { _fileReferences } } - + private var _versionGroups: [PBXObjectReference: XCVersionGroup] = [:] var versionGroups: [PBXObjectReference: XCVersionGroup] { lock.whileLocked { _versionGroups } } - + private var _variantGroups: [PBXObjectReference: PBXVariantGroup] = [:] var variantGroups: [PBXObjectReference: PBXVariantGroup] { lock.whileLocked { _variantGroups } } - + private var _groups: [PBXObjectReference: PBXGroup] = [:] var groups: [PBXObjectReference: PBXGroup] { lock.whileLocked { _groups } } - + // Configuration private var _buildConfigurations: [PBXObjectReference: XCBuildConfiguration] = [:] var buildConfigurations: [PBXObjectReference: XCBuildConfiguration] { lock.whileLocked { _buildConfigurations } } - + private var _configurationLists: [PBXObjectReference: XCConfigurationList] = [:] var configurationLists: [PBXObjectReference: XCConfigurationList] { lock.whileLocked { _configurationLists } } - + // Targets private var _legacyTargets: [PBXObjectReference: PBXLegacyTarget] = [:] var legacyTargets: [PBXObjectReference: PBXLegacyTarget] { lock.whileLocked { _legacyTargets } } - + private var _aggregateTargets: [PBXObjectReference: PBXAggregateTarget] = [:] var aggregateTargets: [PBXObjectReference: PBXAggregateTarget] { lock.whileLocked { _aggregateTargets } } - + private var _nativeTargets: [PBXObjectReference: PBXNativeTarget] = [:] var nativeTargets: [PBXObjectReference: PBXNativeTarget] { lock.whileLocked { _nativeTargets } } - + private var _targetDependencies: [PBXObjectReference: PBXTargetDependency] = [:] var targetDependencies: [PBXObjectReference: PBXTargetDependency] { lock.whileLocked { _targetDependencies } } - + private var _containerItemProxies: [PBXObjectReference: PBXContainerItemProxy] = [:] var containerItemProxies: [PBXObjectReference: PBXContainerItemProxy] { lock.whileLocked { _containerItemProxies } } - + private var _buildRules: [PBXObjectReference: PBXBuildRule] = [:] var buildRules: [PBXObjectReference: PBXBuildRule] { lock.whileLocked { _buildRules } } - + // Build Phases private var _buildFiles: [PBXObjectReference: PBXBuildFile] = [:] var buildFiles: [PBXObjectReference: PBXBuildFile] { lock.whileLocked { _buildFiles } } - + private var _copyFilesBuildPhases: [PBXObjectReference: PBXCopyFilesBuildPhase] = [:] var copyFilesBuildPhases: [PBXObjectReference: PBXCopyFilesBuildPhase] { lock.whileLocked { _copyFilesBuildPhases } } - + private var _shellScriptBuildPhases: [PBXObjectReference: PBXShellScriptBuildPhase] = [:] var shellScriptBuildPhases: [PBXObjectReference: PBXShellScriptBuildPhase] { lock.whileLocked { _shellScriptBuildPhases } } - + private var _resourcesBuildPhases: [PBXObjectReference: PBXResourcesBuildPhase] = [:] var resourcesBuildPhases: [PBXObjectReference: PBXResourcesBuildPhase] { lock.whileLocked { _resourcesBuildPhases } } - + private var _frameworksBuildPhases: [PBXObjectReference: PBXFrameworksBuildPhase] = [:] var frameworksBuildPhases: [PBXObjectReference: PBXFrameworksBuildPhase] { lock.whileLocked { _frameworksBuildPhases } } - + private var _headersBuildPhases: [PBXObjectReference: PBXHeadersBuildPhase] = [:] var headersBuildPhases: [PBXObjectReference: PBXHeadersBuildPhase] { lock.whileLocked { _headersBuildPhases } } - + private var _sourcesBuildPhases: [PBXObjectReference: PBXSourcesBuildPhase] = [:] var sourcesBuildPhases: [PBXObjectReference: PBXSourcesBuildPhase] { lock.whileLocked { _sourcesBuildPhases } } - + private var _carbonResourcesBuildPhases: [PBXObjectReference: PBXRezBuildPhase] = [:] var carbonResourcesBuildPhases: [PBXObjectReference: PBXRezBuildPhase] { lock.whileLocked { _carbonResourcesBuildPhases } } - + private var _remoteSwiftPackageReferences: [PBXObjectReference: XCRemoteSwiftPackageReference] = [:] var remoteSwiftPackageReferences: [PBXObjectReference: XCRemoteSwiftPackageReference] { lock.whileLocked { _remoteSwiftPackageReferences } } - + private var _localSwiftPackageReferences: [PBXObjectReference: XCLocalSwiftPackageReference] = [:] var localSwiftPackageReferences: [PBXObjectReference: XCLocalSwiftPackageReference] { lock.whileLocked { _localSwiftPackageReferences } } - + private var _swiftPackageProductDependencies: [PBXObjectReference: XCSwiftPackageProductDependency] = [:] var swiftPackageProductDependencies: [PBXObjectReference: XCSwiftPackageProductDependency] { lock.whileLocked { _swiftPackageProductDependencies } @@ -144,9 +144,9 @@ class PBXObjects: Equatable { var fileSystemSynchronizedBuildFileExceptionSets: [PBXObjectReference: PBXFileSystemSynchronizedBuildFileExceptionSet] { lock.whileLocked { _fileSystemSynchronizedBuildFileExceptionSets } } - + // XCSwiftPackageProductDependency - + /// Initializes the project objects container /// /// - Parameters: @@ -156,38 +156,40 @@ class PBXObjects: Equatable { self.add(object: $0) } } - + // MARK: - Equatable - + public static func == (lhs: PBXObjects, rhs: PBXObjects) -> Bool { lhs.buildFiles == rhs.buildFiles && - lhs.legacyTargets == rhs.legacyTargets && - lhs.aggregateTargets == rhs.aggregateTargets && - lhs.containerItemProxies == rhs.containerItemProxies && - lhs.copyFilesBuildPhases == rhs.copyFilesBuildPhases && - lhs.groups == rhs.groups && - lhs.configurationLists == rhs.configurationLists && - lhs.buildConfigurations == rhs.buildConfigurations && - lhs.variantGroups == rhs.variantGroups && - lhs.targetDependencies == rhs.targetDependencies && - lhs.sourcesBuildPhases == rhs.sourcesBuildPhases && - lhs.shellScriptBuildPhases == rhs.shellScriptBuildPhases && - lhs.resourcesBuildPhases == rhs.resourcesBuildPhases && - lhs.frameworksBuildPhases == rhs.frameworksBuildPhases && - lhs.headersBuildPhases == rhs.headersBuildPhases && - lhs.nativeTargets == rhs.nativeTargets && - lhs.fileReferences == rhs.fileReferences && - lhs.projects == rhs.projects && - lhs.versionGroups == rhs.versionGroups && - lhs.referenceProxies == rhs.referenceProxies && - lhs.carbonResourcesBuildPhases == rhs.carbonResourcesBuildPhases && - lhs.buildRules == rhs.buildRules && - lhs.swiftPackageProductDependencies == rhs._swiftPackageProductDependencies && - lhs.remoteSwiftPackageReferences == rhs.remoteSwiftPackageReferences + lhs.legacyTargets == rhs.legacyTargets && + lhs.aggregateTargets == rhs.aggregateTargets && + lhs.containerItemProxies == rhs.containerItemProxies && + lhs.copyFilesBuildPhases == rhs.copyFilesBuildPhases && + lhs.groups == rhs.groups && + lhs.configurationLists == rhs.configurationLists && + lhs.buildConfigurations == rhs.buildConfigurations && + lhs.variantGroups == rhs.variantGroups && + lhs.targetDependencies == rhs.targetDependencies && + lhs.sourcesBuildPhases == rhs.sourcesBuildPhases && + lhs.shellScriptBuildPhases == rhs.shellScriptBuildPhases && + lhs.resourcesBuildPhases == rhs.resourcesBuildPhases && + lhs.frameworksBuildPhases == rhs.frameworksBuildPhases && + lhs.headersBuildPhases == rhs.headersBuildPhases && + lhs.nativeTargets == rhs.nativeTargets && + lhs.fileReferences == rhs.fileReferences && + lhs.projects == rhs.projects && + lhs.versionGroups == rhs.versionGroups && + lhs.referenceProxies == rhs.referenceProxies && + lhs.carbonResourcesBuildPhases == rhs.carbonResourcesBuildPhases && + lhs.buildRules == rhs.buildRules && + lhs.swiftPackageProductDependencies == rhs._swiftPackageProductDependencies && + lhs.remoteSwiftPackageReferences == rhs.remoteSwiftPackageReferences && + lhs.fileSystemSynchronizedRootGroups == rhs.fileSystemSynchronizedRootGroups && + lhs.fileSystemSynchronizedBuildFileExceptionSets == rhs.fileSystemSynchronizedBuildFileExceptionSets } - + // MARK: - Helpers - + /// Add a new object. /// /// - Parameters: @@ -199,13 +201,13 @@ class PBXObjects: Equatable { } let objectReference: PBXObjectReference = object.reference objectReference.objects = self - + switch object { - // subclasses of PBXGroup; must be tested before PBXGroup + // subclasses of PBXGroup; must be tested before PBXGroup case let object as PBXVariantGroup: _variantGroups[objectReference] = object case let object as XCVersionGroup: _versionGroups[objectReference] = object - - // everything else + + // 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 @@ -231,11 +233,11 @@ class PBXObjects: Equatable { case let object as XCSwiftPackageProductDependency: _swiftPackageProductDependencies[objectReference] = object case let object as PBXFileSystemSynchronizedRootGroup: _fileSystemSynchronizedRootGroups[objectReference] = object case let object as PBXFileSystemSynchronizedBuildFileExceptionSet: _fileSystemSynchronizedBuildFileExceptionSets[objectReference] = object - + default: fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo") } } - + /// Deletes the object with the given reference. /// /// - Parameter reference: referenc of the object to be deleted. @@ -292,10 +294,15 @@ class PBXObjects: Equatable { return _remoteSwiftPackageReferences.remove(at: index).value } else if let index = swiftPackageProductDependencies.index(forKey: reference) { return _swiftPackageProductDependencies.remove(at: index).value + } else if let index = fileSystemSynchronizedRootGroups.index(forKey: reference) { + return _fileSystemSynchronizedRootGroups.remove(at: index).value + } else if let index = fileSystemSynchronizedBuildFileExceptionSets.index(forKey: reference) { + return _fileSystemSynchronizedBuildFileExceptionSets.remove(at: index).value } + return nil } - + /// It returns the object with the given reference. /// /// - Parameter reference: Xcode reference. @@ -354,6 +361,10 @@ class PBXObjects: Equatable { return object } else if let object = swiftPackageProductDependencies[reference] { return object + } else if let object = fileSystemSynchronizedRootGroups[reference] { + return object + } else if let object = fileSystemSynchronizedBuildFileExceptionSets[reference] { + return object } else { return nil } @@ -378,16 +389,16 @@ extension PBXObjects { targets.append(contentsOf: filter(aggregateTargets)) return targets } - + /// Invalidates all the objects references. func invalidateReferences() { forEach { $0.reference.invalidate() } } - + // MARK: - Computed Properties - + var buildPhases: [PBXObjectReference: PBXBuildPhase] { var phases: [PBXObjectReference: PBXBuildPhase] = [:] phases.merge(copyFilesBuildPhases as [PBXObjectReference: PBXBuildPhase], uniquingKeysWith: { first, _ in first }) @@ -399,7 +410,7 @@ extension PBXObjects { phases.merge(frameworksBuildPhases as [PBXObjectReference: PBXBuildPhase], uniquingKeysWith: { first, _ in first }) return phases } - + // This dictionary is used to quickly get a connection between the build phase and the build files of this phase. // 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. @@ -416,7 +427,7 @@ extension PBXObjects { } return Dictionary(values.flatMap { $0 }.map { ($0.buildFile.reference, $0) }, uniquingKeysWith: { first, _ in first }) } - + /// Runs the given closure for each of the objects that are part of the project. /// /// - Parameter closure: closure to be run. @@ -445,5 +456,7 @@ extension PBXObjects { carbonResourcesBuildPhases.values.forEach(closure) remoteSwiftPackageReferences.values.forEach(closure) swiftPackageProductDependencies.values.forEach(closure) + fileSystemSynchronizedRootGroups.values.forEach(closure) + fileSystemSynchronizedBuildFileExceptionSets.values.forEach(closure) } } diff --git a/Sources/XcodeProj/Objects/Project/PBXProject.swift b/Sources/XcodeProj/Objects/Project/PBXProject.swift index 9a2ccd66..a445387c 100644 --- a/Sources/XcodeProj/Objects/Project/PBXProject.swift +++ b/Sources/XcodeProj/Objects/Project/PBXProject.swift @@ -443,7 +443,7 @@ extension PBXProject { productDependency = XCSwiftPackageProductDependency(productName: productName, package: reference) objects.add(object: productDependency) } - target.packageProductDependencies.append(productDependency) + target.packageProductDependencies?.append(productDependency) return productDependency } @@ -465,7 +465,7 @@ extension PBXProject { productDependency = XCSwiftPackageProductDependency(productName: productName) objects.add(object: productDependency) } - target.packageProductDependencies.append(productDependency) + target.packageProductDependencies?.append(productDependency) return productDependency } diff --git a/Sources/XcodeProj/Objects/Targets/PBXTarget.swift b/Sources/XcodeProj/Objects/Targets/PBXTarget.swift index 518b2673..af8c8365 100644 --- a/Sources/XcodeProj/Objects/Targets/PBXTarget.swift +++ b/Sources/XcodeProj/Objects/Targets/PBXTarget.swift @@ -76,15 +76,15 @@ public class PBXTarget: PBXContainerItem { } /// Swift package product references. - var packageProductDependencyReferences: [PBXObjectReference] + var packageProductDependencyReferences: [PBXObjectReference]? /// Swift packages products. - public var packageProductDependencies: [XCSwiftPackageProductDependency] { + public var packageProductDependencies: [XCSwiftPackageProductDependency]? { set { - packageProductDependencyReferences = newValue.references() + packageProductDependencyReferences = newValue?.references() } get { - packageProductDependencyReferences.objects() + packageProductDependencyReferences?.objects() } } @@ -176,10 +176,15 @@ public class PBXTarget: PBXContainerItem { } else { productReference = nil } - let packageProductDependencyReferenceStrings: [String] = try container.decodeIfPresent(.packageProductDependencies) ?? [] + let packageProductDependencyReferenceStrings: [String]? = try container.decodeIfPresent(.packageProductDependencies) + if let packageProductDependencyReferenceStrings = packageProductDependencyReferenceStrings { packageProductDependencyReferences = packageProductDependencyReferenceStrings.map { objectReferenceRepository.getOrCreate(reference: $0, objects: objects) } + } else { + packageProductDependencyReferences = nil + } + let fileSystemSynchronizedGroupsReferences: [String]? = try container.decodeIfPresent(.fileSystemSynchronizedGroups) if let fileSystemSynchronizedGroupsReferences = fileSystemSynchronizedGroupsReferences { self.fileSystemSynchronizedGroupsReferences = fileSystemSynchronizedGroupsReferences.map { @@ -213,7 +218,7 @@ public class PBXTarget: PBXContainerItem { if let fileSystemSynchronizedGroupsReferences { dictionary["fileSystemSynchronizedGroups"] = .array(fileSystemSynchronizedGroupsReferences.map { fileSystemSynchronizedGroupReference in let fileSystemSynchronizedGroup: PBXFileSystemSynchronizedRootGroup? = fileSystemSynchronizedGroupReference.getObject() - return .string(CommentedString(fileSystemSynchronizedGroupReference.value, comment: fileSystemSynchronizedGroup?.name)) + return .string(CommentedString(fileSystemSynchronizedGroupReference.value, comment: fileSystemSynchronizedGroup?.path)) }) } @@ -228,11 +233,12 @@ public class PBXTarget: PBXContainerItem { let fileElement: PBXFileElement? = productReference.getObject() dictionary["productReference"] = .string(CommentedString(productReference.value, comment: fileElement?.fileName())) } - if !packageProductDependencies.isEmpty { - dictionary["packageProductDependencies"] = .array(packageProductDependencies.map { - PlistValue.string(.init($0.reference.value, comment: $0.productName)) - }) - } + if let packageProductDependencies = self.packageProductDependencies { + dictionary["packageProductDependencies"] = .array(packageProductDependencies.map { + PlistValue.string(.init($0.reference.value, comment: $0.productName)) + }) + } + return (key: CommentedString(reference, comment: name), value: .dictionary(dictionary)) } diff --git a/Sources/XcodeProj/Utils/ReferenceGenerator.swift b/Sources/XcodeProj/Utils/ReferenceGenerator.swift index e4a5f3da..5728ab1d 100644 --- a/Sources/XcodeProj/Utils/ReferenceGenerator.swift +++ b/Sources/XcodeProj/Utils/ReferenceGenerator.swift @@ -100,7 +100,7 @@ final class ReferenceGenerator: ReferenceGenerating { identifiers.append(target.name) // Packages - target.packageProductDependencies.forEach { + target.packageProductDependencies?.forEach { var identifiers = identifiers identifiers.append($0.productName) fixReference(for: $0, identifiers: identifiers) diff --git a/Tests/XcodeProjTests/Objects/Project/PBXProjectTests.swift b/Tests/XcodeProjTests/Objects/Project/PBXProjectTests.swift index f2b9e091..5a343bfe 100644 --- a/Tests/XcodeProjTests/Objects/Project/PBXProjectTests.swift +++ b/Tests/XcodeProjTests/Objects/Project/PBXProjectTests.swift @@ -67,7 +67,7 @@ final class PBXProjectTests: XCTestCase { // Then XCTAssertEqual(packageProduct, objects.buildFiles.first?.value.product) XCTAssertEqual(packageProduct, objects.swiftPackageProductDependencies.first?.value) - XCTAssertEqual(packageProduct, target.packageProductDependencies.first) + XCTAssertEqual(packageProduct, target.packageProductDependencies?.first) XCTAssertEqual(objects.fileReferences.first?.value.name, "Product") @@ -240,8 +240,8 @@ final class PBXProjectTests: XCTestCase { XCTAssertEqual(packageProduct, secondPackageProduct) XCTAssertEqual(packageProduct, thirdPackageProduct) XCTAssertEqual(project.remotePackages.count, 1) - XCTAssertEqual(target.packageProductDependencies.count, 2) - XCTAssertEqual(secondTarget.packageProductDependencies.count, 1) + XCTAssertEqual(target.packageProductDependencies?.count, 2) + XCTAssertEqual(secondTarget.packageProductDependencies?.count, 1) XCTAssertNotEqual(buildPhase.files?.first?.hashValue, secondBuildPhase.files?.first?.hashValue) XCTAssertEqual(objects.swiftPackageProductDependencies.count, 2) diff --git a/Tests/XcodeProjTests/Project/XcodeProjTests.swift b/Tests/XcodeProjTests/Project/XcodeProjIntegrationTests.swift similarity index 89% rename from Tests/XcodeProjTests/Project/XcodeProjTests.swift rename to Tests/XcodeProjTests/Project/XcodeProjIntegrationTests.swift index 3f4a8194..dee7610d 100644 --- a/Tests/XcodeProjTests/Project/XcodeProjTests.swift +++ b/Tests/XcodeProjTests/Project/XcodeProjIntegrationTests.swift @@ -25,6 +25,11 @@ final class XcodeProjIntegrationTests: XCTestCase { initModel: XcodeProj.init(path:)) } + func test_read_write_produces_no_diff_when_synchronizedRootGroupsFixture() throws { + try testReadWriteProducesNoDiff(from: synchronizedRootGroupsFixturePath, + initModel: XcodeProj.init(path:)) + } + func test_initialize_PBXProj_with_data() throws { // Given let pbxprojPath = iosProjectPath + "project.pbxproj" @@ -90,4 +95,8 @@ final class XcodeProjIntegrationTests: XCTestCase { private var iosProjectPath: Path { fixturesPath() + "iOS/Project.xcodeproj" } + + private var synchronizedRootGroupsFixturePath: Path { + fixturesPath() + "SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj" + } }