From 523b19aedb66a0e09b793015aa335c5a8abca420 Mon Sep 17 00:00:00 2001
From: Yusuf Olokoba <olokobayusuf@gmail.com>
Date: Tue, 29 Oct 2024 13:18:46 -0400
Subject: [PATCH 1/2] Add support for reading
 `PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet`

---
 ...temSynchronizedBuildFileExceptionSet.swift |  2 +-
 ...BXFileSystemSynchronizedExceptionSet.swift |  3 +
 ...roupBuildPhaseMembershipExceptionSet.swift | 83 +++++++++++++++++++
 .../PBXFileSystemSynchronizedRootGroup.swift  | 10 +--
 .../Objects/Project/PBXObjectParser.swift     |  2 +
 .../Objects/Project/PBXObjects.swift          | 16 +++-
 .../Objects/Project/PBXProjEncoder.swift      |  6 ++
 .../Objects/Sourcery/Equality.generated.swift |  9 ++
 8 files changed, 124 insertions(+), 7 deletions(-)
 create mode 100644 Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift
 create mode 100644 Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.swift

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..72f233d69
--- /dev/null
+++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedExceptionSet.swift
@@ -0,0 +1,3 @@
+import Foundation
+
+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..8958dc9c1
--- /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..5cc29adeb 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()
@@ -88,9 +88,9 @@ public class PBXFileSystemSynchronizedRootGroup: PBXFileElement {
     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/PBXObjectParser.swift b/Sources/XcodeProj/Objects/Project/PBXObjectParser.swift
index 3c0ad0044..b9452b64f 100644
--- a/Sources/XcodeProj/Objects/Project/PBXObjectParser.swift
+++ b/Sources/XcodeProj/Objects/Project/PBXObjectParser.swift
@@ -72,6 +72,8 @@ final class PBXObjectParser {
             return try decoder.decode(PBXFileSystemSynchronizedRootGroup.self, from: data)
         case PBXFileSystemSynchronizedBuildFileExceptionSet.isa:
             return try decoder.decode(PBXFileSystemSynchronizedBuildFileExceptionSet.self, from: data)
+        case PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.isa:
+            return try decoder.decode(PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet.self, from: data)
         default:
             throw PBXObjectError.unknownElement(isa)
         }
diff --git a/Sources/XcodeProj/Objects/Project/PBXObjects.swift b/Sources/XcodeProj/Objects/Project/PBXObjects.swift
index 22cfe75cd..22276036b 100644
--- a/Sources/XcodeProj/Objects/Project/PBXObjects.swift
+++ b/Sources/XcodeProj/Objects/Project/PBXObjects.swift
@@ -144,6 +144,13 @@ class PBXObjects: Equatable {
     var fileSystemSynchronizedBuildFileExceptionSets: [PBXObjectReference: PBXFileSystemSynchronizedBuildFileExceptionSet] {
         lock.whileLocked { _fileSystemSynchronizedBuildFileExceptionSets }
     }
+  
+    private var _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: [PBXObjectReference: PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet] = [:]
+    var fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet: [PBXObjectReference: PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet] {
+        lock.whileLocked { _fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet }
+    }
+  
+  
 
     // XCSwiftPackageProductDependency
 
@@ -185,7 +192,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 +240,7 @@ 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 +305,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 +374,8 @@ class PBXObjects: Equatable {
             return object
         } else if let object = fileSystemSynchronizedBuildFileExceptionSets[reference] {
             return object
+        } else if let object = fileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet[reference] {
+            return object
         } else {
             return nil
         }
@@ -456,5 +469,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)
+    }
+}

From 6bb836a802521a710333727b566e4f634d172da4 Mon Sep 17 00:00:00 2001
From: Yusuf Olokoba <olokobayusuf@gmail.com>
Date: Tue, 29 Oct 2024 13:19:01 -0400
Subject: [PATCH 2/2] Mention our usage of `XcodeProj`

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index ff763a9e6..d5e4797f3 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ XcodeProj is a library written in Swift for parsing and working with Xcode proje
 | XcodeGen        | [github.com/yonaskolb/XcodeGen](https://github.com/yonaskolb/XcodeGen)                       |
 | xspm            | [gitlab.com/Pyroh/xspm](https://gitlab.com/Pyroh/xspm)                                       |
 | Privacy Manifest| [github.com/stelabouras/privacy-manifest](https://github.com/stelabouras/privacy-manifest)   |
+| Function        | [github.com/fxnai/fxnios](https://github.com/fxnai/fxnios)                                   | 
 
 If you are also leveraging XcodeProj in your project, feel free to open a PR to include it in the list above.
 
