mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
Fix issue 1602 - Synced folders: includes silently ignored + no deduplication across targets (#1604)
* Fix issue 1602 - Synced folders: includes silently ignored + no deduplication across targets * Add more tests
This commit is contained in:
@@ -1489,7 +1489,7 @@ public class PBXProjGenerator {
|
||||
}) else { return }
|
||||
|
||||
var exceptions: Set<String> = Set(
|
||||
sourceGenerator.expandedExcludes(for: targetSource)
|
||||
sourceGenerator.syncedFolderExceptions(for: targetSource, at: syncedPath)
|
||||
.compactMap { try? $0.relativePath(from: syncedPath).string }
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class SourceGenerator {
|
||||
private var fileReferencesByPath: [String: PBXFileElement] = [:]
|
||||
private var groupsByPath: [Path: PBXGroup] = [:]
|
||||
private var variantGroupsByPath: [Path: PBXVariantGroup] = [:]
|
||||
private var syncedGroupsByPath: [String: PBXFileSystemSynchronizedRootGroup] = [:]
|
||||
|
||||
private let project: Project
|
||||
let pbxProj: PBXProj
|
||||
@@ -377,6 +378,34 @@ class SourceGenerator {
|
||||
getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes)
|
||||
}
|
||||
|
||||
/// 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<Path> {
|
||||
let excludePaths = expandedExcludes(for: targetSource)
|
||||
if targetSource.includes.isEmpty {
|
||||
return excludePaths
|
||||
}
|
||||
|
||||
let includePaths = SortedArray(getSourceMatches(targetSource: targetSource, patterns: targetSource.includes))
|
||||
var exceptions: Set<Path> = []
|
||||
|
||||
func findExceptions(in path: Path) {
|
||||
guard let children = try? path.children() else { return }
|
||||
|
||||
for child in children {
|
||||
if isIncludedPath(child, excludePaths: excludePaths, includePaths: includePaths) {
|
||||
if child.isDirectory && !Xcode.isDirectoryFileWrapper(path: child) {
|
||||
findExceptions(in: child)
|
||||
}
|
||||
} else {
|
||||
exceptions.insert(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findExceptions(in: syncedPath)
|
||||
return exceptions
|
||||
}
|
||||
|
||||
/// Collects all the excluded paths within the targetSource
|
||||
private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set<Path> {
|
||||
let rootSourcePath = project.basePath + targetSource.path
|
||||
@@ -711,15 +740,25 @@ class SourceGenerator {
|
||||
let relativePath = (try? path.relativePath(from: project.basePath)) ?? path
|
||||
let resolvedExplicitFolders = resolveExplicitFolders(targetSource: targetSource)
|
||||
|
||||
let syncedRootGroup = PBXFileSystemSynchronizedRootGroup(
|
||||
sourceTree: .group,
|
||||
path: relativePath.string,
|
||||
name: targetSource.name,
|
||||
explicitFileTypes: [:],
|
||||
exceptions: [],
|
||||
explicitFolders: resolvedExplicitFolders
|
||||
)
|
||||
addObject(syncedRootGroup)
|
||||
let syncedRootGroup: PBXFileSystemSynchronizedRootGroup
|
||||
if let existingGroup = syncedGroupsByPath[relativePath.string] {
|
||||
syncedRootGroup = existingGroup
|
||||
let newExplicitFolders = Set(syncedRootGroup.explicitFolders ?? [])
|
||||
.union(resolvedExplicitFolders)
|
||||
.sorted()
|
||||
syncedRootGroup.explicitFolders = newExplicitFolders
|
||||
} else {
|
||||
syncedRootGroup = PBXFileSystemSynchronizedRootGroup(
|
||||
sourceTree: .group,
|
||||
path: relativePath.string,
|
||||
name: targetSource.name,
|
||||
explicitFileTypes: [:],
|
||||
exceptions: [],
|
||||
explicitFolders: resolvedExplicitFolders
|
||||
)
|
||||
addObject(syncedRootGroup)
|
||||
syncedGroupsByPath[relativePath.string] = syncedRootGroup
|
||||
}
|
||||
sourceReference = syncedRootGroup
|
||||
|
||||
if !(createIntermediateGroups || hasCustomParent) || path.parent() == project.basePath {
|
||||
|
||||
@@ -290,6 +290,207 @@ class SourceGeneratorTests: XCTestCase {
|
||||
try expect(hasResourcesPhase) == true
|
||||
}
|
||||
|
||||
$0.it("deduplicates synced folders across targets") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- a.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source = TargetSource(path: "Sources", type: .syncedFolder)
|
||||
let target1 = Target(name: "Target1", type: .application, platform: .iOS, sources: [source])
|
||||
let target2 = Target(name: "Target2", type: .application, platform: .iOS, sources: [source])
|
||||
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
|
||||
|
||||
let pbxProj = try project.generatePbxProj()
|
||||
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
||||
|
||||
try expect(syncedFolders.count) == 1
|
||||
}
|
||||
|
||||
$0.it("supports includes for synced folders") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- included.swift
|
||||
- excluded.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source = TargetSource(path: "Sources", includes: ["included.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("excluded.swift")) == true
|
||||
try expect(exceptions.contains("included.swift")) == false
|
||||
}
|
||||
|
||||
$0.it("merges explicitFolders for synced folders across targets") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- a.swift
|
||||
- FolderA:
|
||||
- b.swift
|
||||
- FolderB:
|
||||
- c.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source1 = TargetSource(path: "Sources", explicitFolders: ["FolderA"], type: .syncedFolder)
|
||||
let source2 = TargetSource(path: "Sources", explicitFolders: ["FolderB"], type: .syncedFolder)
|
||||
let target1 = Target(name: "Target1", type: .application, platform: .iOS, sources: [source1])
|
||||
let target2 = Target(name: "Target2", type: .application, platform: .iOS, sources: [source2])
|
||||
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
|
||||
|
||||
let pbxProj = try project.generatePbxProj()
|
||||
let syncedFolders = try pbxProj.getMainGroup().children.compactMap { $0 as? PBXFileSystemSynchronizedRootGroup }
|
||||
let syncedFolder = try unwrap(syncedFolders.first)
|
||||
|
||||
try expect(syncedFolder.explicitFolders?.sorted()) == ["FolderA", "FolderB"]
|
||||
}
|
||||
|
||||
$0.it("supports different includes for the same synced folder across targets") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- target1.swift
|
||||
- target2.swift
|
||||
- common.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source1 = TargetSource(path: "Sources", includes: ["target1.swift", "common.swift"], type: .syncedFolder)
|
||||
let source2 = TargetSource(path: "Sources", includes: ["target2.swift", "common.swift"], type: .syncedFolder)
|
||||
let target1 = Target(name: "Target1", type: .application, platform: .iOS, sources: [source1])
|
||||
let target2 = Target(name: "Target2", type: .application, platform: .iOS, sources: [source2])
|
||||
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
|
||||
|
||||
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 }
|
||||
try expect(exceptionSets?.count) == 2
|
||||
|
||||
let t1Exceptions = try unwrap(exceptionSets?.first { $0.target?.name == "Target1" }?.membershipExceptions)
|
||||
try expect(t1Exceptions.contains("target2.swift")) == true
|
||||
try expect(t1Exceptions.contains("target1.swift")) == false
|
||||
try expect(t1Exceptions.contains("common.swift")) == false
|
||||
|
||||
let t2Exceptions = try unwrap(exceptionSets?.first { $0.target?.name == "Target2" }?.membershipExceptions)
|
||||
try expect(t2Exceptions.contains("target1.swift")) == true
|
||||
try expect(t2Exceptions.contains("target2.swift")) == false
|
||||
try expect(t2Exceptions.contains("common.swift")) == false
|
||||
}
|
||||
|
||||
$0.it("correctly identifies exceptions for nested directories in includes") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- a.swift
|
||||
- Nested:
|
||||
- b.swift
|
||||
- c.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source = TargetSource(path: "Sources", includes: ["Nested/b.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 exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet)
|
||||
let exceptions = try unwrap(exceptionSet.membershipExceptions)
|
||||
|
||||
try expect(exceptions.contains("a.swift")) == true
|
||||
try expect(exceptions.contains("Nested/c.swift")) == true
|
||||
try expect(exceptions.contains("Nested/b.swift")) == false
|
||||
}
|
||||
|
||||
$0.it("excludes entire subdirectory as single exception when no files in it are included") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- a.swift
|
||||
- ExcludedDir:
|
||||
- x.swift
|
||||
- y.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source = TargetSource(path: "Sources", includes: ["a.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 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
|
||||
try expect(exceptions.contains("a.swift")) == false
|
||||
}
|
||||
|
||||
$0.it("respects excludes when includes are also specified") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- a.swift
|
||||
- b.swift
|
||||
- c.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
// includes a.swift and b.swift, but b.swift is also excluded → only a.swift is effectively included
|
||||
let source = TargetSource(path: "Sources", excludes: ["b.swift"], includes: ["a.swift", "b.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 exceptionSet = try unwrap(syncedFolder.exceptions?.first as? PBXFileSystemSynchronizedBuildFileExceptionSet)
|
||||
let exceptions = try unwrap(exceptionSet.membershipExceptions)
|
||||
|
||||
try expect(exceptions.contains("a.swift")) == false
|
||||
try expect(exceptions.contains("b.swift")) == true
|
||||
try expect(exceptions.contains("c.swift")) == true
|
||||
}
|
||||
|
||||
$0.it("deduplicates synced folders and both targets reference the same group object") {
|
||||
let directories = """
|
||||
Sources:
|
||||
- a.swift
|
||||
"""
|
||||
try createDirectories(directories)
|
||||
|
||||
let source = TargetSource(path: "Sources", type: .syncedFolder)
|
||||
let target1 = Target(name: "App", type: .application, platform: .iOS, sources: [source])
|
||||
let target2 = Target(name: "Tests", type: .unitTestBundle, platform: .iOS, sources: [source])
|
||||
let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2])
|
||||
|
||||
let pbxProj = try project.generatePbxProj()
|
||||
let nativeTargets = pbxProj.nativeTargets
|
||||
let appTarget = try unwrap(nativeTargets.first { $0.name == "App" })
|
||||
let testsTarget = try unwrap(nativeTargets.first { $0.name == "Tests" })
|
||||
|
||||
let appGroup = try unwrap(appTarget.fileSystemSynchronizedGroups?.first)
|
||||
let testsGroup = try unwrap(testsTarget.fileSystemSynchronizedGroups?.first)
|
||||
try expect(appGroup === testsGroup) == true
|
||||
}
|
||||
|
||||
$0.it("supports frameworks in sources") {
|
||||
let directories = """
|
||||
Sources:
|
||||
|
||||
Reference in New Issue
Block a user