diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ff4ff89..ec4bb5fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed - Fix `syncedFolder` source paths being relative to the spec directory instead of the project directory when they differ, which caused Xcode to treat the synced folder as empty #1636 @Ckitakishi +- Fix `destinationFilters` and `inferDestinationFiltersByPath` being ignored for `syncedFolder` sources. They are now written as `platformFiltersByRelativePath` on the synced folder's exception set #1647 @anandghegde - Fix nested target attributes (e.g. `attributes.SystemCapabilities`) being serialized as a stringified Swift `Dictionary` description instead of a proper nested plist dictionary, which also caused non-deterministic key ordering in generated `project.pbxproj` files across runs #1639 @imadaan @sergeyospanov ### Internal diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 604ff473b..0fa83cf51 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -1501,15 +1501,19 @@ public class PBXProjGenerator { } } - guard !exceptions.isEmpty else { return } + let platformFilters = sourceGenerator.syncedFolderPlatformFilters(for: targetSource, at: syncedPath) + .filter { !exceptions.contains($0.key) } + + guard !exceptions.isEmpty || !platformFilters.isEmpty else { return } let exceptionSet = PBXFileSystemSynchronizedBuildFileExceptionSet( target: targetObject, - membershipExceptions: exceptions.sorted(), + membershipExceptions: exceptions.isEmpty ? nil : exceptions.sorted(), publicHeaders: nil, privateHeaders: nil, additionalCompilerFlagsByRelativePath: nil, - attributesByRelativePath: nil + attributesByRelativePath: nil, + platformFiltersByRelativePath: platformFilters.isEmpty ? nil : platformFilters ) addObject(exceptionSet) syncedGroup.exceptions = (syncedGroup.exceptions ?? []) + [exceptionSet] diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 10313f47f..de381d327 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -433,6 +433,32 @@ class SourceGenerator { return exceptions } + /// Returns the platform filters for each file in a synced folder, keyed by path relative to the synced folder. + /// Xcode only honors file paths in `platformFiltersByRelativePath`, so directories are expanded into their files. + func syncedFolderPlatformFilters(for targetSource: TargetSource, at syncedPath: Path) -> [String: [String]] { + guard targetSource.destinationFilters?.isEmpty == false || targetSource.inferDestinationFiltersByPath == true else { + return [:] + } + + var platformFilters: [String: [String]] = [:] + + func findFiles(in path: Path) { + guard let children = try? path.children() else { return } + + for child in children where !child.lastComponent.hasPrefix(".") { + if child.isDirectory && !Xcode.isDirectoryFileWrapper(path: child) { + findFiles(in: child) + } else if let filters = makeDestinationFilters(for: child, with: targetSource.destinationFilters, or: targetSource.inferDestinationFiltersByPath), + let relativePath = try? child.relativePath(from: syncedPath) { + platformFilters[relativePath.string] = filters + } + } + } + + findFiles(in: syncedPath) + return platformFilters + } + /// Collects all the excluded paths within the targetSource private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set { let rootSourcePath = project.basePath + targetSource.path diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/SyncedSources/SyncedFile_iOS.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/SyncedSources/SyncedFile_iOS.swift new file mode 100644 index 000000000..e795540cf --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/SyncedSources/SyncedFile_iOS.swift @@ -0,0 +1 @@ +let syncedPlatformName = "iOS" diff --git a/Tests/Fixtures/TestProject/App_supportedDestinations/SyncedSources/SyncedFile_tvOS.swift b/Tests/Fixtures/TestProject/App_supportedDestinations/SyncedSources/SyncedFile_tvOS.swift new file mode 100644 index 000000000..41fc53f39 --- /dev/null +++ b/Tests/Fixtures/TestProject/App_supportedDestinations/SyncedSources/SyncedFile_tvOS.swift @@ -0,0 +1 @@ +let syncedPlatformName = "tvOS" diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index bbfb8e41c..2c50979e1 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -841,6 +841,18 @@ ); target = 0867B0DACEF28C11442DE8F7 /* App_iOS */; }; + 9A8E6A7891C068692E5463B2 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + platformFiltersByRelativePath = { + SyncedFile_iOS.swift = ( + ios, + ); + SyncedFile_tvOS.swift = ( + tvos, + ); + }; + target = C0570E2FB50D830D8D423396 /* App_supportedDestinations */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -869,6 +881,19 @@ path = SyncedFolder; sourceTree = ""; }; + E55B4EB82638DF874A0308EC /* SyncedSources */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 9A8E6A7891C068692E5463B2 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + name = SyncedSources; + path = SyncedSources; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1448,6 +1473,7 @@ children = ( 85FEBB7D2103B020423407A2 /* Sources */, 69C61547C081D04364A5DE42 /* Storyboards */, + E55B4EB82638DF874A0308EC /* SyncedSources */, ); path = App_supportedDestinations; sourceTree = ""; @@ -2281,6 +2307,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + E55B4EB82638DF874A0308EC /* SyncedSources */, + ); name = App_supportedDestinations; packageProductDependencies = ( C7F9B7EDE85527EFEA85D46D /* Swinject */, diff --git a/Tests/Fixtures/TestProject/project.yml b/Tests/Fixtures/TestProject/project.yml index dbd4c317f..a826c30f8 100644 --- a/Tests/Fixtures/TestProject/project.yml +++ b/Tests/Fixtures/TestProject/project.yml @@ -257,6 +257,10 @@ targets: - path: App_supportedDestinations/Storyboards group: App_supportedDestinations destinationFilters: [iOS] + - path: App_supportedDestinations/SyncedSources + group: App_supportedDestinations + type: syncedFolder + inferDestinationFiltersByPath: true dependencies: - package: Swinject product: Swinject diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index f24ad71e4..5bcba9b16 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -353,6 +353,58 @@ class SourceGeneratorTests: XCTestCase { try expect(exceptions.contains("included.swift")) == false } + $0.it("adds destinationFilters as platform filters for synced folder files") { + let directories = """ + Sources: + - a.swift + - excluded.swift + - Nested: + - b.swift + """ + try createDirectories(directories) + + let source = TargetSource(path: "Sources", excludes: ["excluded.swift"], type: .syncedFolder, destinationFilters: [.iOS]) + let target = Target(name: "Test", type: .application, platform: .auto, supportedDestinations: [.iOS, .tvOS], 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 exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet) + try expect(exceptionSet.membershipExceptions) == ["excluded.swift"] + try expect(exceptionSet.platformFiltersByRelativePath) == [ + "a.swift": ["ios"], + "Nested/b.swift": ["ios"], + ] + } + + $0.it("infers platform filters by path for synced folder files") { + let directories = """ + Sources: + - common.swift + - File_tvOS.swift + - iOS: + - a.swift + """ + try createDirectories(directories) + + let source = TargetSource(path: "Sources", type: .syncedFolder, inferDestinationFiltersByPath: true) + let target = Target(name: "Test", type: .application, platform: .auto, supportedDestinations: [.iOS, .tvOS], 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 exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet) + try expect(exceptionSet.membershipExceptions) == nil + try expect(exceptionSet.platformFiltersByRelativePath) == [ + "File_tvOS.swift": ["tvos"], + "iOS/a.swift": ["ios"], + ] + } + $0.it("merges explicitFolders for synced folders across targets") { let directories = """ Sources: