diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 1f20e850..001fef22 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -1489,7 +1489,7 @@ public class PBXProjGenerator { }) else { return } var exceptions: Set = Set( - sourceGenerator.syncedFolderExceptions(for: targetSource, at: syncedPath) + sourceGenerator.syncedFolderExceptions(for: targetSource, at: syncedPath, targetType: target.type) .compactMap { try? $0.relativePath(from: syncedPath).string } ) diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 59b28588..5fad6aeb 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -404,13 +404,9 @@ class SourceGenerator { } /// Returns the expanded set of exception paths for a synced folder, including excludes and non-included files. - func syncedFolderExceptions(for targetSource: TargetSource, at syncedPath: Path) -> Set { + func syncedFolderExceptions(for targetSource: TargetSource, at syncedPath: Path, targetType: PBXProductType) -> Set { let excludePaths = expandedExcludes(for: targetSource) - if targetSource.includes.isEmpty { - return excludePaths - } - - let includePaths = SortedArray(getSourceMatches(targetSource: targetSource, patterns: targetSource.includes)) + let includePaths = targetSource.includes.isEmpty ? nil : SortedArray(getSourceMatches(targetSource: targetSource, patterns: targetSource.includes)) var exceptions: Set = [] func findExceptions(in path: Path) { @@ -420,6 +416,11 @@ class SourceGenerator { if isIncludedPath(child, excludePaths: excludePaths, includePaths: includePaths) { if child.isDirectory && !Xcode.isDirectoryFileWrapper(path: child) { findExceptions(in: child) + } else { + let buildPhase = getDefaultBuildPhase(for: child, targetType: targetType) + if buildPhase == BuildPhaseSpec.none { + exceptions.insert(child) + } } } else if child.isDirectory && !Xcode.isDirectoryFileWrapper(path: child) { findExceptions(in: child) @@ -469,10 +470,15 @@ class SourceGenerator { } } + /// Checks whether the path is in any default excludes + func isDefaultExcluded(_ path: Path) -> Bool { + return defaultExcludedFiles.contains(where: { path.lastComponent == $0 }) + || (path.extension.map(defaultExcludedExtensions.contains) ?? false) + } + /// Checks whether the path is not in any default or TargetSource excludes func isIncludedPath(_ path: Path, excludePaths: Set, includePaths: SortedArray?) -> Bool { - return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 }) - && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) + return !isDefaultExcluded(path) && !excludePaths.contains(path) // If includes is empty, it's included. If it's not empty, the path either needs to match exactly, or it needs to be a direct parent of an included path. && (includePaths.flatMap { _isIncludedPathSorted(path, sortedPaths: $0) } ?? true) diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index b8450c5f..82bda703 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -36,7 +36,7 @@ class SourceGeneratorTests: XCTestCase { } } - let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil } + let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil || $0.lastComponent.hasPrefix(".") } for file in files { try file.parent().mkpath() try file.write("") @@ -203,6 +203,33 @@ class SourceGeneratorTests: XCTestCase { try expect(exceptions.contains("a.swift")) == false } + $0.it("excludes .DS_Store and .xcconfig from synced folder membership") { + let directories = """ + Sources: + - a.swift + - .DS_Store + - config.xcconfig + - Subfolder: + - nested.xcconfig + """ + 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 syncedFolder = try unwrap(pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }.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("config.xcconfig")) == true + try expect(exceptions.contains("Subfolder/nested.xcconfig")) == true + try expect(exceptions.contains(".DS_Store")) == true + } + $0.it("adds membership exceptions for nested synced folder with intermediate groups") { let directories = """ Sources: @@ -308,6 +335,82 @@ class SourceGeneratorTests: XCTestCase { try expect(syncedFolders.count) == 1 } + $0.it("excludes xcconfig files from synced folder membership") { + let directories = """ + Sources: + - a.swift + - Config.xcconfig + - Icon.icon + """ + try createDirectories(directories) + _ = try createFile(at: "Sources/README", content: "") + + 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) + + let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet } + let exceptionSet = try unwrap(exceptionSets?.first) + let exceptions = try unwrap(exceptionSet.membershipExceptions) + + try expect(exceptions.contains("Config.xcconfig")) == true + try expect(exceptions.contains("README")) == false + try expect(exceptions.contains("a.swift")) == false + try expect(exceptions.contains("Icon.icon")) == false + } + + $0.it("excludes nested xcconfig files from synced folder membership") { + let directories = """ + Sources: + Nested: + - a.swift + - Config.xcconfig + """ + 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) + + let exceptionSets = syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet } + let exceptionSet = try unwrap(exceptionSets?.first) + let exceptions = try unwrap(exceptionSet.membershipExceptions) + + try expect(exceptions.contains("Nested/Config.xcconfig")) == true + try expect(exceptions.contains("Nested/a.swift")) == false + } + + $0.it("includes default excluded files in synced folder membership exceptions") { + let directories = """ + Sources: + - a.swift + - .DS_Store + - a.swift.orig + """ + 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) + + let exceptionSets = try unwrap(syncedFolder.exceptions?.compactMap { $0 as? PBXFileSystemSynchronizedBuildFileExceptionSet }) + let exceptionSet = try unwrap(exceptionSets.first) + let exceptions = try unwrap(exceptionSet.membershipExceptions) + try expect(exceptions.contains(".DS_Store")) == true + try expect(exceptions.contains("a.swift.orig")) == true + } $0.it("supports includes for synced folders") { let directories = """ Sources: