Add support for nested templates

It would be convenient if templates could be nested, which means
a template can be based on another template.

This change implements support for nested templates. It avoids
cycles by just ignoring templates that have already been visited
when collecting the set of templates to use.
This commit is contained in:
Tom Quist 2019-03-14 08:39:08 +01:00
parent f32673af70
commit 0c2554db2b
3 changed files with 143 additions and 2 deletions

View File

@ -6,6 +6,7 @@
- Added `missingConfigFiles` to `options.disabledValidations` to optionally skip checking for the existence of config files.
- Added ability to automatically include Carthage related dependencies via `includeRelated: true` [#506](https://github.com/yonaskolb/XcodeGen/pull/506) @rpassis
- Added ability to define a per-platform `deploymentTarget` for Multi-Platform targets. [#510](https://github.com/yonaskolb/XcodeGen/pull/510) @ainopara
- Added support for nested target templates [#534](https://github.com/yonaskolb/XcodeGen/pull/534) @tomquist
#### Fixed
- Sources outside a project spec's directory will be correctly referenced as relative paths in the project file. [#524](https://github.com/yonaskolb/XcodeGen/pull/524)

View File

@ -139,9 +139,28 @@ extension Target {
let targetTemplatesDictionary: [String: JSONDictionary] = jsonDictionary["targetTemplates"] as? [String: JSONDictionary] ?? [:]
for (targetName, var target) in targetsDictionary {
// Recursively collects all nested template names of a given dictionary.
func collectTemplates(of jsonDictionary: JSONDictionary,
into allTemplates: inout [String],
insertAt insertionIndex: inout Int) {
guard let templates = jsonDictionary["templates"] as? [String] else {
return
}
for template in templates where !allTemplates.contains(template) {
guard let templateDictionary = targetTemplatesDictionary[template] else {
continue
}
allTemplates.insert(template, at: insertionIndex)
collectTemplates(of: templateDictionary, into: &allTemplates, insertAt: &insertionIndex)
insertionIndex += 1
}
}
if let templates = target["templates"] as? [String] {
for (targetName, var target) in targetsDictionary {
var templates: [String] = []
var index: Int = 0
collectTemplates(of: target, into: &templates, insertAt: &index)
if !templates.isEmpty {
var mergedDictionary: JSONDictionary = [:]
for template in templates {
if let templateDictionary = targetTemplatesDictionary[template] {

View File

@ -363,6 +363,127 @@ class SpecLoadingTests: XCTestCase {
try expect(target.configFiles["debug"]) == "Configs/Framework/debug.xcconfig" // replaces $target_name
}
$0.it("parses nested target templates") {
let targetDictionary: [String: Any] = [
"deploymentTarget": "1.2.0",
"sources": ["targetSource"],
"templates": ["temp2"],
]
let project = try getProjectSpec([
"targets": ["Framework": targetDictionary],
"targetTemplates": [
"temp": [
"type": "framework",
"platform": "iOS",
"sources": ["nestedTemplateSource1"],
],
"temp1": [
"type": "application",
"sources": ["nestedTemplateSource2"],
],
"temp2": [
"platform": "tvOS",
"deploymentTarget": "1.1.0",
"configFiles": ["debug": "Configs/$target_name/debug.xcconfig"],
"templates": ["temp", "temp1"],
"sources": ["templateSource"],
]
],
])
let target = project.targets.first!
try expect(target.type) == .application // uses value of last nested template
try expect(target.platform) == .tvOS // uses latest value
try expect(target.deploymentTarget) == Version("1.2.0") // keeps value
try expect(target.sources) == ["nestedTemplateSource1", "nestedTemplateSource2", "templateSource", "targetSource"] // merges array in order
try expect(target.configFiles["debug"]) == "Configs/Framework/debug.xcconfig" // replaces $target_name
}
$0.it("parses complex nested target templates") {
let targetDictionary: [String: Any] = [
"type": "framework",
"platform": "iOS",
"templates": ["temp"],
"sources": ["target"],
]
let project = try getProjectSpec([
"targets": ["Framework": targetDictionary],
"targetTemplates": [
"temp": [
"templates": ["a", "d"],
"sources": ["temp"],
],
"a": [
"templates": ["b", "c"],
"sources": ["a"],
],
"b": [
"sources": ["b"],
],
"c": [
"sources": ["c"],
],
"d": [
"sources": ["d"],
"templates": ["e"],
],
"e": [
"sources": ["e"],
],
],
])
let target = project.targets.first!
try expect(target.type) == .framework // uses value of last nested template
try expect(target.platform) == .iOS // uses latest value
try expect(target.sources) == ["b", "c", "a", "e", "d", "temp", "target"] // merges array in order
}
$0.it("parses nested target templates with cycle") {
let targetDictionary: [String: Any] = [
"deploymentTarget": "1.2.0",
"sources": ["targetSource"],
"templates": ["temp2"],
]
let project = try getProjectSpec([
"targets": ["Framework": targetDictionary],
"targetTemplates": [
"temp": [
"type": "framework",
"platform": "iOS",
"templates": ["temp1"],
"sources": ["nestedTemplateSource1"],
],
"temp1": [
"platform": "macOS",
"templates": ["temp2"],
"sources": ["nestedTemplateSource2"],
],
"temp2": [
"platform": "tvOS",
"deploymentTarget": "1.1.0",
"configFiles": ["debug": "Configs/$target_name/debug.xcconfig"],
"templates": ["temp", "temp1"],
"sources": ["templateSource"],
]
],
])
let target = project.targets.first!
try expect(target.type) == .framework // uses value
try expect(target.platform) == .tvOS // uses latest value
try expect(target.deploymentTarget) == Version("1.2.0") // keeps value
try expect(target.sources) == ["nestedTemplateSource2", "nestedTemplateSource1", "templateSource", "targetSource"] // merges array in order
try expect(target.configFiles["debug"]) == "Configs/Framework/debug.xcconfig" // replaces $target_name
}
$0.it("parses aggregate targets") {
let dictionary: [String: Any] = [
"targets": ["target_1", "target_2"],