diff --git a/Fixtures/SynchronizedRootGroups/.gitignore b/Fixtures/SynchronizedRootGroups/.gitignore new file mode 100644 index 000000000..9e69ba792 --- /dev/null +++ b/Fixtures/SynchronizedRootGroups/.gitignore @@ -0,0 +1 @@ +*.xcodeproj/xcuserdata diff --git a/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj b/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj index c6a599659..a8db250fe 100644 --- a/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj +++ b/Fixtures/SynchronizedRootGroups/SynchronizedRootGroups.xcodeproj/project.pbxproj @@ -3,9 +3,21 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 73; objects = { +/* Begin PBXCopyFilesBuildPhase section */ + F841A9CA2D63AFBB00059ED6 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 6CF05B8C2C53F5F200EF267F /* SynchronizedRootGroups.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SynchronizedRootGroups.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -20,19 +32,23 @@ }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, - ); - explicitFileTypes = { +/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ + F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { + isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; + attributesByRelativePath = { + XPCService.xpc = ( + RemoveHeadersOnCopy, + ); }; - explicitFolders = ( + buildPhase = F841A9CA2D63AFBB00059ED6 /* CopyFiles */; + membershipExceptions = ( + XPCService.xpc, ); - path = SynchronizedRootGroups; - sourceTree = ""; }; +/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SynchronizedRootGroups; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,6 +99,7 @@ 6CF05B882C53F5F200EF267F /* Sources */, 6CF05B892C53F5F200EF267F /* Frameworks */, 6CF05B8A2C53F5F200EF267F /* Resources */, + F841A9CA2D63AFBB00059ED6 /* CopyFiles */, ); buildRules = ( ); @@ -114,7 +131,6 @@ }; }; buildConfigurationList = 6CF05B862C53F5F200EF267F /* Build configuration list for PBXProject "SynchronizedRootGroups" */; - compatibilityVersion = "Xcode 15.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -122,6 +138,7 @@ Base, ); mainGroup = 6CF05B822C53F5F200EF267F; + preferredProjectObjectVersion = 60; productRefGroup = 6CF05B8D2C53F5F200EF267F /* Products */; projectDirPath = ""; projectRoot = ""; diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift index 4f363322a..c375f16d0 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift @@ -1,7 +1,7 @@ import Foundation /// Class representing an element that may contain other elements. -public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXObject, PlistSerializable { +public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchronizedExceptionSet, PlistSerializable { // MARK: - Attributes /// A list of relative paths to children subfolders for which exceptions are applied. diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift new file mode 100644 index 000000000..095bd24e9 --- /dev/null +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift @@ -0,0 +1,4 @@ +import Foundation + +/// Common class for exception sets, such as `PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet` and `PBXFileSystemSynchronizedBuildFileExceptionSet` +public class PBXFileSystemSynchronizedExceptionSet: PBXObject {} diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift new file mode 100644 index 000000000..b2326893e --- /dev/null +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift @@ -0,0 +1,83 @@ +import Foundation + +public class PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: PBXFileSystemSynchronizedExceptionSet, PlistSerializable { + // MARK: - Attributes + + /// A list of relative paths to children subfolders for which exceptions are applied. + public var membershipExceptions: [String]? + + /// Build phase that this exception set applies to. + public var buildPhase: PBXBuildPhase! { + get { + buildPhaseReference.getObject() as? PBXBuildPhase + } + set { + buildPhaseReference = newValue.reference + } + } + + /// Attributes by relative path. + /// Every item in the list is the relative path inside the root synchronized group. + /// For example `RemoveHeadersOnCopy` and `CodeSignOnCopy`. + public var attributesByRelativePath: [String: [String]]? + + var buildPhaseReference: PBXObjectReference + + // MARK: - Init + + public init( + buildPhase: PBXBuildPhase, + membershipExceptions: [String]?, + attributesByRelativePath: [String: [String]]? + ) { + buildPhaseReference = buildPhase.reference + self.membershipExceptions = membershipExceptions + self.attributesByRelativePath = attributesByRelativePath + super.init() + } + + // MARK: - Decodable + + fileprivate enum CodingKeys: String, CodingKey { + case buildPhase + case membershipExceptions + case attributesByRelativePath + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let referenceRepository = decoder.context.objectReferenceRepository + let objects = decoder.context.objects + let buildPhaseReference: String = try container.decode(.buildPhase) + self.buildPhaseReference = referenceRepository.getOrCreate(reference: buildPhaseReference, objects: objects) + membershipExceptions = try container.decodeIfPresent(.membershipExceptions) + attributesByRelativePath = try container.decodeIfPresent(.attributesByRelativePath) + try super.init(from: decoder) + } + + // MARK: - Equatable + + override func isEqual(to object: Any?) -> Bool { + guard let rhs = object as? PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet else { + return false + } + return isEqual(to: rhs) + } + + // MARK: - PlistSerializable + + func plistKeyAndValue(proj _: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) { + var dictionary: [CommentedString: PlistValue] = [:] + dictionary["isa"] = .string(CommentedString(type(of: self).isa)) + if let membershipExceptions { + dictionary["membershipExceptions"] = .array(membershipExceptions.map { .string(CommentedString($0)) }) + } + if let attributesByRelativePath { + dictionary["attributesByRelativePath"] = .dictionary(Dictionary(uniqueKeysWithValues: attributesByRelativePath.map { key, value in + (CommentedString(key), .array(value.map { .string(CommentedString($0)) })) + })) + } + dictionary["buildPhase"] = .string(CommentedString(buildPhase.reference.value, comment: buildPhase.name() ?? "CopyFiles")) + return (key: CommentedString(reference, comment: "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet"), value: .dictionary(dictionary)) + } +} diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift index 882d27db5..43e090eff 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedRootGroup.swift @@ -12,7 +12,7 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement { /// It returns a list of exception objects that override the configuration for some children /// in the synchronized root group. - public var exceptions: [PBXFileSystemSynchronizedBuildFileExceptionSet]? { + public var exceptions: [PBXFileSystemSynchronizedExceptionSet]? { set { exceptionsReferences = newValue?.references() } @@ -47,7 +47,7 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement { tabWidth: UInt? = nil, wrapsLines: Bool? = nil, explicitFileTypes: [String: String] = [:], - exceptions: [PBXFileSystemSynchronizedBuildFileExceptionSet] = [], + exceptions: [PBXFileSystemSynchronizedExceptionSet] = [], explicitFolders: [String] = []) { self.explicitFileTypes = explicitFileTypes exceptionsReferences = exceptions.references() @@ -83,14 +83,14 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement { // MARK: - PlistSerializable - override var multiline: Bool { true } + override var multiline: Bool { (exceptions?.count ?? 0) < 2 } override func plistKeyAndValue(proj: PBXProj, reference: String) throws -> (key: CommentedString, value: PlistValue) { var dictionary: [CommentedString: PlistValue] = try super.plistKeyAndValue(proj: proj, reference: reference).value.dictionary ?? [:] dictionary["isa"] = .string(CommentedString(type(of: self).isa)) - if let exceptionsReferences, !exceptionsReferences.isEmpty { - dictionary["exceptions"] = .array(exceptionsReferences.map { exceptionReference in - .string(CommentedString(exceptionReference.value, comment: "PBXFileSystemSynchronizedBuildFileExceptionSet")) + if let exceptions, !exceptions.isEmpty { + dictionary["exceptions"] = .array(exceptions.map { exception in + .string(CommentedString(exception.reference.value, comment: type(of: exception).isa)) }) } if let explicitFileTypes { diff --git a/Sources/XcodeProj/Objects/Project/PBXObjectDictionaryEntry.swift b/Sources/XcodeProj/Objects/Project/PBXObjectDictionaryEntry.swift index 33e3adaa1..d2a541bf7 100644 --- a/Sources/XcodeProj/Objects/Project/PBXObjectDictionaryEntry.swift +++ b/Sources/XcodeProj/Objects/Project/PBXObjectDictionaryEntry.swift @@ -40,6 +40,7 @@ struct PBXObjectDictionaryEntry: Decodable { case XCSwiftPackageProductDependency.isa: try XCSwiftPackageProductDependency(from: decoder) case PBXFileSystemSynchronizedRootGroup.isa: try PBXFileSystemSynchronizedRootGroup(from: decoder) case PBXFileSystemSynchronizedBuildFileExceptionSet.isa: try PBXFileSystemSynchronizedBuildFileExceptionSet(from: decoder) + case PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.isa: try PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet(from: decoder) default: throw PBXObjectError.unknownElement(isa) } diff --git a/Sources/XcodeProj/Objects/Project/PBXObjects.swift b/Sources/XcodeProj/Objects/Project/PBXObjects.swift index 33d767a1c..70083a779 100644 --- a/Sources/XcodeProj/Objects/Project/PBXObjects.swift +++ b/Sources/XcodeProj/Objects/Project/PBXObjects.swift @@ -145,6 +145,11 @@ class PBXObjects: Equatable { lock.whileLocked { _fileSystemSynchronizedBuildFileExceptionSets } } + private var _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: [PBXObjectReference: PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet] = [:] + var fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: [PBXObjectReference: PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet] { + lock.whileLocked { _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet } + } + // XCSwiftPackageProductDependency /// Initializes the project objects container @@ -185,7 +190,8 @@ class PBXObjects: Equatable { lhs.swiftPackageProductDependencies == rhs._swiftPackageProductDependencies && lhs.remoteSwiftPackageReferences == rhs.remoteSwiftPackageReferences && lhs.fileSystemSynchronizedRootGroups == rhs.fileSystemSynchronizedRootGroups && - lhs.fileSystemSynchronizedBuildFileExceptionSets == rhs.fileSystemSynchronizedBuildFileExceptionSets + lhs.fileSystemSynchronizedBuildFileExceptionSets == rhs.fileSystemSynchronizedBuildFileExceptionSets && + lhs.fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet == rhs.fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet } // MARK: - Helpers @@ -232,6 +238,8 @@ 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 + case let object as PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: + _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet[objectReference] = object default: fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo") } } @@ -296,6 +304,8 @@ class PBXObjects: Equatable { return _fileSystemSynchronizedRootGroups.remove(at: index).value } else if let index = fileSystemSynchronizedBuildFileExceptionSets.index(forKey: reference) { return _fileSystemSynchronizedBuildFileExceptionSets.remove(at: index).value + } else if let index = fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.index(forKey: reference) { + return _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.remove(at: index).value } return nil @@ -363,6 +373,8 @@ class PBXObjects: Equatable { object } else if let object = fileSystemSynchronizedBuildFileExceptionSets[reference] { object + } else if let object = fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet[reference] { + object } else { nil } @@ -456,5 +468,6 @@ extension PBXObjects { swiftPackageProductDependencies.values.forEach(closure) fileSystemSynchronizedRootGroups.values.forEach(closure) fileSystemSynchronizedBuildFileExceptionSets.values.forEach(closure) + fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.values.forEach(closure) } } diff --git a/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift b/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift index 7321fb1d0..bfbb4adec 100644 --- a/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift +++ b/Sources/XcodeProj/Objects/Project/PBXProjEncoder.swift @@ -119,6 +119,12 @@ final class PBXProjEncoder { outputSettings: outputSettings, stateHolder: &stateHolder, to: &output) + try write(section: "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet", + proj: proj, + objects: proj.objects.fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet, + outputSettings: outputSettings, + stateHolder: &stateHolder, + to: &output) try write(section: "PBXFileSystemSynchronizedRootGroup", proj: proj, objects: proj.objects.fileSystemSynchronizedRootGroups, diff --git a/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift b/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift index 4c9a1d98c..b8087736f 100644 --- a/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift +++ b/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift @@ -322,3 +322,12 @@ extension PBXFileSystemSynchronizedBuildFileExceptionSet { return super.isEqual(to: rhs) } } + +extension PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet { + /// :nodoc: + func isEqual(to rhs: PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet) -> Bool { + if membershipExceptions != rhs.membershipExceptions { return false } + if buildPhaseReference != rhs.buildPhaseReference { return false } + return super.isEqual(to: rhs) + } +} diff --git a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift index 81c674fb7..2f0e49c81 100644 --- a/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift +++ b/Tests/XcodeProjTests/Objects/Project/PBXProjEncoderTests.swift @@ -294,18 +294,7 @@ class PBXProjEncoderTests: XCTestCase { let lines = lines(fromFile: encodeProject(settings: settings)) let beginGroup = lines.findLine("/* Begin PBXFileSystemSynchronizedRootGroup section */") - var line = lines.validate(line: "6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = {", after: beginGroup) - line = lines.validate(line: "isa = PBXFileSystemSynchronizedRootGroup;", after: line) - line = lines.validate(line: "exceptions = (", after: line) - line = lines.validate(line: "6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */,", after: line) - line = lines.validate(line: ");", after: line) - line = lines.validate(line: "explicitFileTypes = {", after: line) - line = lines.validate(line: "};", after: line) - line = lines.validate(line: "explicitFolders = (", after: line) - line = lines.validate(line: ");", after: line) - line = lines.validate(line: "path = SynchronizedRootGroups;", after: line) - line = lines.validate(line: "sourceTree = \"\";", after: line) - line = lines.validate(line: "};", after: line) + var line = lines.validate(line: "6CF05B9D2C53F64800EF267F /* SynchronizedRootGroups */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (6CF05BA32C53F97F00EF267F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SynchronizedRootGroups; sourceTree = \"\"; };", after: beginGroup) line = lines.validate(line: "/* End PBXFileSystemSynchronizedRootGroup section */", after: line) } @@ -328,6 +317,30 @@ class PBXProjEncoderTests: XCTestCase { line = lines.validate(line: "/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */", after: line) } + // MARK: - File system synchronized group build phase membership exception set + + func test_fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSets_when_projectWithFileSystemSynchronizedRootGroups() throws { + // Given + try loadSynchronizedRootGroups() + let settings = PBXOutputSettings(projNavigatorFileOrder: .byFilenameGroupsFirst) + let lines = lines(fromFile: encodeProject(settings: settings)) + + let beginGroup = lines.findLine("/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */") + var line = lines.validate(line: "F841A9D12D63B00A00059ED6 /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = {", after: beginGroup) + line = lines.validate(line: "isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;", after: line) + line = lines.validate(line: "attributesByRelativePath = {", after: line) + line = lines.validate(line: "XPCService.xpc = (", after: line) + line = lines.validate(line: "RemoveHeadersOnCopy,", after: line) + line = lines.validate(line: ");", after: line) + line = lines.validate(line: "};", after: line) + line = lines.validate(line: "buildPhase = F841A9CA2D63AFBB00059ED6 /* CopyFiles */;", after: line) + line = lines.validate(line: "membershipExceptions = (", after: line) + line = lines.validate(line: "XPCService.xpc,", after: line) + line = lines.validate(line: ");", after: line) + line = lines.validate(line: "};", after: line) + line = lines.validate(line: "/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */", after: line) + } + // MARK: - Build phases func test_build_phase_sources_unsorted_when_iOSProject() throws {