diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index ceccfc4a..cba17c6b 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -200,7 +200,13 @@ class SourceGenerator { let createIntermediateGroups = project.options.createIntermediateGroups let parentPath = path.parent() + + guard !isInsideSyncedFolder(path: path) else { + return getFileReference(path: path, inPath: project.basePath, sourceTree: .sourceRoot) + } + let fileReference = getFileReference(path: path, inPath: parentPath) + let parentGroup = getGroup( path: parentPath, mergingChildren: [fileReference], @@ -277,6 +283,19 @@ class SourceGenerator { } } + /// Whether the given path falls inside a target source configured as a synced folder. + /// Checks the project spec directly because configFiles are resolved before target sources + /// populate `syncedGroupsByPath`. + private func isInsideSyncedFolder(path: Path) -> Bool { + let relativePath = (try? path.relativePath(from: project.basePath)) ?? path + return project.targets.contains { target in + target.sources.contains { source in + let type = source.type ?? (project.options.defaultSourceDirectoryType ?? .group) + return type == .syncedFolder && relativePath.string.hasPrefix(source.path + "/") + } + } + } + /// returns a default build phase for a given path. This is based off the filename private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> BuildPhaseSpec? { if let buildPhase = getFileType(path: path)?.buildPhase { @@ -350,7 +369,7 @@ class SourceGenerator { groupReference = addObject(group) groupsByPath[path] = groupReference - if isTopLevelGroup { + if isTopLevelGroup && !isInsideSyncedFolder(path: path) { rootGroups.insert(groupReference) } } @@ -396,6 +415,8 @@ class SourceGenerator { if child.isDirectory && !Xcode.isDirectoryFileWrapper(path: child) { findExceptions(in: child) } + } else if child.isDirectory && !Xcode.isDirectoryFileWrapper(path: child) { + findExceptions(in: child) } else { exceptions.insert(child) } diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index 54db7f29..2fd5a622 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -415,7 +415,7 @@ class SourceGeneratorTests: XCTestCase { try expect(exceptions.contains("Nested/b.swift")) == false } - $0.it("excludes entire subdirectory as single exception when no files in it are included") { + $0.it("excludes individual files in subdirectory when no files in it are included") { let directories = """ Sources: - a.swift @@ -436,10 +436,11 @@ class SourceGeneratorTests: XCTestCase { let exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet) let exceptions = try unwrap(exceptionSet.membershipExceptions) - // The whole directory should be a single exception entry, not each file within it - try expect(exceptions.contains("ExcludedDir")) == true - try expect(exceptions.contains("ExcludedDir/x.swift")) == false - try expect(exceptions.contains("ExcludedDir/y.swift")) == false + // Xcode does not recursively exclude directory contents from membershipExceptions, + // so individual files must be listed instead of the directory name + try expect(exceptions.contains("ExcludedDir")) == false + try expect(exceptions.contains("ExcludedDir/x.swift")) == true + try expect(exceptions.contains("ExcludedDir/y.swift")) == true try expect(exceptions.contains("a.swift")) == false } @@ -491,6 +492,34 @@ class SourceGeneratorTests: XCTestCase { try expect(appGroup === testsGroup) == true } + $0.it("does not create duplicate group for configFiles inside synced folder") { + let directories = """ + Sources: + - a.swift + - Config: + - config.xcconfig + """ + try createDirectories(directories) + + let source = TargetSource(path: "Sources", type: .syncedFolder) + let target = Target(name: "Target1", type: .application, platform: .iOS, sources: [source]) + let project = Project( + basePath: directoryPath, + name: "Test", + targets: [target], + configFiles: ["Debug": "Sources/Config/config.xcconfig"] + ) + + let pbxProj = try project.generatePbxProj() + let mainGroup = try pbxProj.getMainGroup() + + let sourcesChildren = mainGroup.children.filter { $0.path == "Sources" || $0.name == "Sources" } + try expect(sourcesChildren.count) == 1 + + let syncedFolders = mainGroup.children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup } + try expect(syncedFolders.count) == 1 + } + $0.it("supports frameworks in sources") { let directories = """ Sources: