Merge pull request #489 from ellneal/feature/current-directory-expander.yml

Spec file relative directory expansion
This commit is contained in:
Yonas Kolb
2019-01-28 11:53:47 +11:00
committed by GitHub
21 changed files with 543 additions and 62 deletions
+3
View File
@@ -7,6 +7,9 @@
- Added support for the `instrumentsPackage` product type [#482](https://github.com/yonaskolb/XcodeGen/pull/482) @ksulliva
- Added support for `inputFileLists` and `outputFileLists` within project build scripts [#500](https://github.com/yonaskolb/XcodeGen/pull/500) @lukewakeford
#### Changed
- **BREAKING** All the paths within included files are now relative to that file and not the root spec. This can be disabled with a `relativePaths: false` on the include. See [docs](https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md#include) for more details [#489](https://github.com/yonaskolb/XcodeGen/pull/489) @ellneal
#### Fixed
- Fixed `--project` argument not taking effect [#487](https://github.com/yonaskolb/XcodeGen/pull/487) @monowerker
- Fixed Sticker Packs from generating an empty Source file phase which caused in error in the new build system [#492](https://github.com/yonaskolb/XcodeGen/pull/492) @rpassis
+16 -1
View File
@@ -45,7 +45,22 @@ Required properties are marked with checkbox. Some of the YAML examples don't sh
One or more specs can be included in the project spec. This can be used to split your project spec into multiple files, for easier structuring or sharing between multiple specs. Included specs can also include other specs and so on.
Include can either be a list of string paths or a single string path. They will be merged in order and then the current spec will be merged on top.
Include can either be a list of includes or a single include. They will be merged in order and then the current spec will be merged on top.
An include can be provided via a string (the path) or an object of the form:
**Include Object**
- [x] **path**: **String** - The path to the included file.
- [ ] **relativePaths**: **Bool** - Dictates whether the included spec specifies paths relative to itself (the default) or the root spec file.
```yaml
include:
- includedFile.yml
- path: path/to/includedFile.yml
relativePaths: false
```
By default specs are merged additively. That is for every value:
- if existing value and new value are both dictionaries merge them and continue down the hierachy
+12
View File
@@ -61,3 +61,15 @@ extension AggregateTarget: NamedJSONDictionaryConvertible {
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:]
}
}
extension AggregateTarget: PathContainer {
static var pathProperties: [PathProperty] {
return [
.dictionary([
.string("configFiles"),
.object("buildScripts", BuildScript.pathProperties),
]),
]
}
}
+9
View File
@@ -61,3 +61,12 @@ extension BuildScript: JSONObjectConvertible {
showEnvVars = jsonDictionary.json(atKeyPath: "showEnvVars") ?? true
}
}
extension BuildScript: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("path"),
]
}
}
+9
View File
@@ -72,3 +72,12 @@ extension Dependency: JSONObjectConvertible {
}
}
}
extension Dependency: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("framework"),
]
}
}
+50
View File
@@ -0,0 +1,50 @@
import Foundation
import JSONUtilities
import PathKit
protocol PathContainer {
static var pathProperties: [PathProperty] { get }
}
enum PathProperty {
case string(String)
case dictionary([PathProperty])
case object(String, [PathProperty])
}
extension Array where Element == PathProperty {
func resolvingPaths(in jsonDictionary: JSONDictionary, relativeTo path: Path) -> JSONDictionary {
var result = jsonDictionary
for pathProperty in self {
switch pathProperty {
case .string(let key):
if let source = result[key] as? String {
result[key] = (path + source).string
} else if let source = result[key] as? [String] {
result[key] = source.map { (path + $0).string }
} else if let source = result[key] as? [String: String] {
result[key] = source.mapValues { (path + $0).string }
}
case .dictionary(let pathProperties):
for (key, dictionary) in result {
if let source = dictionary as? JSONDictionary {
result[key] = pathProperties.resolvingPaths(in: source, relativeTo: path)
}
}
case .object(let key, let pathProperties):
if let source = result[key] as? JSONDictionary {
result[key] = pathProperties.resolvingPaths(in: source, relativeTo: path)
} else if let source = result[key] as? [JSONDictionary] {
result[key] = source.map { pathProperties.resolvingPaths(in: $0, relativeTo: path) }
} else if let source = result[key] as? [String: JSONDictionary] {
result[key] = source.mapValues { pathProperties.resolvingPaths(in: $0, relativeTo: path) }
}
}
}
return result
}
}
+9
View File
@@ -24,3 +24,12 @@ extension Plist: JSONObjectConvertible {
properties = jsonDictionary.json(atKeyPath: "properties") ?? [:]
}
}
extension Plist: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("path"),
]
}
}
+21 -1
View File
@@ -133,8 +133,16 @@ extension Project: Equatable {
extension Project {
public init(basePath: Path, jsonDictionary: JSONDictionary) throws {
let spec = Spec(relativePath: Path(), jsonDictionary: jsonDictionary)
try self.init(spec: spec, basePath: basePath)
}
public init(spec: Spec, basePath: Path) throws {
self.basePath = basePath
let jsonDictionary = try Project.resolveProject(jsonDictionary: jsonDictionary)
let spec = spec.resolvingPaths()
let jsonDictionary = try Project.resolveProject(jsonDictionary: spec.resolvedDictionary())
name = try jsonDictionary.json(atKeyPath: "name")
settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty
settingGroups = jsonDictionary.json(atKeyPath: "settingGroups")
@@ -166,6 +174,18 @@ extension Project {
}
}
extension Project: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("configFiles"),
.object("options", SpecOptions.pathProperties),
.object("targets", Target.pathProperties),
.object("aggregateTargets", AggregateTarget.pathProperties),
]
}
}
extension Project {
public var allFiles: [Path] {
-18
View File
@@ -101,24 +101,6 @@ extension Dictionary where Key == String, Value: Any {
}
}
func merge(dictionary: JSONDictionary, onto base: JSONDictionary) -> JSONDictionary {
var merged = base
for (key, value) in dictionary {
if key.hasSuffix(":REPLACE") {
let newKey = key.replacingOccurrences(of: ":REPLACE", with: "")
merged[newKey] = value
} else if let dictionary = value as? JSONDictionary, let base = merged[key] as? JSONDictionary {
merged[key] = merge(dictionary: dictionary, onto: base)
} else if let array = value as? [Any], let base = merged[key] as? [Any] {
merged[key] = base + array
} else {
merged[key] = value
}
}
return merged
}
public func += (lhs: inout BuildSettings, rhs: BuildSettings?) {
guard let rhs = rhs else { return }
lhs.merge(rhs)
+111
View File
@@ -0,0 +1,111 @@
import Foundation
import JSONUtilities
import PathKit
public struct Spec {
public let relativePath: Path
public let jsonDictionary: JSONDictionary
public let subSpecs: [Spec]
public init(relativePath: Path, jsonDictionary: JSONDictionary, subSpecs: [Spec] = []) {
self.relativePath = relativePath
self.jsonDictionary = jsonDictionary
self.subSpecs = subSpecs
}
public init(filename: String, basePath: Path, relativePath: Path = Path()) throws {
let path = basePath + relativePath + filename
// Depending on the extension we will either load the file as YAML or JSON
var json: [String: Any]
if path.extension?.lowercased() == "json" {
let data: Data = try path.read()
let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
guard let jsonDictionary = jsonData as? [String: Any] else {
fatalError("Invalid JSON at path \(path)")
}
json = jsonDictionary
} else {
json = try loadYamlDictionary(path: path)
}
let processIncludeOption = { (option: Any) -> (String, Bool)? in
if let option = option as? String {
return (option, true)
} else if let option = option as? JSONDictionary, let path = option["path"] as? String {
return (path, (option["relativePaths"] as? Bool) ?? true)
}
return nil
}
let includeSources: [(String, Bool)]
if let sources = json["include"] as? [Any] {
includeSources = sources.compactMap { processIncludeOption($0) }
} else if let source = json["include"] {
includeSources = [processIncludeOption(source)].compactMap { $0 }
} else {
includeSources = []
}
let includes = try includeSources.map { include -> Spec in
let path = Path(include.0)
let basePath = include.1 ? basePath + relativePath : basePath + relativePath + path.parent()
let relativePath = include.1 ? path.parent() : Path()
return try Spec(filename: path.lastComponent, basePath: basePath, relativePath: relativePath)
}
self.relativePath = relativePath
self.jsonDictionary = json
self.subSpecs = includes
}
public func resolvedDictionary() -> JSONDictionary {
return jsonDictionary.merged(onto:
subSpecs
.map { $0.resolvedDictionary() }
.reduce([:]) { $1.merged(onto: $0) }
)
}
}
extension Spec {
func resolvingPaths(relativeTo basePath: Path = Path()) -> Spec {
let relativePath = (basePath + self.relativePath).normalize()
guard relativePath != Path() else {
return self
}
let jsonDictionary = Project.pathProperties.resolvingPaths(in: self.jsonDictionary, relativeTo: relativePath)
return Spec(
relativePath: self.relativePath,
jsonDictionary: jsonDictionary,
subSpecs: self.subSpecs.map { template in
return template.resolvingPaths(relativeTo: relativePath)
}
)
}
}
extension Dictionary where Key == String, Value: Any {
func merged(onto other: Dictionary<Key, Value>) -> Dictionary<Key, Value> {
var merged = other
for (key, value) in self {
if key.hasSuffix(":REPLACE") {
let newKey = key[key.startIndex ..< key.index(key.endIndex, offsetBy: -8)]
merged[Key(newKey)] = value
} else if let dictionary = value as? Dictionary<Key, Value>, let base = merged[key] as? Dictionary<Key, Value> {
merged[key] = dictionary.merged(onto: base) as? Value
} else if let array = value as? [Any], let base = merged[key] as? [Any] {
merged[key] = (base + array) as? Value
} else {
merged[key] = value
}
}
return merged
}
}
+4 -36
View File
@@ -1,48 +1,16 @@
import Foundation
import JSONUtilities
import PathKit
import Yams
extension Project {
public init(path: Path) throws {
let dictionary = try Project.loadDictionary(path: path)
try self.init(basePath: path.parent(), jsonDictionary: dictionary)
let basePath = path.parent()
let template = try Spec(filename: path.lastComponent, basePath: basePath)
try self.init(spec: template, basePath: basePath)
}
public static func loadDictionary(path: Path) throws -> JSONDictionary {
// Depending on the extension we will either load the file as YAML or JSON
var json: [String: Any]
if path.extension?.lowercased() == "json" {
let data: Data = try path.read()
let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
guard let jsonDictionary = jsonData as? [String: Any] else {
fatalError("Invalid JSON at path \(path)")
}
json = jsonDictionary
} else {
json = try loadYamlDictionary(path: path)
}
var includes: [String]
if let includeString = json["include"] as? String {
includes = [includeString]
} else if let includeArray = json["include"] as? [String] {
includes = includeArray
} else {
includes = []
}
if !includes.isEmpty {
var includeDictionary: JSONDictionary = [:]
for include in includes {
let includePath = path.parent() + include
let dictionary = try loadDictionary(path: includePath)
includeDictionary = merge(dictionary: dictionary, onto: includeDictionary)
}
json = merge(dictionary: json, onto: includeDictionary)
}
return json
return try Spec(filename: path.lastComponent, basePath: path.parent()).jsonDictionary
}
}
+11
View File
@@ -120,3 +120,14 @@ extension SpecOptions: JSONObjectConvertible {
generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? false
}
}
extension SpecOptions: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("carthageBuildPath"),
.string("carthageExecutablePath"),
.string("defaultConfig"),
]
}
}
+22 -2
View File
@@ -110,6 +110,26 @@ extension Target: CustomStringConvertible {
}
}
extension Target: PathContainer {
static var pathProperties: [PathProperty] {
return [
.dictionary([
.string("sources"),
.object("sources", TargetSource.pathProperties),
.string("configFiles"),
.object("dependencies", Dependency.pathProperties),
.object("info", Plist.pathProperties),
.object("entitlements", Plist.pathProperties),
.object("preBuildScripts", BuildScript.pathProperties),
.object("prebuildScripts", BuildScript.pathProperties),
.object("postCompileScripts", BuildScript.pathProperties),
.object("postBuildScripts", BuildScript.pathProperties),
]),
]
}
}
extension Target {
static func resolveTargetTemplates(jsonDictionary: JSONDictionary) throws -> JSONDictionary {
@@ -125,10 +145,10 @@ extension Target {
var mergedDictionary: JSONDictionary = [:]
for template in templates {
if let templateDictionary = targetTemplatesDictionary[template] {
mergedDictionary = merge(dictionary: templateDictionary, onto: mergedDictionary)
mergedDictionary = templateDictionary.merged(onto: mergedDictionary)
}
}
target = merge(dictionary: target, onto: mergedDictionary)
target = target.merged(onto: mergedDictionary)
}
targetsDictionary[targetName] = target
}
+10
View File
@@ -207,3 +207,13 @@ extension TargetSource.BuildPhase.CopyFilesSettings: JSONObjectConvertible {
phaseOrder = .postCompile
}
}
extension TargetSource: PathContainer {
static var pathProperties: [PathProperty] {
return [
.string("path"),
.string("excludes"),
]
}
}
+3 -3
View File
@@ -16,11 +16,11 @@ public class SpecLoader {
}
public func loadProject(path: Path) throws -> Project {
let dictionary = try Project.loadDictionary(path: path)
let project = try Project(basePath: path.parent(), jsonDictionary: dictionary)
let template = try Spec(filename: path.lastComponent, basePath: path.parent())
let project = try Project(spec: template, basePath: path.parent())
self.project = project
projectDictionary = dictionary
projectDictionary = template.jsonDictionary
return project
}
+4
View File
@@ -0,0 +1,4 @@
include:
path: legacy_paths_test/legacy_included_paths_test.yml
relativePaths: false
name: NewName
@@ -0,0 +1,33 @@
configFiles:
IncludedConfig: config
options:
carthageBuildPath: carthage_build
carthageExecutablePath: carthage_executable
targets:
IncludedTarget:
type: application
platform: tvOS
configFiles:
Config: config
sources:
- source
dependencies:
- framework: Framework
info:
path: info
entitlements:
path: entitlements
preBuildScripts:
- path: preBuildScript
postCompileScripts:
- path: postCompileScript
postBuildScripts:
- path: postBuildScript
aggregateTargets:
IncludedAggregateTarget:
targets:
- IncludedTarget
configFiles:
Config: config
buildScripts:
- path: buildScript
+32
View File
@@ -0,0 +1,32 @@
include: paths_test/included_paths_test.yml
name: NewName
configFiles:
NewConfig: config
targets:
NewTarget:
type: application
platform: iOS
configFiles:
Config: config
sources:
- source
dependencies:
- framework: Framework
info:
path: info
entitlements:
path: entitlements
preBuildScripts:
- path: preBuildScript
postCompileScripts:
- path: postCompileScript
postBuildScripts:
- path: postBuildScript
aggregateTargets:
NewAggregateTarget:
targets:
- NewTarget
configFiles:
Config: config
buildScripts:
- path: buildScript
@@ -0,0 +1,31 @@
include: recursive_test/recursive_test.yml
configFiles:
IncludedConfig: config
targets:
IncludedTarget:
type: application
platform: tvOS
configFiles:
Config: config
sources:
- source
dependencies:
- framework: Framework
info:
path: info
entitlements:
path: entitlements
preBuildScripts:
- path: preBuildScript
postCompileScripts:
- path: postCompileScript
postBuildScripts:
- path: postBuildScript
aggregateTargets:
IncludedAggregateTarget:
targets:
- IncludedTarget
configFiles:
Config: config
buildScripts:
- path: buildScript
@@ -0,0 +1,33 @@
options:
carthageBuildPath: carthage_build
carthageExecutablePath: carthage_executable
configFiles:
RecursiveConfig: config
targets:
RecursiveTarget:
type: application
platform: macOS
configFiles:
Config: config
sources:
- source
dependencies:
- framework: Framework
info:
path: info
entitlements:
path: entitlements
prebuildScripts:
- path: prebuildScript
postCompileScripts:
- path: postCompileScript
postBuildScripts:
- path: postBuildScript
aggregateTargets:
RecursiveAggregateTarget:
targets:
- RecursiveTarget
configFiles:
Config: config
buildScripts:
- path: buildScript
+120 -1
View File
@@ -26,6 +26,124 @@ class SpecLoadingTests: XCTestCase {
]
}
$0.it("expands directories") {
let path = fixturePath + "paths_test.yml"
let project = try Project(path: path)
try expect(project.configFiles) == [
"IncludedConfig": "paths_test/config",
"NewConfig": "config",
"RecursiveConfig": "paths_test/recursive_test/config",
]
try expect(project.options) == SpecOptions(
carthageBuildPath: "paths_test/recursive_test/carthage_build",
carthageExecutablePath: "paths_test/recursive_test/carthage_executable"
)
try expect(project.aggregateTargets) == [
AggregateTarget(
name: "IncludedAggregateTarget",
targets: ["IncludedTarget"],
configFiles: ["Config": "paths_test/config"],
buildScripts: [BuildScript(script: .path("paths_test/buildScript"))]
),
AggregateTarget(
name: "NewAggregateTarget",
targets: ["NewTarget"],
configFiles: ["Config": "config"],
buildScripts: [BuildScript(script: .path("buildScript"))]
),
AggregateTarget(
name: "RecursiveAggregateTarget",
targets: ["RecursiveTarget"],
configFiles: ["Config": "paths_test/recursive_test/config"],
buildScripts: [BuildScript(script: .path("paths_test/recursive_test/buildScript"))]
),
]
try expect(project.targets) == [
Target(
name: "IncludedTarget",
type: .application,
platform: .tvOS,
configFiles: ["Config": "paths_test/config"],
sources: ["paths_test/source"],
dependencies: [Dependency(type: .framework, reference: "paths_test/Framework")],
info: Plist(path: "paths_test/info"),
entitlements: Plist(path: "paths_test/entitlements"),
preBuildScripts: [BuildScript(script: .path("paths_test/preBuildScript"))],
postCompileScripts: [BuildScript(script: .path("paths_test/postCompileScript"))],
postBuildScripts: [BuildScript(script: .path("paths_test/postBuildScript"))]
),
Target(
name: "NewTarget",
type: .application,
platform: .iOS,
configFiles: ["Config": "config"],
sources: ["source"],
dependencies: [Dependency(type: .framework, reference: "Framework")],
info: Plist(path: "info"),
entitlements: Plist(path: "entitlements"),
preBuildScripts: [BuildScript(script: .path("preBuildScript"))],
postCompileScripts: [BuildScript(script: .path("postCompileScript"))],
postBuildScripts: [BuildScript(script: .path("postBuildScript"))]
),
Target(
name: "RecursiveTarget",
type: .application,
platform: .macOS,
configFiles: ["Config": "paths_test/recursive_test/config"],
sources: ["paths_test/recursive_test/source"],
dependencies: [Dependency(type: .framework, reference: "paths_test/recursive_test/Framework")],
info: Plist(path: "paths_test/recursive_test/info"),
entitlements: Plist(path: "paths_test/recursive_test/entitlements"),
preBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/prebuildScript"))],
postCompileScripts: [BuildScript(script: .path("paths_test/recursive_test/postCompileScript"))],
postBuildScripts: [BuildScript(script: .path("paths_test/recursive_test/postBuildScript"))]
),
]
}
$0.it("respects directory expansion preference") {
let path = fixturePath + "legacy_paths_test.yml"
let project = try Project(path: path)
try expect(project.configFiles) == [
"IncludedConfig": "config",
]
try expect(project.options) == SpecOptions(
carthageBuildPath: "carthage_build",
carthageExecutablePath: "carthage_executable"
)
try expect(project.aggregateTargets) == [
AggregateTarget(
name: "IncludedAggregateTarget",
targets: ["IncludedTarget"],
configFiles: ["Config": "config"],
buildScripts: [BuildScript(script: .path("buildScript"))]
),
]
try expect(project.targets) == [
Target(
name: "IncludedTarget",
type: .application,
platform: .tvOS,
configFiles: ["Config": "config"],
sources: ["source"],
dependencies: [Dependency(type: .framework, reference: "Framework")],
info: Plist(path: "info"),
entitlements: Plist(path: "entitlements"),
preBuildScripts: [BuildScript(script: .path("preBuildScript"))],
postCompileScripts: [BuildScript(script: .path("postCompileScript"))],
postBuildScripts: [BuildScript(script: .path("postBuildScript"))]
),
]
}
$0.it("parses yaml types") {
let path = fixturePath + "yaml.yml"
let dictionary = try loadYamlDictionary(path: path)
@@ -537,7 +655,8 @@ fileprivate func getProjectSpec(_ project: [String: Any], file: String = #file,
projectDictionary[key] = value
}
do {
return try Project(basePath: "", jsonDictionary: projectDictionary)
let template = Spec(relativePath: "", jsonDictionary: projectDictionary)
return try Project(spec: template, basePath: "")
} catch {
throw failure("\(error)", file: file, line: line)
}