diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e0f80d..b0561ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,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 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 +- Fix `.DS_Store` and `.orig` files causing projects to be regenerated when using `--use-cache` #1622 @kimjune01 ### Internal - Use a dedicated local package in the SPM fixture so generated fixtures don't depend on the checkout directory name, such as when running tests from a git worktree @yonaskolb diff --git a/Sources/ProjectSpec/FileType.swift b/Sources/ProjectSpec/FileType.swift index 5e5a6575..22ded342 100644 --- a/Sources/ProjectSpec/FileType.swift +++ b/Sources/ProjectSpec/FileType.swift @@ -7,6 +7,7 @@ import Foundation import JSONUtilities +import PathKit import enum XcodeProj.BuildPhase public struct FileType: Equatable { @@ -124,3 +125,22 @@ extension FileType { "xctestplan": FileType(buildPhase: BuildPhaseSpec.none), ] } + +extension FileType { + + /// File names that are never added to the project or tracked by the cache + static let defaultExcludedFiles: Set = [ + ".DS_Store", + ] + + /// File extensions that are never added to the project or tracked by the cache + static let defaultExcludedExtensions: Set = [ + "orig", + ] + + /// Checks whether the path is in any default excludes + public static func isDefaultExcluded(_ path: Path) -> Bool { + defaultExcludedFiles.contains(path.lastComponent) + || (path.extension.map(defaultExcludedExtensions.contains) ?? false) + } +} diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index 5244d28d..06f3d398 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -272,12 +272,16 @@ extension Project: PathContainer { extension Project { public var allTrackedFiles: [Path] { + // Skip files that SourceGenerator never adds to the project. Directories are kept, as they can still become groups + func trackedChildren(of path: Path) -> [Path] { + ((try? path.recursiveChildren()) ?? []).filter { $0.isDirectory || !FileType.isDefaultExcluded($0) } + } + var files: [Path] = [] files.append(contentsOf: configFilePaths) for fileGroup in fileGroups { let fileGroupPath = basePath + fileGroup - let fileGroupChildren = (try? fileGroupPath.recursiveChildren()) ?? [] - files.append(contentsOf: fileGroupChildren) + files.append(contentsOf: trackedChildren(of: fileGroupPath)) files.append(fileGroupPath) } @@ -289,11 +293,10 @@ extension Project { files.append(contentsOf: target.configFilePaths) for source in target.sources { let sourcePath = basePath + source.path - + let type = source.type ?? options.defaultSourceDirectoryType ?? .group if type.projectTracksChildren { - let sourceChildren = (try? sourcePath.recursiveChildren()) ?? [] - files.append(contentsOf: sourceChildren) + files.append(contentsOf: trackedChildren(of: sourcePath)) } files.append(sourcePath) } diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 10313f47..d07737ae 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -23,13 +23,6 @@ class SourceGenerator { private let project: Project let pbxProj: PBXProj - private var defaultExcludedFiles = [ - ".DS_Store", - ] - private let defaultExcludedExtensions = [ - "orig", - ] - private(set) var knownRegions: Set = [] /// The effective base path for resolving group and file paths in the generated project. @@ -471,8 +464,7 @@ class SourceGenerator { /// 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 !FileType.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/ProjectSpecTests/ProjectSpecTests.swift b/Tests/ProjectSpecTests/ProjectSpecTests.swift index c308df96..771a3fd2 100644 --- a/Tests/ProjectSpecTests/ProjectSpecTests.swift +++ b/Tests/ProjectSpecTests/ProjectSpecTests.swift @@ -591,6 +591,84 @@ class ProjectSpecTests: XCTestCase { } } + func testAllTrackedFilesExcludesIgnoredFiles() { + describe { + let directoryPath = Path(components: [NSTemporaryDirectory(), ProcessInfo.processInfo.globallyUniqueString]) + + $0.after { + try? directoryPath.delete() + } + + $0.it("excludes .DS_Store and .orig files from tracked source files") { + let sourcesDir = directoryPath + "Sources" + let featureDir = sourcesDir + "Feature" + let assetsDir = sourcesDir + "Assets.xcassets" + try featureDir.mkpath() + try assetsDir.mkpath() + try (sourcesDir + "main.swift").write("") + try (sourcesDir + ".DS_Store").write("") + try (sourcesDir + "file.swift.orig").write("") + try (featureDir + "Feature.swift").write("") + try (featureDir + ".DS_Store").write("") + try (assetsDir + ".DS_Store").write("") + + let target = Target( + name: "App", + type: .application, + platform: .iOS, + sources: [TargetSource(path: "Sources")] + ) + let project = Project(basePath: directoryPath, name: "Test", targets: [target]) + + try expect(Set(project.allTrackedFiles)) == [ + sourcesDir, + sourcesDir + "main.swift", + featureDir, + featureDir + "Feature.swift", + assetsDir, + ] + } + + $0.it("excludes .DS_Store and .orig files from tracked fileGroup files") { + let fileGroupDir = directoryPath + "Resources" + try fileGroupDir.mkpath() + try (fileGroupDir + "image.png").write("") + try (fileGroupDir + ".DS_Store").write("") + try (fileGroupDir + "image.png.orig").write("") + + let project = Project( + basePath: directoryPath, + name: "Test", + fileGroups: ["Resources"] + ) + + try expect(Set(project.allTrackedFiles)) == [ + fileGroupDir, + fileGroupDir + "image.png", + ] + } + } + } + + func testDefaultExcludedFiles() { + describe { + $0.it("matches default excluded file names and extensions") { + try expect(FileType.isDefaultExcluded(".DS_Store")) == true + try expect(FileType.isDefaultExcluded("Sources/.DS_Store")) == true + try expect(FileType.isDefaultExcluded("Sources/file.swift.orig")) == true + } + + $0.it("doesn't match similar names") { + try expect(FileType.isDefaultExcluded("Sources/file.swift")) == false + try expect(FileType.isDefaultExcluded("Sources/file.original")) == false + try expect(FileType.isDefaultExcluded("Sources/orig")) == false + try expect(FileType.isDefaultExcluded("Sources/.orig")) == false + try expect(FileType.isDefaultExcluded("Sources/file.ORIG")) == false + try expect(FileType.isDefaultExcluded("Sources/DS_Store")) == false + } + } + } + func testJSONEncodable() { describe { $0.it("encodes to json") {