From 895e88a94cdb9168f5f89f20f1ccedbed1025f34 Mon Sep 17 00:00:00 2001 From: fortmarek Date: Fri, 20 Feb 2026 11:37:10 +0100 Subject: [PATCH] Add platformFiltersByRelativePath to PBXFileSystemSynchronizedBuildFileExceptionSet Support per-file platform filters on synchronized build file exception sets, following the same pattern as the existing attributesByRelativePath property. This maps relative file paths to arrays of platform filter strings (e.g. "ios", "tvos"), enabling multi-platform targets with platform-specific resources in buildable folders. Co-Authored-By: Claude Opus 4.6 --- ...temSynchronizedBuildFileExceptionSet.swift | 16 +++++- .../Objects/Sourcery/Equality.generated.swift | 1 + ...onizedBuildFileExceptionSet+Fixtures.swift | 6 ++- ...nchronizedBuildFileExceptionSetTests.swift | 49 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift index c375f16d0..e007bb879 100644 --- a/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift +++ b/Sources/XcodeProj/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet.swift @@ -25,6 +25,11 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr /// This is used for example when linking frameworks to specify that they are optional with the attribute "Weak" public var attributesByRelativePath: [String: [String]]? + /// Platform filters by relative path. + /// Every item in the list is the relative path inside the root synchronized group. + /// The value is the list of platform filters (e.g. "ios", "tvos") that the file should be included for. + public var platformFiltersByRelativePath: [String: [String]]? + var targetReference: PBXObjectReference public var target: PBXTarget! { @@ -43,13 +48,15 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr publicHeaders: [String]?, privateHeaders: [String]?, additionalCompilerFlagsByRelativePath: [String: String]?, - attributesByRelativePath: [String: [String]]?) { + attributesByRelativePath: [String: [String]]?, + platformFiltersByRelativePath: [String: [String]]? = nil) { targetReference = target.reference self.membershipExceptions = membershipExceptions self.publicHeaders = publicHeaders self.privateHeaders = privateHeaders self.additionalCompilerFlagsByRelativePath = additionalCompilerFlagsByRelativePath self.attributesByRelativePath = attributesByRelativePath + self.platformFiltersByRelativePath = platformFiltersByRelativePath super.init() } @@ -62,6 +69,7 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr case privateHeaders case additionalCompilerFlagsByRelativePath case attributesByRelativePath + case platformFiltersByRelativePath } public required init(from decoder: Decoder) throws { @@ -75,6 +83,7 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr privateHeaders = try container.decodeIfPresent(.privateHeaders) additionalCompilerFlagsByRelativePath = try container.decodeIfPresent(.additionalCompilerFlagsByRelativePath) attributesByRelativePath = try container.decodeIfPresent(.attributesByRelativePath) + platformFiltersByRelativePath = try container.decodeIfPresent(.platformFiltersByRelativePath) try super.init(from: decoder) } @@ -109,6 +118,11 @@ public class PBXFileSystemSynchronizedBuildFileExceptionSet: PBXFileSystemSynchr (CommentedString(key), .array(value.map { .string(CommentedString($0)) })) })) } + if let platformFiltersByRelativePath { + dictionary["platformFiltersByRelativePath"] = .dictionary(Dictionary(uniqueKeysWithValues: platformFiltersByRelativePath.map { key, value in + (CommentedString(key), .array(value.map { .string(CommentedString($0)) })) + })) + } dictionary["target"] = .string(CommentedString(target.reference.value, comment: target.name)) return (key: CommentedString(reference, comment: "PBXFileSystemSynchronizedBuildFileExceptionSet"), value: .dictionary(dictionary)) } diff --git a/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift b/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift index cd69699a9..016c323ca 100644 --- a/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift +++ b/Sources/XcodeProj/Objects/Sourcery/Equality.generated.swift @@ -319,6 +319,7 @@ extension PBXFileSystemSynchronizedBuildFileExceptionSet { if targetReference != rhs.targetReference { return false } if publicHeaders != rhs.publicHeaders { return false } if privateHeaders != rhs.privateHeaders { return false } + if platformFiltersByRelativePath != rhs.platformFiltersByRelativePath { return false } return super.isEqual(to: rhs) } } diff --git a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet+Fixtures.swift b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet+Fixtures.swift index 17e0b1f0d..bda5f78ea 100644 --- a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet+Fixtures.swift +++ b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSet+Fixtures.swift @@ -6,12 +6,14 @@ extension PBXFileSystemSynchronizedBuildFileExceptionSet { publicHeaders: [String]? = [], privateHeaders: [String]? = [], additionalCompilerFlagsByRelativePath: [String: String]? = nil, - attributesByRelativePath: [String: [String]]? = nil) -> PBXFileSystemSynchronizedBuildFileExceptionSet { + attributesByRelativePath: [String: [String]]? = nil, + platformFiltersByRelativePath: [String: [String]]? = nil) -> PBXFileSystemSynchronizedBuildFileExceptionSet { PBXFileSystemSynchronizedBuildFileExceptionSet(target: target, membershipExceptions: membershipExceptions, publicHeaders: publicHeaders, privateHeaders: privateHeaders, additionalCompilerFlagsByRelativePath: additionalCompilerFlagsByRelativePath, - attributesByRelativePath: attributesByRelativePath) + attributesByRelativePath: attributesByRelativePath, + platformFiltersByRelativePath: platformFiltersByRelativePath) } } diff --git a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift index 842954846..0ef9fda58 100644 --- a/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift +++ b/Tests/XcodeProjTests/Objects/Files/PBXFileSystemSynchronizedBuildFileExceptionSetTests.swift @@ -26,4 +26,53 @@ final class PBXFileSystemSynchronizedBuildFileExceptionSetTests: XCTestCase { let another = PBXFileSystemSynchronizedBuildFileExceptionSet.fixture(target: target) XCTAssertEqual(subject, another) } + + func test_equal_withDifferentPlatformFilters_returnsNotEqual() { + let one = PBXFileSystemSynchronizedBuildFileExceptionSet.fixture( + target: target, + platformFiltersByRelativePath: ["file.swift": ["ios"]] + ) + let another = PBXFileSystemSynchronizedBuildFileExceptionSet.fixture( + target: target, + platformFiltersByRelativePath: ["file.swift": ["tvos"]] + ) + XCTAssertNotEqual(one, another) + } + + func test_plistKeyAndValue_platformFiltersByRelativePath_serializesCorrectly() throws { + let proj = PBXProj() + let exceptionSet = PBXFileSystemSynchronizedBuildFileExceptionSet.fixture( + target: target, + platformFiltersByRelativePath: [ + "Resources/ios_only.mp4": ["ios"], + "Resources/multi.mp4": ["ios", "tvos"], + ] + ) + proj.add(object: exceptionSet) + + let (_, plistValue) = try exceptionSet.plistKeyAndValue(proj: proj, reference: "ref") + + let dict = try XCTUnwrap(plistValue.dictionary?[CommentedString("platformFiltersByRelativePath")]?.dictionary) + XCTAssertEqual( + dict[CommentedString("Resources/ios_only.mp4")], + .array([.string(CommentedString("ios"))]) + ) + XCTAssertEqual( + dict[CommentedString("Resources/multi.mp4")], + .array([.string(CommentedString("ios")), .string(CommentedString("tvos"))]) + ) + } + + func test_plistKeyAndValue_platformFiltersByRelativePath_omittedWhenNil() throws { + let proj = PBXProj() + let exceptionSet = PBXFileSystemSynchronizedBuildFileExceptionSet.fixture( + target: target, + platformFiltersByRelativePath: nil + ) + proj.add(object: exceptionSet) + + let (_, plistValue) = try exceptionSet.plistKeyAndValue(proj: proj, reference: "ref") + + XCTAssertNil(plistValue.dictionary?[CommentedString("platformFiltersByRelativePath")]) + } }