From 3bbb4ffd338e2097d1ceb47ba2cea138d1f87e78 Mon Sep 17 00:00:00 2001 From: June Kim Date: Tue, 12 May 2026 21:08:23 -0700 Subject: [PATCH 1/3] Add failing test: allTrackedFiles includes .DS_Store and .orig files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #1032 — .DS_Store files in the cache cause spurious project regeneration. The source generator already excludes these files, but allTrackedFiles (used by CacheFile) does not. This commit adds a test that asserts .DS_Store and .orig files are excluded from allTrackedFiles. The test fails on the current implementation. --- Tests/ProjectSpecTests/ProjectSpecTests.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Tests/ProjectSpecTests/ProjectSpecTests.swift b/Tests/ProjectSpecTests/ProjectSpecTests.swift index c308df96..a6d40aed 100644 --- a/Tests/ProjectSpecTests/ProjectSpecTests.swift +++ b/Tests/ProjectSpecTests/ProjectSpecTests.swift @@ -591,6 +591,69 @@ class ProjectSpecTests: XCTestCase { } } + func testAllTrackedFilesExcludesIgnoredFiles() { + describe { + let directoryPath = Path(ProcessInfo.processInfo.globallyUniqueString) + + $0.before { + try? directoryPath.delete() + } + + $0.after { + try? directoryPath.delete() + } + + $0.it("excludes .DS_Store and .orig files from tracked files") { + // Create source directory with a mix of valid and ignored files + let sourcesDir = directoryPath + "Sources" + try sourcesDir.mkpath() + try (sourcesDir + "main.swift").write("") + try (sourcesDir + ".DS_Store").write("") + try (sourcesDir + "file.swift.orig").write("") + + let target = Target( + name: "App", + type: .application, + platform: .iOS, + sources: [TargetSource(path: "Sources")] + ) + let project = Project(basePath: directoryPath, name: "Test", targets: [target]) + let trackedFiles = project.allTrackedFiles + + let trackedFileStrings = trackedFiles.map { $0.string } + let hasDSStore = trackedFileStrings.contains { $0.contains(".DS_Store") } + let hasOrig = trackedFileStrings.contains { $0.hasSuffix(".orig") } + let hasSwift = trackedFileStrings.contains { $0.contains("main.swift") } + + try expect(hasDSStore).to.beFalse() + try expect(hasOrig).to.beFalse() + try expect(hasSwift).to.beTrue() + } + + $0.it("excludes .DS_Store from fileGroup tracked files") { + // Create fileGroup directory with .DS_Store + let fileGroupDir = directoryPath + "Resources" + try fileGroupDir.mkpath() + try (fileGroupDir + "image.png").write("") + try (fileGroupDir + ".DS_Store").write("") + + let project = Project( + basePath: directoryPath, + name: "Test", + fileGroups: ["Resources"] + ) + let trackedFiles = project.allTrackedFiles + + let trackedFileStrings = trackedFiles.map { $0.string } + let hasDSStore = trackedFileStrings.contains { $0.contains(".DS_Store") } + let hasImage = trackedFileStrings.contains { $0.contains("image.png") } + + try expect(hasDSStore).to.beFalse() + try expect(hasImage).to.beTrue() + } + } + } + func testJSONEncodable() { describe { $0.it("encodes to json") { From 4f7f242084065a86e63a182a342d2cae3610708a Mon Sep 17 00:00:00 2001 From: June Kim Date: Tue, 12 May 2026 21:09:30 -0700 Subject: [PATCH 2/3] Exclude .DS_Store and .orig files from cache tracking allTrackedFiles now filters out the same files that SourceGenerator already excludes from project generation. Previously, .DS_Store files appearing in source directories would invalidate the cache and trigger a full project regeneration even though nothing in the generated project would change. Resolves #1032 --- Sources/ProjectSpec/Project.swift | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index e872a27b..5c171f05 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -252,6 +252,21 @@ extension Project: PathContainer { extension Project { + /// Files excluded from cache tracking to match SourceGenerator's default exclusions. + /// Without this filter, changes to these files cause spurious project regeneration. + private static let defaultExcludedFileNames: Set = [".DS_Store"] + private static let defaultExcludedExtensions: Set = ["orig"] + + private static func isTrackedFile(_ path: Path) -> Bool { + if defaultExcludedFileNames.contains(path.lastComponent) { + return false + } + if let ext = path.extension, defaultExcludedExtensions.contains(ext) { + return false + } + return true + } + public var allTrackedFiles: [Path] { var files: [Path] = [] files.append(contentsOf: configFilePaths) @@ -270,7 +285,7 @@ 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()) ?? [] @@ -279,7 +294,7 @@ extension Project { files.append(sourcePath) } } - return files + return files.filter(Self.isTrackedFile) } } From e5d7b777e5a597f705ff013f17eaf4bb547f7635 Mon Sep 17 00:00:00 2001 From: June Kim Date: Tue, 12 May 2026 21:13:57 -0700 Subject: [PATCH 3/3] Use NSTemporaryDirectory for test temp files to avoid repo pollution --- Tests/ProjectSpecTests/ProjectSpecTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ProjectSpecTests/ProjectSpecTests.swift b/Tests/ProjectSpecTests/ProjectSpecTests.swift index a6d40aed..43914c30 100644 --- a/Tests/ProjectSpecTests/ProjectSpecTests.swift +++ b/Tests/ProjectSpecTests/ProjectSpecTests.swift @@ -593,7 +593,7 @@ class ProjectSpecTests: XCTestCase { func testAllTrackedFilesExcludesIgnoredFiles() { describe { - let directoryPath = Path(ProcessInfo.processInfo.globallyUniqueString) + let directoryPath = Path(components: [NSTemporaryDirectory(), ProcessInfo.processInfo.globallyUniqueString]) $0.before { try? directoryPath.delete()