From 1c505f70d1aa5d29090822970f78b457a0789757 Mon Sep 17 00:00:00 2001 From: Mirko Tomic Date: Fri, 21 Nov 2025 01:10:48 +0100 Subject: [PATCH] Handle references for exception sets in Synchronized Groups Generate permanent references for PBXFileSystemSynchronizedExceptionSet objects, including build file and build phase exception sets. Updates tests to verify that references for exception sets are correctly converted to permanent values. --- .../XcodeProj/Utils/ReferenceGenerator.swift | 29 +++++++++ .../Utils/ReferenceGeneratorTests.swift | 62 ++++++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/Sources/XcodeProj/Utils/ReferenceGenerator.swift b/Sources/XcodeProj/Utils/ReferenceGenerator.swift index 01f0267e9..21df39128 100644 --- a/Sources/XcodeProj/Utils/ReferenceGenerator.swift +++ b/Sources/XcodeProj/Utils/ReferenceGenerator.swift @@ -176,6 +176,35 @@ final class ReferenceGenerator: ReferenceGenerating { } fixReference(for: synchronizedRootGroup, identifiers: identifiers) + + // Generate references for exception sets + if let exceptions = synchronizedRootGroup.exceptions { + try exceptions.forEach { exception in + try generateExceptionSetReferences(exception, identifiers: identifiers) + } + } + } + + /// Generates the reference for an exception set object. + /// + /// - Parameters: + /// - exceptionSet: exception set instance. + /// - identifiers: list of identifiers. + private func generateExceptionSetReferences(_ exceptionSet: PBXFileSystemSynchronizedExceptionSet, + identifiers: [String]) throws { + var identifiers = identifiers + + if let buildFileException = exceptionSet as? PBXFileSystemSynchronizedBuildFileExceptionSet { + if let target = buildFileException.target { + identifiers.append(target.reference.value) + } + fixReference(for: buildFileException, identifiers: identifiers) + } else if let buildPhaseException = exceptionSet as? PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet { + if let buildPhase = buildPhaseException.buildPhase { + identifiers.append(buildPhase.reference.value) + } + fixReference(for: buildPhaseException, identifiers: identifiers) + } } /// Generates the reference for a configuration list object. diff --git a/Tests/XcodeProjTests/Utils/ReferenceGeneratorTests.swift b/Tests/XcodeProjTests/Utils/ReferenceGeneratorTests.swift index af3a2f1b1..de9efd080 100644 --- a/Tests/XcodeProjTests/Utils/ReferenceGeneratorTests.swift +++ b/Tests/XcodeProjTests/Utils/ReferenceGeneratorTests.swift @@ -98,6 +98,63 @@ class ReferenceGeneratorTests: XCTestCase { XCTAssert(!syncedGroup.reference.temporary) } + + func test_projectWithFilesystemSynchronizedRootGroupWithExceptions_convertsReferencesToPermanent() throws { + let project = PBXProj(rootObject: nil, objectVersion: 0, archiveVersion: 0, classes: [:], objects: []) + let pbxProject = project.makeProject() + + let target = project.makeTarget() + pbxProject.targets.append(target) + + let buildFileException = PBXFileSystemSynchronizedBuildFileExceptionSet( + target: target, + membershipExceptions: ["Info.plist"], + publicHeaders: nil, + privateHeaders: nil, + additionalCompilerFlagsByRelativePath: nil, + attributesByRelativePath: nil + ) + project.add(object: buildFileException) + + let syncedGroup = project.makeSynchronizedRootGroup(exceptions: [buildFileException]) + target.fileSystemSynchronizedGroups = [syncedGroup] + + let referenceGenerator = ReferenceGenerator(outputSettings: PBXOutputSettings()) + try referenceGenerator.generateReferences(proj: project) + + XCTAssert(!syncedGroup.reference.temporary, "Synced group reference should not be temporary") + XCTAssert(!buildFileException.reference.temporary, "Build file exception reference should not be temporary") + XCTAssertFalse(buildFileException.reference.value.hasPrefix("TEMP_"), "Build file exception reference should not have TEMP_ prefix") + } + + func test_projectWithFilesystemSynchronizedRootGroupWithBuildPhaseException_convertsReferencesToPermanent() throws { + let project = PBXProj(rootObject: nil, objectVersion: 0, archiveVersion: 0, classes: [:], objects: []) + let pbxProject = project.makeProject() + + let target = project.makeTarget() + pbxProject.targets.append(target) + + let buildPhase = PBXSourcesBuildPhase() + project.add(object: buildPhase) + target.buildPhases.append(buildPhase) + + let buildPhaseException = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet( + buildPhase: buildPhase, + membershipExceptions: ["ExcludedFile.swift"], + attributesByRelativePath: nil + ) + project.add(object: buildPhaseException) + + let syncedGroup = project.makeSynchronizedRootGroup(exceptions: [buildPhaseException]) + target.fileSystemSynchronizedGroups = [syncedGroup] + + let referenceGenerator = ReferenceGenerator(outputSettings: PBXOutputSettings()) + try referenceGenerator.generateReferences(proj: project) + + XCTAssert(!syncedGroup.reference.temporary, "Synced group reference should not be temporary") + XCTAssert(!buildPhaseException.reference.temporary, "Build phase exception reference should not be temporary") + XCTAssertFalse(buildPhaseException.reference.value.hasPrefix("TEMP_"), "Build phase exception reference should not have TEMP_ prefix") + } } private extension PBXProj { @@ -192,11 +249,12 @@ private extension PBXProj { return target } - func makeSynchronizedRootGroup() -> PBXFileSystemSynchronizedRootGroup { + func makeSynchronizedRootGroup(exceptions: [PBXFileSystemSynchronizedExceptionSet] = []) -> PBXFileSystemSynchronizedRootGroup { let syncedGroup = PBXFileSystemSynchronizedRootGroup( sourceTree: .group, path: "SyncedPath", - name: "SyncedGroup" + name: "SyncedGroup", + exceptions: exceptions ) add(object: syncedGroup) rootObject!.mainGroup.children.append(syncedGroup)