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"],