Skip to content
Merged
51 changes: 50 additions & 1 deletion Sources/XcodeGenKit/PBXProjGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions Sources/XcodeGenKit/SourceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
1 change: 1 addition & 0 deletions Tests/Fixtures/TestProject/SyncedFolder/ExcludedFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// excluded
5 changes: 5 additions & 0 deletions Tests/Fixtures/TestProject/SyncedFolder/Info.plist
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions Tests/Fixtures/TestProject/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions Tests/XcodeGenKitTests/SourceGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down