Skip to content

Commit f98bc3e

Browse files
ViktorSimkoaciidgh
authored andcommitted
SR-1741: Add non-source files from source directories and root, ignore files from .gitignore (#1507)
* Add non-source files from source directories * ignore files from .gitignore * Simplify findNonSourceFiles, fix its documentation * Check if sources directories exist * Add tests for GenerateXcodeproj Add test cases for the inclusion of files in the root directory, for the inclusion of non-source files in source directories and for the exclusion of files by git. * Fix the style of trailing closures Put parentheses around the functional trailing closures * Refactor the finding and filtering of non-source files into a new function Pass all the extra file paths to `isGitIgnored` at once to improve performance * Only add non-source files to Xcode project if there is a working checkout * Exclude dot files in findNonSourceFiles * Move the check for dot files before the check for extensions in findNonSourceFiles Like this, we won't check the extension for dot files unnecessarily
1 parent 992c71d commit f98bc3e

File tree

4 files changed

+190
-8
lines changed

4 files changed

+190
-8
lines changed

Sources/Xcodeproj/generate().swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import POSIX
1313
import PackageGraph
1414
import PackageModel
1515
import PackageLoading
16+
import SourceControl
1617
import Utility
1718

1819
public struct XcodeprojOptions {
@@ -63,6 +64,7 @@ public func generate(
6364
projectName: String,
6465
xcodeprojPath: AbsolutePath,
6566
graph: PackageGraph,
67+
repositoryProvider: RepositoryProvider = GitRepositoryProvider(),
6668
options: XcodeprojOptions,
6769
diagnostics: DiagnosticsEngine
6870
) throws -> Xcode.Project {
@@ -82,10 +84,16 @@ public func generate(
8284
// references in the project.
8385
let extraDirs = try findDirectoryReferences(path: srcroot)
8486

87+
var extraFiles = [AbsolutePath]()
88+
if try repositoryProvider.checkoutExists(at: srcroot) {
89+
let workingCheckout = try repositoryProvider.openCheckout(at: srcroot)
90+
extraFiles = try getExtraFilesFor(package: graph.rootPackages[0], in: workingCheckout)
91+
}
92+
8593
/// Generate the contents of project.xcodeproj (inside the .xcodeproj).
8694
// FIXME: This could be more efficient by directly writing to a stream
8795
// instead of first creating a string.
88-
let project = try pbxproj(xcodeprojPath: xcodeprojPath, graph: graph, extraDirs: extraDirs, options: options, diagnostics: diagnostics)
96+
let project = try pbxproj(xcodeprojPath: xcodeprojPath, graph: graph, extraDirs: extraDirs, extraFiles: extraFiles, options: options, diagnostics: diagnostics)
8997
try open(xcodeprojPath.appending(component: "project.pbxproj")) { stream in
9098
// Serialize the project model we created to a plist, and return
9199
// its string description.
@@ -223,3 +231,40 @@ func generateSchemes(
223231
).generate()
224232
}
225233
}
234+
235+
// Find and return non-source files in the source directories and root that should be added
236+
// as a reference to the project.
237+
func getExtraFilesFor(package: ResolvedPackage, in workingCheckout: WorkingCheckout) throws -> [AbsolutePath] {
238+
let srcroot = package.path
239+
var extraFiles = try findNonSourceFiles(path: srcroot)
240+
241+
for target in package.targets {
242+
let sourcesDirectory = target.sources.root
243+
if localFileSystem.isDirectory(sourcesDirectory) {
244+
let sourcesExtraFiles = try findNonSourceFiles(path: sourcesDirectory, recursively: true)
245+
extraFiles.append(contentsOf: sourcesExtraFiles)
246+
}
247+
}
248+
249+
let isIgnored = try workingCheckout.areIgnored(extraFiles)
250+
extraFiles = extraFiles.enumerated().filter({ !isIgnored[$0.offset] }).map({ $0.element })
251+
252+
return extraFiles
253+
}
254+
255+
/// Finds the non-source files from `path`
256+
/// - parameters:
257+
/// - path: The path of the directory to get the files from
258+
/// - recursively: Specifies if the directory at `path` should be searched recursively
259+
func findNonSourceFiles(path: AbsolutePath, recursively: Bool = false) throws -> [AbsolutePath] {
260+
let filesFromPath = try walk(path, recursively: recursively)
261+
262+
return filesFromPath.filter({
263+
if !isFile($0) { return false }
264+
if $0.basename.hasPrefix(".") { return false }
265+
if let `extension` = $0.extension, SupportedLanguageExtension.validExtensions.contains(`extension`) {
266+
return false
267+
}
268+
return true
269+
})
270+
}

Sources/Xcodeproj/pbxproj().swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public func pbxproj(
3232
xcodeprojPath: AbsolutePath,
3333
graph: PackageGraph,
3434
extraDirs: [AbsolutePath],
35+
extraFiles: [AbsolutePath],
3536
options: XcodeprojOptions,
3637
diagnostics: DiagnosticsEngine,
3738
fileSystem: FileSystem = localFileSystem
@@ -40,6 +41,7 @@ public func pbxproj(
4041
xcodeprojPath: xcodeprojPath,
4142
graph: graph,
4243
extraDirs: extraDirs,
44+
extraFiles: extraFiles,
4345
options: options,
4446
fileSystem: fileSystem,
4547
diagnostics: diagnostics)
@@ -55,6 +57,7 @@ func xcodeProject(
5557
xcodeprojPath: AbsolutePath,
5658
graph: PackageGraph,
5759
extraDirs: [AbsolutePath],
60+
extraFiles: [AbsolutePath],
5861
options: XcodeprojOptions,
5962
fileSystem: FileSystem,
6063
diagnostics: DiagnosticsEngine,
@@ -322,6 +325,11 @@ func xcodeProject(
322325
project.mainGroup.addFileReference(path: extraDir.relative(to: sourceRootDir).asString, pathBase: .projectDir)
323326
}
324327

328+
for extraFile in extraFiles {
329+
let groupOfFile = srcPathsToGroups[extraFile.parentDirectory]
330+
groupOfFile?.addFileReference(path: extraFile.basename)
331+
}
332+
325333
// Determine the set of targets to generate in the project by excluding
326334
// any system targets.
327335
let targets = graph.reachableTargets.filter({ $0.type != .systemModule })

Tests/XcodeprojTests/GenerateXcodeprojTests.swift

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import TestSupport
1313
import PackageDescription
1414
import PackageGraph
1515
import PackageModel
16+
import SourceControl
1617
@testable import Xcodeproj
1718
import Utility
1819
import XCTest
@@ -76,7 +77,7 @@ class GenerateXcodeprojTests: XCTestCase {
7677
let options = XcodeprojOptions(xcconfigOverrides: AbsolutePath("/doesntexist"))
7778
do {
7879
_ = try xcodeProject(xcodeprojPath: AbsolutePath.root.appending(component: "xcodeproj"),
79-
graph: graph, extraDirs: [], options: options, fileSystem: fileSystem, diagnostics: diagnostics)
80+
graph: graph, extraDirs: [], extraFiles: [], options: options, fileSystem: fileSystem, diagnostics: diagnostics)
8081
XCTFail("Project generation should have failed")
8182
} catch ProjectGenerationError.xcconfigOverrideNotFound(let path) {
8283
XCTAssertEqual(options.xcconfigOverrides, path)
@@ -94,16 +95,144 @@ class GenerateXcodeprojTests: XCTestCase {
9495
XCTAssertFalse(diagnostics.hasErrors)
9596

9697
_ = try xcodeProject(xcodeprojPath: AbsolutePath.root.appending(component: "xcodeproj"),
97-
graph: graph, extraDirs: [], options: XcodeprojOptions(), fileSystem: fileSystem, diagnostics: diagnostics,
98+
graph: graph, extraDirs: [], extraFiles: [], options: XcodeprojOptions(), fileSystem: fileSystem, diagnostics: diagnostics,
9899
warningStream: warningStream)
99100

100101
let warnings = warningStream.bytes.asReadableString
101102
XCTAssertTrue(warnings.contains("warning: Target '\(moduleName)' conflicts with required framework filenames, rename this target to avoid conflicts."))
102103
}
103104

105+
func testGenerateXcodeprojWithoutGitRepo() {
106+
mktmpdir { dstdir in
107+
108+
let packagePath = dstdir.appending(component: "Foo")
109+
let modulePath = packagePath.appending(components: "Sources", "DummyModule")
110+
try makeDirectories(modulePath)
111+
try localFileSystem.writeFileContents(modulePath.appending(component: "dummy.swift"), bytes: "dummy_data")
112+
try localFileSystem.writeFileContents(packagePath.appending(component: "a.txt"), bytes: "dummy_data")
113+
114+
let diagnostics = DiagnosticsEngine()
115+
let graph = loadMockPackageGraph([packagePath.asString: Package(name: "Foo")], root: packagePath.asString, diagnostics: diagnostics, in: localFileSystem)
116+
XCTAssertFalse(diagnostics.hasErrors)
117+
118+
let projectName = "DummyProjectName"
119+
let outpath = Xcodeproj.buildXcodeprojPath(outputDir: dstdir, projectName: projectName)
120+
let project = try Xcodeproj.generate(projectName: projectName, xcodeprojPath: outpath, graph: graph, options: XcodeprojOptions(), diagnostics: diagnostics)
121+
122+
XCTAssertFalse(project.mainGroup.subitems.contains { $0.path == "a.txt" })
123+
}
124+
}
125+
126+
func testGenerateXcodeprojWithDotFiles() {
127+
mktmpdir { dstdir in
128+
129+
let packagePath = dstdir.appending(component: "Foo")
130+
let modulePath = packagePath.appending(components: "Sources", "DummyModule")
131+
try makeDirectories(modulePath)
132+
try localFileSystem.writeFileContents(modulePath.appending(component: "dummy.swift"), bytes: "dummy_data")
133+
try localFileSystem.writeFileContents(packagePath.appending(component: ".a.txt"), bytes: "dummy_data")
134+
135+
initGitRepo(packagePath, addFile: false)
136+
137+
let diagnostics = DiagnosticsEngine()
138+
let graph = loadMockPackageGraph([packagePath.asString: Package(name: "Foo")], root: packagePath.asString, diagnostics: diagnostics, in: localFileSystem)
139+
XCTAssertFalse(diagnostics.hasErrors)
140+
141+
let projectName = "DummyProjectName"
142+
let outpath = Xcodeproj.buildXcodeprojPath(outputDir: dstdir, projectName: projectName)
143+
let project = try Xcodeproj.generate(projectName: projectName, xcodeprojPath: outpath, graph: graph, options: XcodeprojOptions(), diagnostics: diagnostics)
144+
145+
XCTAssertFalse(project.mainGroup.subitems.contains { $0.path == ".a.txt" })
146+
}
147+
}
148+
149+
func testGenerateXcodeprojWithRootFiles() {
150+
mktmpdir { dstdir in
151+
152+
let packagePath = dstdir.appending(component: "Foo")
153+
let modulePath = packagePath.appending(components: "Sources", "DummyModule")
154+
try makeDirectories(modulePath)
155+
try localFileSystem.writeFileContents(modulePath.appending(component: "dummy.swift"), bytes: "dummy_data")
156+
try localFileSystem.writeFileContents(packagePath.appending(component: "a.txt"), bytes: "dummy_data")
157+
158+
initGitRepo(packagePath, addFile: false)
159+
160+
let diagnostics = DiagnosticsEngine()
161+
let graph = loadMockPackageGraph([packagePath.asString: Package(name: "Foo")], root: packagePath.asString, diagnostics: diagnostics, in: localFileSystem)
162+
XCTAssertFalse(diagnostics.hasErrors)
163+
164+
let projectName = "DummyProjectName"
165+
let outpath = Xcodeproj.buildXcodeprojPath(outputDir: dstdir, projectName: projectName)
166+
let project = try Xcodeproj.generate(projectName: projectName, xcodeprojPath: outpath, graph: graph, options: XcodeprojOptions(), diagnostics: diagnostics)
167+
168+
XCTAssertTrue(project.mainGroup.subitems.contains { $0.path == "a.txt" })
169+
}
170+
}
171+
172+
func testGenerateXcodeprojWithNonSourceFilesInSourceDirectories() {
173+
mktmpdir { dstdir in
174+
175+
let packagePath = dstdir.appending(component: "Foo")
176+
let modulePath = packagePath.appending(components: "Sources", "DummyModule")
177+
try makeDirectories(modulePath)
178+
try localFileSystem.writeFileContents(modulePath.appending(component: "dummy.swift"), bytes: "dummy_data")
179+
try localFileSystem.writeFileContents(modulePath.appending(component: "a.txt"), bytes: "dummy_data")
180+
181+
initGitRepo(packagePath, addFile: false)
182+
183+
let diagnostics = DiagnosticsEngine()
184+
let graph = loadMockPackageGraph([packagePath.asString: Package(name: "Foo")], root: packagePath.asString, diagnostics: diagnostics, in: localFileSystem)
185+
XCTAssertFalse(diagnostics.hasErrors)
186+
187+
let projectName = "DummyProjectName"
188+
let outpath = Xcodeproj.buildXcodeprojPath(outputDir: dstdir, projectName: projectName)
189+
let project = try Xcodeproj.generate(projectName: projectName, xcodeprojPath: outpath, graph: graph, options: XcodeprojOptions(), diagnostics: diagnostics)
190+
191+
let sources = project.mainGroup.subitems[1] as? Xcode.Group
192+
let dummyModule = sources?.subitems[0] as? Xcode.Group
193+
let aTxt = dummyModule?.subitems[0]
194+
195+
XCTAssertEqual(aTxt?.path, "a.txt")
196+
}
197+
}
198+
199+
func testGenerateXcodeprojWithFilesIgnoredByGit() {
200+
mktmpdir { dstdir in
201+
202+
let packagePath = dstdir.appending(component: "Foo")
203+
let modulePath = packagePath.appending(components: "Sources", "DummyModule")
204+
205+
try makeDirectories(modulePath)
206+
try localFileSystem.writeFileContents(modulePath.appending(component: "dummy.swift"), bytes: "dummy_data")
207+
208+
initGitRepo(packagePath, addFile: false)
209+
// Add a .gitignore
210+
try localFileSystem.writeFileContents(packagePath.appending(component: ".gitignore"), bytes: "ignored_file")
211+
try localFileSystem.writeFileContents(modulePath.appending(component: "ignored_file"), bytes: "dummy_data")
212+
try localFileSystem.writeFileContents(packagePath.appending(component: "ignored_file"), bytes: "dummy_data")
213+
214+
let diagnostics = DiagnosticsEngine()
215+
let graph = loadMockPackageGraph([packagePath.asString: Package(name: "Foo")], root: packagePath.asString, diagnostics: diagnostics, in: localFileSystem)
216+
XCTAssertFalse(diagnostics.hasErrors)
217+
218+
let projectName = "DummyProjectName"
219+
let outpath = Xcodeproj.buildXcodeprojPath(outputDir: dstdir, projectName: projectName)
220+
let project = try Xcodeproj.generate(projectName: projectName, xcodeprojPath: outpath, graph: graph, options: XcodeprojOptions(), diagnostics: diagnostics)
221+
222+
let sources = project.mainGroup.subitems[1] as? Xcode.Group
223+
let dummyModule = sources?.subitems[0] as? Xcode.Group
224+
225+
XCTAssertEqual(dummyModule?.subitems.count, 1)
226+
XCTAssertFalse(project.mainGroup.subitems.contains { $0.path == "ignored_file" })
227+
}
228+
}
229+
104230
static var allTests = [
105231
("testXcodebuildCanParseIt", testXcodebuildCanParseIt),
106232
("testXcconfigOverrideValidatesPath", testXcconfigOverrideValidatesPath),
107233
("testGenerateXcodeprojWithInvalidModuleNames", testGenerateXcodeprojWithInvalidModuleNames),
234+
("testGenerateXcodeprojWithRootFiles", testGenerateXcodeprojWithRootFiles),
235+
("testGenerateXcodeprojWithNonSourceFilesInSourceDirectories", testGenerateXcodeprojWithNonSourceFilesInSourceDirectories),
236+
("testGenerateXcodeprojWithFilesIgnoredByGit", testGenerateXcodeprojWithFilesIgnoredByGit),
108237
]
109238
}

Tests/XcodeprojTests/PackageGraphTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class PackageGraphTests: XCTestCase {
4141

4242
let options = XcodeprojOptions(xcconfigOverrides: AbsolutePath("/Overrides.xcconfig"))
4343

44-
let project = try xcodeProject(xcodeprojPath: AbsolutePath.root.appending(component: "xcodeproj"), graph: g, extraDirs: [], options: options, fileSystem: fs, diagnostics: diagnostics)
44+
let project = try xcodeProject(xcodeprojPath: AbsolutePath.root.appending(component: "xcodeproj"), graph: g, extraDirs: [], extraFiles: [], options: options, fileSystem: fs, diagnostics: diagnostics)
4545

4646
XcodeProjectTester(project) { result in
4747
result.check(projectDir: "Bar")
@@ -167,7 +167,7 @@ class PackageGraphTests: XCTestCase {
167167
products: [.library(name: "Bar", type: .dynamic, targets: ["Foo"])],
168168
targets: [.target(name: "Foo")]),
169169
], root: "/Foo", diagnostics: diagnostics, in: fs)
170-
let project = try xcodeProject(xcodeprojPath: AbsolutePath("/Foo/build").appending(component: "xcodeproj"), graph: g, extraDirs: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
170+
let project = try xcodeProject(xcodeprojPath: AbsolutePath("/Foo/build").appending(component: "xcodeproj"), graph: g, extraDirs: [], extraFiles: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
171171
XcodeProjectTester(project) { result in
172172
result.check(target: "Foo") { targetResult in
173173
targetResult.check(productType: .framework)
@@ -193,7 +193,7 @@ class PackageGraphTests: XCTestCase {
193193
let g = loadMockPackageGraph([
194194
"/Bar": Package(name: "Bar", targets: [Target(name: "swift", dependencies: ["Sea", "Sea2"])]),
195195
], root: "/Bar", diagnostics: diagnostics, in: fs)
196-
let project = try xcodeProject(xcodeprojPath: AbsolutePath("/Bar/build").appending(component: "xcodeproj"), graph: g, extraDirs: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
196+
let project = try xcodeProject(xcodeprojPath: AbsolutePath("/Bar/build").appending(component: "xcodeproj"), graph: g, extraDirs: [], extraFiles: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
197197

198198
XcodeProjectTester(project) { result in
199199
result.check(target: "swift") { targetResult in
@@ -225,7 +225,7 @@ class PackageGraphTests: XCTestCase {
225225
"/Pkg": Package(name: "Pkg", targets: [Target(name: "LibraryTests", dependencies: ["Library", "HelperTool"])]),
226226
], root: "/Pkg", diagnostics: diagnostics, in: fs)
227227

228-
let project = try xcodeProject(xcodeprojPath: AbsolutePath.root.appending(component: "xcodeproj"), graph: g, extraDirs: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
228+
let project = try xcodeProject(xcodeprojPath: AbsolutePath.root.appending(component: "xcodeproj"), graph: g, extraDirs: [], extraFiles: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
229229

230230
XcodeProjectTester(project) { result in
231231
result.check(projectDir: "Pkg")
@@ -331,7 +331,7 @@ class PackageGraphTests: XCTestCase {
331331
),
332332
], root: "/Foo", diagnostics: diagnostics, in: fs)
333333

334-
let project = try xcodeProject(xcodeprojPath: AbsolutePath("/Foo").appending(component: "xcodeproj"), graph: graph, extraDirs: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
334+
let project = try xcodeProject(xcodeprojPath: AbsolutePath("/Foo").appending(component: "xcodeproj"), graph: graph, extraDirs: [], extraFiles: [], options: XcodeprojOptions(), fileSystem: fs, diagnostics: diagnostics)
335335
XCTAssertNoDiagnostics(diagnostics)
336336

337337
XcodeProjectTester(project) { result in

0 commit comments

Comments
 (0)