Skip to content

Commit

Permalink
Merge pull request #1 from bclymer/BC-IncludesOnly
Browse files Browse the repository at this point in the history
Add support for explicit includes on sources.
  • Loading branch information
bclymer authored Sep 1, 2019
2 parents 367c6bd + e4204e5 commit 26e1540
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 15 deletions.
1 change: 1 addition & 0 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_. XcodeGen uses Bash 4's Glob behaviors where globstar (**) is enabled.
- [ ] **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:
Expand Down
5 changes: 5 additions & 0 deletions Sources/ProjectSpec/TargetSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand Down
48 changes: 33 additions & 15 deletions Sources/XcodeGenKit/SourceGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ class SourceGenerator {
private let project: Project
let pbxProj: PBXProj

var targetSourceExcludePaths: Set<Path> = []
var defaultExcludedFiles = [
private var defaultExcludedFiles = [
".DS_Store",
]
private let defaultExcludedExtensions = [
Expand Down Expand Up @@ -298,11 +297,11 @@ class SourceGenerator {
}

/// Collects all the excluded paths within the targetSource
private func getSourceExcludes(targetSource: TargetSource) -> Set<Path> {
private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set<Path> {
let rootSourcePath = project.basePath + targetSource.path

return Set(
targetSource.excludes.map { pattern in
patterns.map { pattern in
guard !pattern.isEmpty else { return [] }
return Glob(pattern: "\(rootSourcePath)/\(pattern)")
.map { Path($0) }
Expand All @@ -320,14 +319,19 @@ 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<Path>, targetSourceIncludePaths: Set<Path>) -> Bool {
return !defaultExcludedFiles.contains(where: { path.lastComponent.contains($0) })
&& !(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: { includedFile in
if path == includedFile { return true }
return includedFile.description.contains(path.description)
}))
}

/// 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<Path>, targetSourceIncludePaths: Set<Path>) throws -> [Path] {
return try dirPath.children()
.filter {
if $0.isDirectory {
Expand All @@ -337,20 +341,22 @@ 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
}
}
}

/// 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<Path>, targetSourceIncludePaths: Set<Path>)
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" }
Expand All @@ -368,7 +374,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
Expand Down Expand Up @@ -400,7 +411,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)
Expand All @@ -420,7 +431,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
Expand Down Expand Up @@ -476,7 +487,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
Expand Down Expand Up @@ -532,7 +545,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
Expand Down
81 changes: 81 additions & 0 deletions Tests/XcodeGenKitTests/SourceGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,87 @@ 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
- group3:
- group4:
- group5:
- file.swift
- file5Tests.swift
- file6Tests.m
- file6Tests.h
"""
try createDirectories(directories)

let includes = [
"**/*Tests.*"
]

let target = Target(name: "Test", type: .application, platform: .iOS, sources: [TargetSource(path: "Sources", includes: includes)])

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.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"])
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"])
}
}
}
}
Expand Down

0 comments on commit 26e1540

Please sign in to comment.