From f918bf54eaf07d1a4957e19827c5392ee97f453e Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Wed, 28 Aug 2019 14:22:35 -0500 Subject: [PATCH 1/4] Add support for explicit includes on sources. They are overridden by excludes. --- Docs/ProjectSpec.md | 1 + Sources/ProjectSpec/TargetSource.swift | 5 ++ Sources/XcodeGenKit/SourceGenerator.swift | 44 ++++++++---- .../SourceGeneratorTests.swift | 70 +++++++++++++++++++ 4 files changed, 105 insertions(+), 15 deletions(-) diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index 5abada6bf..3bbbdcc61 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -305,6 +305,7 @@ A source can be provided via a string (the path) or an object of the form: - [ ] **name**: **String** - Can be used to override the name of the source file or directory. By default the last component of the path is used for the name - [ ] **compilerFlags**: **[String]** or **String** - A list of compilerFlags to add to files under this specific path provided as a list or a space delimitted string. Defaults to empty. - [ ] **excludes**: **[String]** - A list of [global patterns](https://en.wikipedia.org/wiki/Glob_(programming)) representing the files to exclude. These rules are relative to `path` and _not the directory where `project.yml` resides_. +- [ ] **includes**: **[String]** - A list of [global patterns](https://en.wikipedia.org/wiki/Glob_(programming)) representing the files to include. These rules are relative to `path` and _not the directory where `project.yml` resides_. If **excludes** is present and file conflicts with **includes**, **excludes** will override the **includes** behavior. - [ ] **createIntermediateGroups**: **Bool** - This overrides the value in [Options](#options) - [ ] **optional**: **Bool** - Disable missing path check. Defaults to false. - [ ] **buildPhase**: **String** - This manually sets the build phase this file or files in this directory will be added to, otherwise XcodeGen will guess based on the file extension. Note that `Info.plist` files will never be added to any build phases, no matter what this setting is. Possible values are: diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index 6ad090617..83ac28ccf 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -11,6 +11,7 @@ public struct TargetSource: Equatable { public var name: String? public var compilerFlags: [String] public var excludes: [String] + public var includes: [String] public var type: SourceType? public var optional: Bool public var buildPhase: BuildPhase? @@ -125,6 +126,7 @@ public struct TargetSource: Equatable { name: String? = nil, compilerFlags: [String] = [], excludes: [String] = [], + includes: [String] = [], type: SourceType? = nil, optional: Bool = optionalDefault, buildPhase: BuildPhase? = nil, @@ -136,6 +138,7 @@ public struct TargetSource: Equatable { self.name = name self.compilerFlags = compilerFlags self.excludes = excludes + self.includes = includes self.type = type self.optional = optional self.buildPhase = buildPhase @@ -173,6 +176,7 @@ extension TargetSource: JSONObjectConvertible { headerVisibility = jsonDictionary.json(atKeyPath: "headerVisibility") excludes = jsonDictionary.json(atKeyPath: "excludes") ?? [] + includes = jsonDictionary.json(atKeyPath: "includes") ?? [] type = jsonDictionary.json(atKeyPath: "type") optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault @@ -192,6 +196,7 @@ extension TargetSource: JSONEncodable { var dict: [String: Any?] = [ "compilerFlags": compilerFlags, "excludes": excludes, + "includes": includes, "name": name, "headerVisibility": headerVisibility?.rawValue, "type": type?.rawValue, diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 1d58a5ae0..28cb48bdc 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -20,8 +20,7 @@ class SourceGenerator { private let project: Project let pbxProj: PBXProj - var targetSourceExcludePaths: Set = [] - var defaultExcludedFiles = [ + private var defaultExcludedFiles = [ ".DS_Store", ] private let defaultExcludedExtensions = [ @@ -298,11 +297,11 @@ class SourceGenerator { } /// Collects all the excluded paths within the targetSource - private func getSourceExcludes(targetSource: TargetSource) -> Set { + private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set { let rootSourcePath = project.basePath + targetSource.path return Set( - targetSource.excludes.map { + patterns.map { return expandPattern("\(rootSourcePath)/\($0)") .map { guard $0.isDirectory else { @@ -333,14 +332,15 @@ class SourceGenerator { } /// Checks whether the path is not in any default or TargetSource excludes - func isIncludedPath(_ path: Path) -> Bool { + func isIncludedPath(_ path: Path, targetSourceExcludePaths: Set, targetSourceIncludePaths: Set) -> Bool { return !defaultExcludedFiles.contains(where: { path.lastComponent.contains($0) }) && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) && !targetSourceExcludePaths.contains(path) + && (targetSourceIncludePaths.isEmpty || targetSourceIncludePaths.contains(path)) } /// Gets all the children paths that aren't excluded - private func getSourceChildren(targetSource: TargetSource, dirPath: Path) throws -> [Path] { + private func getSourceChildren(targetSource: TargetSource, dirPath: Path, targetSourceExcludePaths: Set, targetSourceIncludePaths: Set) throws -> [Path] { return try dirPath.children() .filter { if $0.isDirectory { @@ -350,9 +350,11 @@ class SourceGenerator { return project.options.generateEmptyDirectories } - return !children.filter(isIncludedPath).isEmpty + return !children + .filter { self.isIncludedPath($0, targetSourceExcludePaths: targetSourceExcludePaths, targetSourceIncludePaths: targetSourceIncludePaths) } + .isEmpty } else if $0.isFile { - return isIncludedPath($0) + return self.isIncludedPath($0, targetSourceExcludePaths: targetSourceExcludePaths, targetSourceIncludePaths: targetSourceIncludePaths) } else { return false } @@ -360,10 +362,10 @@ class SourceGenerator { } /// creates all the source files and groups they belong to for a given targetSource - private func getGroupSources(targetType: PBXProductType, targetSource: TargetSource, path: Path, isBaseGroup: Bool) + private func getGroupSources(targetType: PBXProductType, targetSource: TargetSource, path: Path, isBaseGroup: Bool, targetSourceExcludePaths: Set, targetSourceIncludePaths: Set) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) { - let children = try getSourceChildren(targetSource: targetSource, dirPath: path) + let children = try getSourceChildren(targetSource: targetSource, dirPath: path, targetSourceExcludePaths: targetSourceExcludePaths, targetSourceIncludePaths: targetSourceIncludePaths) let directories = children .filter { $0.isDirectory && $0.extension == nil && $0.extension != "lproj" } @@ -381,7 +383,12 @@ class SourceGenerator { var groups: [PBXGroup] = [] for path in directories { - let subGroups = try getGroupSources(targetType: targetType, targetSource: targetSource, path: path, isBaseGroup: false) + let subGroups = try getGroupSources(targetType: targetType, + targetSource: targetSource, + path: path, + isBaseGroup: false, + targetSourceExcludePaths: targetSourceExcludePaths, + targetSourceIncludePaths: targetSourceIncludePaths) guard !subGroups.sourceFiles.isEmpty || project.options.generateEmptyDirectories else { continue @@ -413,7 +420,7 @@ class SourceGenerator { if let baseLocalisedDirectory = baseLocalisedDirectory { for filePath in try baseLocalisedDirectory.children() - .filter(isIncludedPath) + .filter({ self.isIncludedPath($0, targetSourceExcludePaths: targetSourceExcludePaths, targetSourceIncludePaths: targetSourceIncludePaths) }) .sorted() { let variantGroup = getVariantGroup(path: filePath, inPath: path) groupChildren.append(variantGroup) @@ -433,7 +440,7 @@ class SourceGenerator { for localisedDirectory in localisedDirectories { let localisationName = localisedDirectory.lastComponentWithoutExtension for filePath in try localisedDirectory.children() - .filter(isIncludedPath) + .filter({ self.isIncludedPath($0, targetSourceExcludePaths: targetSourceExcludePaths, targetSourceIncludePaths: targetSourceIncludePaths) }) .sorted { $0.lastComponent < $1.lastComponent } { // find base localisation variant group // ex: Foo.strings will be added to Foo.strings or Foo.storyboard variant group @@ -489,7 +496,9 @@ class SourceGenerator { private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, path: Path) throws -> [SourceFile] { // generate excluded paths - targetSourceExcludePaths = getSourceExcludes(targetSource: targetSource) + let targetSourceExcludePaths = getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes) + // generate included paths. Excluded paths will override this. + let targetSourceIncludePaths = getSourceMatches(targetSource: targetSource, patterns: targetSource.includes) let type = targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group) let createIntermediateGroups = targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups @@ -545,7 +554,12 @@ class SourceGenerator { // This group is missing, so if's optional just return an empty array return [] } - let (groupSourceFiles, groups) = try getGroupSources(targetType: targetType, targetSource: targetSource, path: path, isBaseGroup: true) + let (groupSourceFiles, groups) = try getGroupSources(targetType: targetType, + targetSource: targetSource, + path: path, + isBaseGroup: true, + targetSourceExcludePaths: targetSourceExcludePaths, + targetSourceIncludePaths: targetSourceIncludePaths) let group = groups.first! if let name = targetSource.name { group.name = name diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index 5faf83056..e29d8f5a4 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -697,6 +697,76 @@ class SourceGeneratorTests: XCTestCase { throw failure("File does not contain no_codegen attribute") } } + + $0.it("includes only the specified files when includes is present") { + let directories = """ + Sources: + - file3.swift + - file3Tests.swift + - file2.swift + - file2Tests.swift + - group2: + - file.swift + - fileTests.swift + - group: + - file.swift + """ + try createDirectories(directories) + + let includes = [ + "**/*Tests.*" + ] + + let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", includes: includes)]) + + let project = Project(basePath: directoryPath, name: "Test", targets: [target]) + let pbxProj = try project.generatePbxProj() + + try pbxProj.expectFile(paths: ["Sources", "file2Tests.swift"]) + try pbxProj.expectFile(paths: ["Sources", "file3Tests.swift"]) + try pbxProj.expectFile(paths: ["Sources", "group2", "fileTests.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "group", "file.swift"]) + } + + $0.it("prioritizes excludes over includes when both are present") { + let directories = """ + Sources: + - file3.swift + - file3Tests.swift + - file2.swift + - file2Tests.swift + - group2: + - file.swift + - fileTests.swift + - group: + - file.swift + """ + try createDirectories(directories) + + let includes = [ + "**/*Tests.*" + ] + + let excludes = [ + "group2" + ] + + let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", excludes: excludes, includes: includes)]) + + let project = Project(basePath: directoryPath, name: "Test", targets: [target]) + let pbxProj = try project.generatePbxProj() + + try pbxProj.expectFile(paths: ["Sources", "file2Tests.swift"]) + try pbxProj.expectFile(paths: ["Sources", "file3Tests.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "group2", "fileTests.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"]) + try pbxProj.expectFileMissing(paths: ["Sources", "group", "file.swift"]) + } } } } From fbac8af3276ac480555ca1ce1ec946b5a7cb0006 Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Wed, 28 Aug 2019 15:48:47 -0500 Subject: [PATCH 2/4] Include path even if it's only a relative of the included file. --- Sources/XcodeGenKit/SourceGenerator.swift | 7 ++++++- .../SourceGeneratorTests.swift | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 28cb48bdc..c666ea7c9 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -336,7 +336,12 @@ class SourceGenerator { return !defaultExcludedFiles.contains(where: { path.lastComponent.contains($0) }) && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) && !targetSourceExcludePaths.contains(path) - && (targetSourceIncludePaths.isEmpty || targetSourceIncludePaths.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. + && (targetSourceIncludePaths.isEmpty || targetSourceIncludePaths.contains(where: { + if path == $0 { return true } + guard let relativePath = try? $0.relativePath(from: path) else { return false } + return !relativePath.description.contains("..") + })) } /// Gets all the children paths that aren't excluded diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index e29d8f5a4..b3c97f580 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -634,7 +634,7 @@ class SourceGeneratorTests: XCTestCase { let project = Project(basePath: directoryPath, name: "Test", targets: [target]) _ = try project.generatePbxProj() } - + $0.it("relative path items outside base path are grouped together") { let directories = """ Sources: @@ -644,21 +644,21 @@ class SourceGeneratorTests: XCTestCase { - b.swift """ try createDirectories(directories) - + let outOfSourceFile1 = outOfRootPath + "Outside/a.swift" try outOfSourceFile1.parent().mkpath() try outOfSourceFile1.write("") - + let outOfSourceFile2 = outOfRootPath + "Outside/Outside2/b.swift" try outOfSourceFile2.parent().mkpath() try outOfSourceFile2.write("") - + let target = Target(name: "Test", type: .application, platform: .iOS, sources: [ "Sources", "../OtherDirectory", ]) let project = Project(basePath: directoryPath, name: "Test", targets: [target]) - + let pbxProj = try project.generatePbxProj() try pbxProj.expectFile(paths: ["Sources", "Inside", "a.swift"], buildPhase: .sources) try pbxProj.expectFile(paths: ["Sources", "Inside", "Inside2", "b.swift"], buildPhase: .sources) @@ -710,6 +710,11 @@ class SourceGeneratorTests: XCTestCase { - fileTests.swift - group: - file.swift + - group3: + - group4: + - group5: + - file.swift + - file5Tests.swift """ try createDirectories(directories) @@ -719,12 +724,14 @@ class SourceGeneratorTests: XCTestCase { let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", includes: includes)]) - let project = Project(basePath: directoryPath, name: "Test", targets: [target]) + let options = SpecOptions(createIntermediateGroups: true, generateEmptyDirectories: true) + let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: options) let pbxProj = try project.generatePbxProj() try pbxProj.expectFile(paths: ["Sources", "file2Tests.swift"]) try pbxProj.expectFile(paths: ["Sources", "file3Tests.swift"]) try pbxProj.expectFile(paths: ["Sources", "group2", "fileTests.swift"]) + try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file5Tests.swift"]) try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"]) try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"]) try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"]) From 407ca123939f991d2c903a50b59f08edb5932d57 Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Wed, 28 Aug 2019 15:54:03 -0500 Subject: [PATCH 3/4] Perform the relative location check much faster. --- Sources/XcodeGenKit/SourceGenerator.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index c666ea7c9..5dbca53b8 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -337,10 +337,9 @@ class SourceGenerator { && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) && !targetSourceExcludePaths.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. - && (targetSourceIncludePaths.isEmpty || targetSourceIncludePaths.contains(where: { - if path == $0 { return true } - guard let relativePath = try? $0.relativePath(from: path) else { return false } - return !relativePath.description.contains("..") + && (targetSourceIncludePaths.isEmpty || targetSourceIncludePaths.contains(where: { includedFile in + if path == includedFile { return true } + return includedFile.description.contains(path.description) })) } From e4204e51eed308628817bdfb6350a44dc53c1bbd Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Sun, 1 Sep 2019 15:33:15 -0500 Subject: [PATCH 4/4] Tweak an includes test. --- Tests/XcodeGenKitTests/SourceGeneratorTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index e7f16db7d..8e94cc20d 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -715,6 +715,8 @@ class SourceGeneratorTests: XCTestCase { - group5: - file.swift - file5Tests.swift + - file6Tests.m + - file6Tests.h """ try createDirectories(directories) @@ -732,6 +734,8 @@ class SourceGeneratorTests: XCTestCase { try pbxProj.expectFile(paths: ["Sources", "file3Tests.swift"]) try pbxProj.expectFile(paths: ["Sources", "group2", "fileTests.swift"]) try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file5Tests.swift"]) + try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file6Tests.h"]) + try pbxProj.expectFile(paths: ["Sources", "group3", "group4", "group5", "file6Tests.m"]) try pbxProj.expectFileMissing(paths: ["Sources", "file2.swift"]) try pbxProj.expectFileMissing(paths: ["Sources", "file3.swift"]) try pbxProj.expectFileMissing(paths: ["Sources", "group2", "file.swift"])