From 0c2554db2b9a6985d843a3c4e07f38f4cf51e040 Mon Sep 17 00:00:00 2001 From: Tom Quist Date: Thu, 14 Mar 2019 08:39:08 +0100 Subject: [PATCH] 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. --- CHANGELOG.md | 1 + Sources/ProjectSpec/Target.swift | 23 +++- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 121 ++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5149de6..92bde526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 5beb82b2..67b578ff 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -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] { diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index 607f3ce6..72b95452 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -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"],