From 30e1a845662b2fb51dbd045dcc749163273b749e Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Wed, 2 Oct 2019 08:52:22 -0500 Subject: [PATCH 1/6] Working support for scheme templates. --- Sources/ProjectSpec/Project.swift | 11 +- Sources/ProjectSpec/Target.swift | 55 ++++++++- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 113 ++++++++++++++++++ 3 files changed, 172 insertions(+), 7 deletions(-) diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index 3e2f1e0d..05905ccb 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -153,7 +153,7 @@ extension Project { public init(basePath: Path = "", jsonDictionary: JSONDictionary) throws { self.basePath = basePath - let jsonDictionary = try Project.resolveProject(jsonDictionary: jsonDictionary) + let jsonDictionary = Project.resolveProject(jsonDictionary: jsonDictionary) name = try jsonDictionary.json(atKeyPath: "name") settings = jsonDictionary.json(atKeyPath: "settings") ?? .empty @@ -184,14 +184,15 @@ extension Project { aggregateTargetsMap = Dictionary(uniqueKeysWithValues: aggregateTargets.map { ($0.name, $0) }) } - static func resolveProject(jsonDictionary: JSONDictionary) throws -> JSONDictionary { + static func resolveProject(jsonDictionary: JSONDictionary) -> JSONDictionary { var jsonDictionary = jsonDictionary // resolve multiple times so that we support both multi-platform templates, // as well as platform specific templates in multi-platform targets - jsonDictionary = try Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary) - jsonDictionary = try Target.resolveTargetTemplates(jsonDictionary: jsonDictionary) - jsonDictionary = try Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary) + jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary) + jsonDictionary = Target.resolveTargetTemplates(jsonDictionary: jsonDictionary) + jsonDictionary = Target.resolveSchemeTemplates(jsonDictionary: jsonDictionary) + jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary) return jsonDictionary } diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index e305cf40..bcab72ca 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -134,7 +134,7 @@ extension Target: PathContainer { extension Target { - static func resolveTargetTemplates(jsonDictionary: JSONDictionary) throws -> JSONDictionary { + static func resolveTargetTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { guard var targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else { return jsonDictionary } @@ -186,7 +186,7 @@ extension Target { return jsonDictionary } - static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) throws -> JSONDictionary { + static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) -> JSONDictionary { guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else { return jsonDictionary } @@ -236,6 +236,57 @@ extension Target { merged["targets"] = crossPlatformTargets return merged } + + static func resolveSchemeTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { + guard var schemesDictionary: [String: JSONDictionary] = jsonDictionary["schemes"] as? [String: JSONDictionary] else { + return jsonDictionary + } + + let schemeTemplatesDictionary: [String: JSONDictionary] = jsonDictionary["schemeTemplates"] as? [String: JSONDictionary] ?? [:] + + // 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 = schemeTemplatesDictionary[template] else { + continue + } + allTemplates.insert(template, at: insertionIndex) + collectTemplates(of: templateDictionary, into: &allTemplates, insertAt: &insertionIndex) + insertionIndex += 1 + } + } + + for (schemeName, var scheme) in schemesDictionary { + var templates: [String] = [] + var index: Int = 0 + collectTemplates(of: scheme, into: &templates, insertAt: &index) + if !templates.isEmpty { + var mergedDictionary: JSONDictionary = [:] + for template in templates { + if let templateDictionary = schemeTemplatesDictionary[template] { + mergedDictionary = templateDictionary.merged(onto: mergedDictionary) + } + } + scheme = scheme.merged(onto: mergedDictionary) + scheme = scheme.replaceString("${scheme_name}", with: schemeName) + if let templateAttributes = scheme["templateAttributes"] as? [String: String] { + for (templateAttribute, value) in templateAttributes { + scheme = scheme.replaceString("${\(templateAttribute)}", with: value) + } + } + } + schemesDictionary[schemeName] = scheme + } + + var jsonDictionary = jsonDictionary + jsonDictionary["schemes"] = schemesDictionary + return jsonDictionary + } } extension Target: Equatable { diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index 0b80fc2a..94a461e4 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -853,6 +853,119 @@ class SpecLoadingTests: XCTestCase { try expect(scheme.profile?.environmentVariables.isEmpty) == true } + $0.it("parses scheme templates") { + let targetDictionary: [String: Any] = [ + "deploymentTarget": "1.2.0", + "sources": ["targetSource"], + "templates": ["temp2", "temp"], + "templateAttributes": [ + "source": "replacedSource", + ], + ] + + let project = try getProjectSpec([ + "targets": ["Framework": targetDictionary], + "targetTemplates": [ + "temp": [ + "platform": "iOS", + "sources": [ + "templateSource", + ["path": "Sources/${target_name}"] + ], + ], + "temp2": [ + "type": "framework", + "platform": "tvOS", + "deploymentTarget": "1.1.0", + "configFiles": [ + "debug": "Configs/$target_name/debug.xcconfig", + "release": "Configs/${target_name}/release.xcconfig", + ], + "sources": ["${source}"], + ], + ], + "schemeTemplates": [ + "base_scheme": [ + "build": [ + "parallelizeBuild": false, + "buildImplicitDependencies": false, + "targets": [ + "Target1": "all", + "Target2": "testing", + "Target3": "none", + "Target4": ["testing": true], + "Target5": ["testing": false], + "Target6": ["test", "analyze"], + ], + "preActions": [ + [ + "script": "${pre-action-name}", + "name": "Before Build ${scheme_name}", + "settingsTarget": "Target1", + ], + ], + ], + "test": [ + "config": "debug", + "targets": [ + "Target1", + [ + "name": "Target2", + "parallelizable": true, + "randomExecutionOrder": true, + "skippedTests": ["Test/testExample()"], + ], + ], + "gatherCoverageData": true, + "disableMainThreadChecker": true, + ], + ], + ], + "schemes": [ + "temp2": [ + "templates": ["base_scheme"], + "templateAttributes": [ + "pre-action-name": "modified-name", + ], + ], + ], + ]) + + let scheme = project.schemes.first! + let expectedTargets: [Scheme.BuildTarget] = [ + Scheme.BuildTarget(target: "Target1", buildTypes: BuildType.all), + Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]), + Scheme.BuildTarget(target: "Target3", buildTypes: []), + Scheme.BuildTarget(target: "Target4", buildTypes: [.testing]), + Scheme.BuildTarget(target: "Target5", buildTypes: []), + Scheme.BuildTarget(target: "Target6", buildTypes: [.testing, .analyzing]), + ] + try expect(scheme.name) == "temp2" + try expect(scheme.build.targets) == expectedTargets + try expect(scheme.build.preActions.first?.script) == "modified-name" + try expect(scheme.build.preActions.first?.name) == "Before Build temp2" + try expect(scheme.build.preActions.first?.settingsTarget) == "Target1" + + try expect(scheme.build.parallelizeBuild) == false + try expect(scheme.build.buildImplicitDependencies) == false + + let expectedTest = Scheme.Test( + config: "debug", + gatherCoverageData: true, + disableMainThreadChecker: true, + targets: [ + "Target1", + Scheme.Test.TestTarget( + name: "Target2", + randomExecutionOrder: true, + parallelizable: true, + skippedTests: ["Test/testExample()"] + ), + ] + ) + try expect(scheme.test) == expectedTest + } + $0.it("parses settings") { let project = try Project(path: fixturePath + "settings_test.yml") let buildSettings: BuildSettings = ["SETTING": "value"] From 682f1882889826fd29815efd541bd70f55bc9fd3 Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Wed, 2 Oct 2019 09:06:15 -0500 Subject: [PATCH 2/6] Refactor to make scheme and target templates use the same code. --- Sources/ProjectSpec/Target.swift | 97 +++++++++++--------------------- 1 file changed, 33 insertions(+), 64 deletions(-) diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index bcab72ca..e31aee50 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -105,6 +105,12 @@ public struct Target: ProjectTarget { } } +struct TemplateStructure { + let baseKey: String + let templatesKey: String + let nameToReplace: String +} + extension Target: CustomStringConvertible { public var description: String { @@ -134,58 +140,6 @@ extension Target: PathContainer { extension Target { - static func resolveTargetTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { - guard var targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else { - return jsonDictionary - } - - let targetTemplatesDictionary: [String: JSONDictionary] = jsonDictionary["targetTemplates"] as? [String: JSONDictionary] ?? [:] - - // 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 - } - } - - 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] { - mergedDictionary = templateDictionary.merged(onto: mergedDictionary) - } - } - target = target.merged(onto: mergedDictionary) - target = target.replaceString("$target_name", with: targetName) // Will be removed in upcoming version - target = target.replaceString("${target_name}", with: targetName) - if let templateAttributes = target["templateAttributes"] as? [String: String] { - for (templateAttribute, value) in templateAttributes { - target = target.replaceString("${\(templateAttribute)}", with: value) - } - } - } - targetsDictionary[targetName] = target - } - - var jsonDictionary = jsonDictionary - jsonDictionary["targets"] = targetsDictionary - return jsonDictionary - } - static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) -> JSONDictionary { guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else { return jsonDictionary @@ -237,12 +191,26 @@ extension Target { return merged } + static func resolveTargetTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { + return resolveTemplates(jsonDictionary: jsonDictionary, + templateStructure: TemplateStructure(baseKey: "targets", + templatesKey: "targetTemplates", + nameToReplace: "target_name")) + } + static func resolveSchemeTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { - guard var schemesDictionary: [String: JSONDictionary] = jsonDictionary["schemes"] as? [String: JSONDictionary] else { + return resolveTemplates(jsonDictionary: jsonDictionary, + templateStructure: TemplateStructure(baseKey: "schemes", + templatesKey: "schemeTemplates", + nameToReplace: "scheme_name")) + } + + private static func resolveTemplates(jsonDictionary: JSONDictionary, templateStructure: TemplateStructure) -> JSONDictionary { + guard var baseDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.baseKey] as? [String: JSONDictionary] else { return jsonDictionary } - let schemeTemplatesDictionary: [String: JSONDictionary] = jsonDictionary["schemeTemplates"] as? [String: JSONDictionary] ?? [:] + let templatesDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.templatesKey] as? [String: JSONDictionary] ?? [:] // Recursively collects all nested template names of a given dictionary. func collectTemplates(of jsonDictionary: JSONDictionary, @@ -252,7 +220,7 @@ extension Target { return } for template in templates where !allTemplates.contains(template) { - guard let templateDictionary = schemeTemplatesDictionary[template] else { + guard let templateDictionary = templatesDictionary[template] else { continue } allTemplates.insert(template, at: insertionIndex) @@ -261,30 +229,31 @@ extension Target { } } - for (schemeName, var scheme) in schemesDictionary { + for (referenceName, var reference) in baseDictionary { var templates: [String] = [] var index: Int = 0 - collectTemplates(of: scheme, into: &templates, insertAt: &index) + collectTemplates(of: reference, into: &templates, insertAt: &index) if !templates.isEmpty { var mergedDictionary: JSONDictionary = [:] for template in templates { - if let templateDictionary = schemeTemplatesDictionary[template] { + if let templateDictionary = templatesDictionary[template] { mergedDictionary = templateDictionary.merged(onto: mergedDictionary) } } - scheme = scheme.merged(onto: mergedDictionary) - scheme = scheme.replaceString("${scheme_name}", with: schemeName) - if let templateAttributes = scheme["templateAttributes"] as? [String: String] { + reference = reference.merged(onto: mergedDictionary) + reference = reference.replaceString("$\(templateStructure.nameToReplace)", with: referenceName) // Will be removed in upcoming version + reference = reference.replaceString("${\(templateStructure.nameToReplace)}", with: referenceName) + if let templateAttributes = reference["templateAttributes"] as? [String: String] { for (templateAttribute, value) in templateAttributes { - scheme = scheme.replaceString("${\(templateAttribute)}", with: value) + reference = reference.replaceString("${\(templateAttribute)}", with: value) } } } - schemesDictionary[schemeName] = scheme + baseDictionary[referenceName] = reference } var jsonDictionary = jsonDictionary - jsonDictionary["schemes"] = schemesDictionary + jsonDictionary[templateStructure.baseKey] = baseDictionary return jsonDictionary } } From 6e5766a2bcfad70f42ae2648ed3f2e018d85df38 Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Wed, 2 Oct 2019 10:40:15 -0500 Subject: [PATCH 3/6] Ensure that target names can be changed by template attributes. --- Sources/ProjectSpec/Scheme.swift | 2 +- Sources/ProjectSpec/SpecFile.swift | 4 +++- Tests/XcodeGenKitTests/SpecLoadingTests.swift | 20 ++++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/ProjectSpec/Scheme.swift b/Sources/ProjectSpec/Scheme.swift index cc53b13a..f7ea7627 100644 --- a/Sources/ProjectSpec/Scheme.swift +++ b/Sources/ProjectSpec/Scheme.swift @@ -238,7 +238,7 @@ public struct Scheme: Equatable { } } - public struct BuildTarget: Equatable { + public struct BuildTarget: Equatable, Hashable { public var target: String public var buildTypes: [BuildType] diff --git a/Sources/ProjectSpec/SpecFile.swift b/Sources/ProjectSpec/SpecFile.swift index 6983f0ab..59947313 100644 --- a/Sources/ProjectSpec/SpecFile.swift +++ b/Sources/ProjectSpec/SpecFile.swift @@ -158,7 +158,9 @@ extension Dictionary where Key == String, Value: Any { func replaceString(_ template: String, with replacement: String) -> JSONDictionary { var replaced: JSONDictionary = self for (key, value) in self { - replaced[key] = replace(value: value, template, with: replacement) + let newKey = key.replacingOccurrences(of: template, with: replacement) + replaced.removeValue(forKey: key) + replaced[newKey] = replace(value: value, template, with: replacement) } return replaced } diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index 94a461e4..a01a4bf9 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -890,9 +890,9 @@ class SpecLoadingTests: XCTestCase { "parallelizeBuild": false, "buildImplicitDependencies": false, "targets": [ - "Target1": "all", + "Target${name_1}": "all", "Target2": "testing", - "Target3": "none", + "Target${name_3}": "none", "Target4": ["testing": true], "Target5": ["testing": false], "Target6": ["test", "analyze"], @@ -901,14 +901,14 @@ class SpecLoadingTests: XCTestCase { [ "script": "${pre-action-name}", "name": "Before Build ${scheme_name}", - "settingsTarget": "Target1", + "settingsTarget": "Target${name_1}", ], ], ], "test": [ "config": "debug", "targets": [ - "Target1", + "Target${name_1}", [ "name": "Target2", "parallelizable": true, @@ -926,6 +926,8 @@ class SpecLoadingTests: XCTestCase { "templates": ["base_scheme"], "templateAttributes": [ "pre-action-name": "modified-name", + "name_1": "FirstTarget", + "name_3": "ThirdTarget", ], ], ], @@ -933,18 +935,18 @@ class SpecLoadingTests: XCTestCase { let scheme = project.schemes.first! let expectedTargets: [Scheme.BuildTarget] = [ - Scheme.BuildTarget(target: "Target1", buildTypes: BuildType.all), + Scheme.BuildTarget(target: "TargetFirstTarget", buildTypes: BuildType.all), Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]), - Scheme.BuildTarget(target: "Target3", buildTypes: []), + Scheme.BuildTarget(target: "TargetThirdTarget", buildTypes: []), Scheme.BuildTarget(target: "Target4", buildTypes: [.testing]), Scheme.BuildTarget(target: "Target5", buildTypes: []), Scheme.BuildTarget(target: "Target6", buildTypes: [.testing, .analyzing]), ] try expect(scheme.name) == "temp2" - try expect(scheme.build.targets) == expectedTargets + try expect(Set(scheme.build.targets)) == Set(expectedTargets) try expect(scheme.build.preActions.first?.script) == "modified-name" try expect(scheme.build.preActions.first?.name) == "Before Build temp2" - try expect(scheme.build.preActions.first?.settingsTarget) == "Target1" + try expect(scheme.build.preActions.first?.settingsTarget) == "TargetFirstTarget" try expect(scheme.build.parallelizeBuild) == false try expect(scheme.build.buildImplicitDependencies) == false @@ -954,7 +956,7 @@ class SpecLoadingTests: XCTestCase { gatherCoverageData: true, disableMainThreadChecker: true, targets: [ - "Target1", + "TargetFirstTarget", Scheme.Test.TestTarget( name: "Target2", randomExecutionOrder: true, From 7152d6aa9fbe45e210dc4d7adcb55f5cb62a72c8 Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Sun, 6 Oct 2019 15:33:27 -0500 Subject: [PATCH 4/6] Address CR. --- Sources/ProjectSpec/Project.swift | 2 +- Sources/ProjectSpec/SpecFile.swift | 4 +- Sources/ProjectSpec/Target.swift | 72 --------------------------- Sources/ProjectSpec/Template.swift | 78 ++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 74 deletions(-) create mode 100644 Sources/ProjectSpec/Template.swift diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index 05905ccb..da092574 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -191,7 +191,7 @@ extension Project { // as well as platform specific templates in multi-platform targets jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary) jsonDictionary = Target.resolveTargetTemplates(jsonDictionary: jsonDictionary) - jsonDictionary = Target.resolveSchemeTemplates(jsonDictionary: jsonDictionary) + jsonDictionary = Scheme.resolveSchemeTemplates(jsonDictionary: jsonDictionary) jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary) return jsonDictionary diff --git a/Sources/ProjectSpec/SpecFile.swift b/Sources/ProjectSpec/SpecFile.swift index 59947313..ef2ba36d 100644 --- a/Sources/ProjectSpec/SpecFile.swift +++ b/Sources/ProjectSpec/SpecFile.swift @@ -159,7 +159,9 @@ extension Dictionary where Key == String, Value: Any { var replaced: JSONDictionary = self for (key, value) in self { let newKey = key.replacingOccurrences(of: template, with: replacement) - replaced.removeValue(forKey: key) + if newKey != key { + replaced.removeValue(forKey: key) + } replaced[newKey] = replace(value: value, template, with: replacement) } return replaced diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index e31aee50..8fa85a66 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -105,12 +105,6 @@ public struct Target: ProjectTarget { } } -struct TemplateStructure { - let baseKey: String - let templatesKey: String - let nameToReplace: String -} - extension Target: CustomStringConvertible { public var description: String { @@ -190,72 +184,6 @@ extension Target { merged["targets"] = crossPlatformTargets return merged } - - static func resolveTargetTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { - return resolveTemplates(jsonDictionary: jsonDictionary, - templateStructure: TemplateStructure(baseKey: "targets", - templatesKey: "targetTemplates", - nameToReplace: "target_name")) - } - - static func resolveSchemeTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { - return resolveTemplates(jsonDictionary: jsonDictionary, - templateStructure: TemplateStructure(baseKey: "schemes", - templatesKey: "schemeTemplates", - nameToReplace: "scheme_name")) - } - - private static func resolveTemplates(jsonDictionary: JSONDictionary, templateStructure: TemplateStructure) -> JSONDictionary { - guard var baseDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.baseKey] as? [String: JSONDictionary] else { - return jsonDictionary - } - - let templatesDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.templatesKey] as? [String: JSONDictionary] ?? [:] - - // 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 = templatesDictionary[template] else { - continue - } - allTemplates.insert(template, at: insertionIndex) - collectTemplates(of: templateDictionary, into: &allTemplates, insertAt: &insertionIndex) - insertionIndex += 1 - } - } - - for (referenceName, var reference) in baseDictionary { - var templates: [String] = [] - var index: Int = 0 - collectTemplates(of: reference, into: &templates, insertAt: &index) - if !templates.isEmpty { - var mergedDictionary: JSONDictionary = [:] - for template in templates { - if let templateDictionary = templatesDictionary[template] { - mergedDictionary = templateDictionary.merged(onto: mergedDictionary) - } - } - reference = reference.merged(onto: mergedDictionary) - reference = reference.replaceString("$\(templateStructure.nameToReplace)", with: referenceName) // Will be removed in upcoming version - reference = reference.replaceString("${\(templateStructure.nameToReplace)}", with: referenceName) - if let templateAttributes = reference["templateAttributes"] as? [String: String] { - for (templateAttribute, value) in templateAttributes { - reference = reference.replaceString("${\(templateAttribute)}", with: value) - } - } - } - baseDictionary[referenceName] = reference - } - - var jsonDictionary = jsonDictionary - jsonDictionary[templateStructure.baseKey] = baseDictionary - return jsonDictionary - } } extension Target: Equatable { diff --git a/Sources/ProjectSpec/Template.swift b/Sources/ProjectSpec/Template.swift new file mode 100644 index 00000000..974fe3d4 --- /dev/null +++ b/Sources/ProjectSpec/Template.swift @@ -0,0 +1,78 @@ +import Foundation +import JSONUtilities + +struct TemplateStructure { + let baseKey: String + let templatesKey: String + let nameToReplace: String +} + +extension Target { + static func resolveTargetTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { + return resolveTemplates(jsonDictionary: jsonDictionary, + templateStructure: TemplateStructure(baseKey: "targets", + templatesKey: "targetTemplates", + nameToReplace: "target_name")) + } +} + +extension Scheme { + static func resolveSchemeTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary { + return resolveTemplates(jsonDictionary: jsonDictionary, + templateStructure: TemplateStructure(baseKey: "schemes", + templatesKey: "schemeTemplates", + nameToReplace: "scheme_name")) + } +} + +private func resolveTemplates(jsonDictionary: JSONDictionary, templateStructure: TemplateStructure) -> JSONDictionary { + guard var baseDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.baseKey] as? [String: JSONDictionary] else { + return jsonDictionary + } + + let templatesDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.templatesKey] as? [String: JSONDictionary] ?? [:] + + // 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 = templatesDictionary[template] else { + continue + } + allTemplates.insert(template, at: insertionIndex) + collectTemplates(of: templateDictionary, into: &allTemplates, insertAt: &insertionIndex) + insertionIndex += 1 + } + } + + for (referenceName, var reference) in baseDictionary { + var templates: [String] = [] + var index: Int = 0 + collectTemplates(of: reference, into: &templates, insertAt: &index) + if !templates.isEmpty { + var mergedDictionary: JSONDictionary = [:] + for template in templates { + if let templateDictionary = templatesDictionary[template] { + mergedDictionary = templateDictionary.merged(onto: mergedDictionary) + } + } + reference = reference.merged(onto: mergedDictionary) + reference = reference.replaceString("$\(templateStructure.nameToReplace)", with: referenceName) // Will be removed in upcoming version + reference = reference.replaceString("${\(templateStructure.nameToReplace)}", with: referenceName) + if let templateAttributes = reference["templateAttributes"] as? [String: String] { + for (templateAttribute, value) in templateAttributes { + reference = reference.replaceString("${\(templateAttribute)}", with: value) + } + } + } + baseDictionary[referenceName] = reference + } + + var jsonDictionary = jsonDictionary + jsonDictionary[templateStructure.baseKey] = baseDictionary + return jsonDictionary +} From ba34891aae8e16c64abad1fdb24d025e35c4ae5c Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Sun, 6 Oct 2019 15:48:10 -0500 Subject: [PATCH 5/6] Add changelog and docs. --- CHANGELOG.md | 4 ++++ Docs/ProjectSpec.md | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba662adf..152f4341 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Version +#### Added + +- Scheme Templates [#672](https://github.com/yonaskolb/XcodeGen/pull/672) @bclymer + #### Fixed - Fixed macOS unit test setting preset [#665](https://github.com/yonaskolb/XcodeGen/pull/665) @yonaskolb - Add `rcproject` files to sources build phase instead of resources [#669](https://github.com/yonaskolb/XcodeGen/pull/669) @Qusic diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index 0321f222..61aa3bfa 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -23,6 +23,7 @@ - [Aggregate Target](#aggregate-target) - [Target Template](#target-template) - [Scheme](#scheme) + - [Scheme Template](#scheme-template) - [Swift Package](#swift-package) ## General @@ -781,6 +782,39 @@ schemes: revealArchiveInOrganizer: false ``` +### Scheme Template + +This is a template that can be referenced from a normal scheme using the `templates` property. The properties of this template are the same as a [Scheme](#scheme). This functions identically in practice to [Target Template](#target-template). +Any instances of `${scheme_name}` within each template will be replaced by the final scheme name which references the template. +Any attributes defined within a targets `templateAttributes` will be used to replace any attribute references in the template using the syntax `${attribute_name}`. + +```yaml +schemes: + MyModule: + templates: + - FeatureModuleScheme + templateAttributes: + testTargetName: MyModuleTests + +schemeTemplates: + FeatureModuleScheme: + templates: + - TestScheme + build: + targets: + ${scheme_name}: build + + TestScheme: + test: + gatherCoverageData: true + targets: + - name: ${testTargetName} + parallelizable: true + randomExecutionOrder: true +``` + +The result will be a scheme that builds `MyModule` when you request a build, and will test against `MyModuleTests` when you request to run tests. This is particularly useful when you work in a very modular application and each module has a similar structure. + ## Swift Package Swift packages are defined at a project level, and then linked to individual targets via a [Dependency](#dependency). @@ -804,4 +838,4 @@ targets: App: dependencies: - package: Yams -``` \ No newline at end of file +``` From 5348e4080b0dea842ae3ce2a1103c297ef935c44 Mon Sep 17 00:00:00 2001 From: Brian Clymer Date: Mon, 7 Oct 2019 19:39:29 -0500 Subject: [PATCH 6/6] Missed a target reference. --- Docs/ProjectSpec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index 61aa3bfa..204cd230 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -786,7 +786,7 @@ schemes: This is a template that can be referenced from a normal scheme using the `templates` property. The properties of this template are the same as a [Scheme](#scheme). This functions identically in practice to [Target Template](#target-template). Any instances of `${scheme_name}` within each template will be replaced by the final scheme name which references the template. -Any attributes defined within a targets `templateAttributes` will be used to replace any attribute references in the template using the syntax `${attribute_name}`. +Any attributes defined within a scheme's `templateAttributes` will be used to replace any attribute references in the template using the syntax `${attribute_name}`. ```yaml schemes: