Skip to content

Commit a0afcef

Browse files
authored
Handle membership exceptions for synchronized root groups (#1587)
* Handle membership exceptions for synchronized root groups Adds logic to detect and register membership exceptions for PBXFileSystemSynchronizedRootGroup objects, specifically excluding Info.plist files from group membership when necessary. Also ensures resources build phase is added if synchronized root groups are present. * Refactor synced folder membership exceptions with glob support Extract configureMembershipExceptions into its own method, use Set for dedup, resolve excludes via glob expansion, and add a no-op test case. Incorporates glob support and tests from macguru/XcodeGen@baf1108. * Update UUID * Comment out excludes in project.yml Comment out excludes for ExcludedFile.swift due to CI issue. * Clean up project.pbxproj by removing exception set Removed PBXFileSystemSynchronizedBuildFileExceptionSet section and its references. * Remove comment * Update SourceGeneratorTests.swift * Update project.pbxproj * Retrigger CI * Add info.plist exclusion
1 parent a904543 commit a0afcef

7 files changed

Lines changed: 171 additions & 1 deletion

File tree

Sources/XcodeGenKit/PBXProjGenerator.swift

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1141,7 +1141,8 @@ public class PBXProjGenerator {
11411141

11421142
func addResourcesBuildPhase() {
11431143
let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences
1144-
if !resourcesBuildPhaseFiles.isEmpty {
1144+
let hasSynchronizedRootGroups = sourceFiles.contains { $0.fileReference is PBXFileSystemSynchronizedRootGroup }
1145+
if !resourcesBuildPhaseFiles.isEmpty || hasSynchronizedRootGroups {
11451146
let resourcesBuildPhase = addObject(PBXResourcesBuildPhase(files: resourcesBuildPhaseFiles))
11461147
buildPhases.append(resourcesBuildPhase)
11471148
}
@@ -1460,9 +1461,57 @@ public class PBXProjGenerator {
14601461
// add fileSystemSynchronizedGroups
14611462
let synchronizedRootGroups = sourceFiles.compactMap { $0.fileReference as? PBXFileSystemSynchronizedRootGroup }
14621463
if !synchronizedRootGroups.isEmpty {
1464+
for syncedGroup in synchronizedRootGroups {
1465+
configureMembershipExceptions(
1466+
for: syncedGroup,
1467+
target: target,
1468+
targetObject: targetObject,
1469+
infoPlistFiles: infoPlistFiles
1470+
)
1471+
}
14631472
targetObject.fileSystemSynchronizedGroups = synchronizedRootGroups
14641473
}
14651474
}
1475+
1476+
private func configureMembershipExceptions(
1477+
for syncedGroup: PBXFileSystemSynchronizedRootGroup,
1478+
target: Target,
1479+
targetObject: PBXTarget,
1480+
infoPlistFiles: [Config: String]
1481+
) {
1482+
guard let syncedGroupPath = syncedGroup.path else { return }
1483+
let syncedPath = (project.basePath + Path(syncedGroupPath)).normalize()
1484+
1485+
guard let targetSource = target.sources.first(where: {
1486+
(project.basePath + $0.path).normalize() == syncedPath
1487+
}) else { return }
1488+
1489+
var exceptions: Set<String> = Set(
1490+
sourceGenerator.expandedExcludes(for: targetSource)
1491+
.compactMap { try? $0.relativePath(from: syncedPath).string }
1492+
)
1493+
1494+
for infoPlistPath in Set(infoPlistFiles.values) {
1495+
let relative = try? (project.basePath + infoPlistPath).normalize()
1496+
.relativePath(from: syncedPath)
1497+
if let rel = relative?.string, !rel.hasPrefix("..") {
1498+
exceptions.insert(rel)
1499+
}
1500+
}
1501+
1502+
guard !exceptions.isEmpty else { return }
1503+
1504+
let exceptionSet = PBXFileSystemSynchronizedBuildFileExceptionSet(
1505+
target: targetObject,
1506+
membershipExceptions: exceptions.sorted(),
1507+
publicHeaders: nil,
1508+
privateHeaders: nil,
1509+
additionalCompilerFlagsByRelativePath: nil,
1510+
attributesByRelativePath: nil
1511+
)
1512+
addObject(exceptionSet)
1513+
syncedGroup.exceptions = (syncedGroup.exceptions ?? []) + [exceptionSet]
1514+
}
14661515

14671516
private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? {
14681517
switch filter {

Sources/XcodeGenKit/SourceGenerator.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,11 @@ class SourceGenerator {
372372
return variantGroup
373373
}
374374

375+
/// Returns the expanded set of excluded paths for a target source by resolving its exclude glob patterns.
376+
func expandedExcludes(for targetSource: TargetSource) -> Set<Path> {
377+
getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes)
378+
}
379+
375380
/// Collects all the excluded paths within the targetSource
376381
private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set<Path> {
377382
let rootSourcePath = project.basePath + targetSource.path

Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,9 +830,23 @@
830830
FED40A89162E446494DDE7C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
831831
/* End PBXFileReference section */
832832

833+
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
834+
9A259ACEBCE19CC5F22B6DD4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
835+
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
836+
membershipExceptions = (
837+
ExcludedFile.swift,
838+
Info.plist,
839+
);
840+
target = 0867B0DACEF28C11442DE8F7 /* App_iOS */;
841+
};
842+
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
843+
833844
/* Begin PBXFileSystemSynchronizedRootGroup section */
834845
AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = {
835846
isa = PBXFileSystemSynchronizedRootGroup;
847+
exceptions = (
848+
9A259ACEBCE19CC5F22B6DD4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
849+
);
836850
explicitFileTypes = {
837851
};
838852
explicitFolders = (
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// excluded
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict/>
5+
</plist>

Tests/Fixtures/TestProject/project.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ targets:
166166
- String Catalogs/LocalizableStrings.xcstrings
167167
- path: SyncedFolder
168168
type: syncedFolder
169+
excludes:
170+
- ExcludedFile.swift
171+
- Info.plist
169172
settings:
170173
INFOPLIST_FILE: App_iOS/Info.plist
171174
PRODUCT_BUNDLE_IDENTIFIER: com.project.app

Tests/XcodeGenKitTests/SourceGeneratorTests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,99 @@ class SourceGeneratorTests: XCTestCase {
120120
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
121121
}
122122

123+
$0.it("adds excludes as membership exceptions for synced folder") {
124+
let directories = """
125+
Sources:
126+
- a.swift
127+
- b.swift
128+
- Generated:
129+
- c.generated.swift
130+
- d.generated.swift
131+
"""
132+
try createDirectories(directories)
133+
134+
let source = TargetSource(path: "Sources", excludes: ["b.swift", "Generated/*.generated.swift"], type: .syncedFolder)
135+
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
136+
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
137+
138+
let pbxProj = try project.generatePbxProj()
139+
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
140+
let syncedFolder = try unwrap(syncedFolders.first)
141+
142+
let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
143+
let exceptionSet = try unwrap(exceptionSets?.first)
144+
let exceptions = try unwrap(exceptionSet.membershipExceptions)
145+
146+
try expect(exceptions.contains("b.swift")) == true
147+
try expect(exceptions.contains("Generated/c.generated.swift")) == true
148+
try expect(exceptions.contains("Generated/d.generated.swift")) == true
149+
try expect(exceptions.contains("a.swift")) == false
150+
}
151+
152+
$0.it("auto-excludes Info.plist from synced folder membership") {
153+
let directories = """
154+
Sources:
155+
- a.swift
156+
- Info.plist
157+
"""
158+
try createDirectories(directories)
159+
160+
let source = TargetSource(path: "Sources", type: .syncedFolder)
161+
let target = Target(
162+
name: "Test",
163+
type: .application,
164+
platform: .iOS,
165+
settings: try Settings(jsonDictionary: ["INFOPLIST_FILE": "Sources/Info.plist"]),
166+
sources: [source]
167+
)
168+
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
169+
170+
let pbxProj = try project.generatePbxProj()
171+
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
172+
let syncedFolder = try unwrap(syncedFolders.first)
173+
174+
let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
175+
let exceptionSet = try unwrap(exceptionSets?.first)
176+
let exceptions = try unwrap(exceptionSet.membershipExceptions)
177+
178+
try expect(exceptions.contains("Info.plist")) == true
179+
}
180+
181+
$0.it("creates no exception set for synced folder without excludes") {
182+
let directories = """
183+
Sources:
184+
- a.swift
185+
"""
186+
try createDirectories(directories)
187+
188+
let source = TargetSource(path: "Sources", type: .syncedFolder)
189+
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
190+
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
191+
192+
let pbxProj = try project.generatePbxProj()
193+
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
194+
let syncedFolder = try unwrap(syncedFolders.first)
195+
196+
try expect(syncedFolder.exceptions?.isEmpty ?? true) == true
197+
}
198+
199+
$0.it("adds empty resources build phase for synced folder") {
200+
let directories = """
201+
Sources:
202+
- a.swift
203+
"""
204+
try createDirectories(directories)
205+
206+
let source = TargetSource(path: "Sources", type: .syncedFolder)
207+
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
208+
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
209+
210+
let pbxProj = try project.generatePbxProj()
211+
let nativeTarget = try unwrap(pbxProj.nativeTargets.first)
212+
let hasResourcesPhase = nativeTarget.buildPhases.contains { $0 is PBXResourcesBuildPhase }
213+
try expect(hasResourcesPhase) == true
214+
}
215+
123216
$0.it("supports frameworks in sources") {
124217
let directories = """
125218
Sources:

0 commit comments

Comments
 (0)