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@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
This commit is contained in:
mirkokg
2026-03-03 22:22:27 +01:00
committed by GitHub
parent a904543801
commit a0afcefaed
7 changed files with 171 additions and 1 deletions
+50 -1
View File
@@ -1141,7 +1141,8 @@ public class PBXProjGenerator {
func addResourcesBuildPhase() {
let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences
if !resourcesBuildPhaseFiles.isEmpty {
let hasSynchronizedRootGroups = sourceFiles.contains { $0.fileReference is PBXFileSystemSynchronizedRootGroup }
if !resourcesBuildPhaseFiles.isEmpty || hasSynchronizedRootGroups {
let resourcesBuildPhase = addObject(PBXResourcesBuildPhase(files: resourcesBuildPhaseFiles))
buildPhases.append(resourcesBuildPhase)
}
@@ -1460,9 +1461,57 @@ public class PBXProjGenerator {
// add fileSystemSynchronizedGroups
let synchronizedRootGroups = sourceFiles.compactMap { $0.fileReference as? PBXFileSystemSynchronizedRootGroup }
if !synchronizedRootGroups.isEmpty {
for syncedGroup in synchronizedRootGroups {
configureMembershipExceptions(
for: syncedGroup,
target: target,
targetObject: targetObject,
infoPlistFiles: infoPlistFiles
)
}
targetObject.fileSystemSynchronizedGroups = synchronizedRootGroups
}
}
private func configureMembershipExceptions(
for syncedGroup: PBXFileSystemSynchronizedRootGroup,
target: Target,
targetObject: PBXTarget,
infoPlistFiles: [Config: String]
) {
guard let syncedGroupPath = syncedGroup.path else { return }
let syncedPath = (project.basePath + Path(syncedGroupPath)).normalize()
guard let targetSource = target.sources.first(where: {
(project.basePath + $0.path).normalize() == syncedPath
}) else { return }
var exceptions: Set<String> = Set(
sourceGenerator.expandedExcludes(for: targetSource)
.compactMap { try? $0.relativePath(from: syncedPath).string }
)
for infoPlistPath in Set(infoPlistFiles.values) {
let relative = try? (project.basePath + infoPlistPath).normalize()
.relativePath(from: syncedPath)
if let rel = relative?.string, !rel.hasPrefix("..") {
exceptions.insert(rel)
}
}
guard !exceptions.isEmpty else { return }
let exceptionSet = PBXFileSystemSynchronizedBuildFileExceptionSet(
target: targetObject,
membershipExceptions: exceptions.sorted(),
publicHeaders: nil,
privateHeaders: nil,
additionalCompilerFlagsByRelativePath: nil,
attributesByRelativePath: nil
)
addObject(exceptionSet)
syncedGroup.exceptions = (syncedGroup.exceptions ?? []) + [exceptionSet]
}
private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? {
switch filter {
@@ -372,6 +372,11 @@ class SourceGenerator {
return variantGroup
}
/// Returns the expanded set of excluded paths for a target source by resolving its exclude glob patterns.
func expandedExcludes(for targetSource: TargetSource) -> Set<Path> {
getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes)
}
/// Collects all the excluded paths within the targetSource
private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set<Path> {
let rootSourcePath = project.basePath + targetSource.path
@@ -830,9 +830,23 @@
FED40A89162E446494DDE7C7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
9A259ACEBCE19CC5F22B6DD4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
ExcludedFile.swift,
Info.plist,
);
target = 0867B0DACEF28C11442DE8F7 /* App_iOS */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
AE2AB2772F70DFFF402AA02B /* SyncedFolder */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
9A259ACEBCE19CC5F22B6DD4 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
);
explicitFileTypes = {
};
explicitFolders = (
@@ -0,0 +1 @@
// excluded
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
+3
View File
@@ -166,6 +166,9 @@ targets:
- String Catalogs/LocalizableStrings.xcstrings
- path: SyncedFolder
type: syncedFolder
excludes:
- ExcludedFile.swift
- Info.plist
settings:
INFOPLIST_FILE: App_iOS/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.project.app
@@ -120,6 +120,99 @@ class SourceGeneratorTests: XCTestCase {
try expect([syncedFolder]) == pbxProj.nativeTargets.first?.fileSystemSynchronizedGroups
}
$0.it("adds excludes as membership exceptions for synced folder") {
let directories = """
Sources:
- a.swift
- b.swift
- Generated:
- c.generated.swift
- d.generated.swift
"""
try createDirectories(directories)
let source = TargetSource(path: "Sources", excludes: ["b.swift", "Generated/*.generated.swift"], type: .syncedFolder)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)
let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
let exceptionSet = try unwrap(exceptionSets?.first)
let exceptions = try unwrap(exceptionSet.membershipExceptions)
try expect(exceptions.contains("b.swift")) == true
try expect(exceptions.contains("Generated/c.generated.swift")) == true
try expect(exceptions.contains("Generated/d.generated.swift")) == true
try expect(exceptions.contains("a.swift")) == false
}
$0.it("auto-excludes Info.plist from synced folder membership") {
let directories = """
Sources:
- a.swift
- Info.plist
"""
try createDirectories(directories)
let source = TargetSource(path: "Sources", type: .syncedFolder)
let target = Target(
name: "Test",
type: .application,
platform: .iOS,
settings: try Settings(jsonDictionary: ["INFOPLIST_FILE": "Sources/Info.plist"]),
sources: [source]
)
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)
let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }
let exceptionSet = try unwrap(exceptionSets?.first)
let exceptions = try unwrap(exceptionSet.membershipExceptions)
try expect(exceptions.contains("Info.plist")) == true
}
$0.it("creates no exception set for synced folder without excludes") {
let directories = """
Sources:
- a.swift
"""
try createDirectories(directories)
let source = TargetSource(path: "Sources", type: .syncedFolder)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
let syncedFolder = try unwrap(syncedFolders.first)
try expect(syncedFolder.exceptions?.isEmpty ?? true) == true
}
$0.it("adds empty resources build phase for synced folder") {
let directories = """
Sources:
- a.swift
"""
try createDirectories(directories)
let source = TargetSource(path: "Sources", type: .syncedFolder)
let target = Target(name: "Test", type: .application, platform: .iOS, sources: [source])
let project = Project(basePath: directoryPath, name: "Test", targets: [target])
let pbxProj = try project.generatePbxProj()
let nativeTarget = try unwrap(pbxProj.nativeTargets.first)
let hasResourcesPhase = nativeTarget.buildPhases.contains { $0 is PBXResourcesBuildPhase }
try expect(hasResourcesPhase) == true
}
$0.it("supports frameworks in sources") {
let directories = """
Sources: