Merge pull request #672 from yonaskolb/BC-SchemeTemplates

Scheme Templates
This commit is contained in:
Yonas Kolb 2019-10-08 17:38:32 +11:00 committed by GitHub
commit 27564f9a28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 245 additions and 61 deletions

View File

@ -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

View File

@ -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 scheme's `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
```
```

View File

@ -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 = Scheme.resolveSchemeTemplates(jsonDictionary: jsonDictionary)
jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary)
return jsonDictionary
}

View File

@ -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]

View File

@ -158,7 +158,11 @@ 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)
if newKey != key {
replaced.removeValue(forKey: key)
}
replaced[newKey] = replace(value: value, template, with: replacement)
}
return replaced
}

View File

@ -134,59 +134,7 @@ extension Target: PathContainer {
extension Target {
static func resolveTargetTemplates(jsonDictionary: JSONDictionary) throws -> 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) throws -> JSONDictionary {
static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) -> JSONDictionary {
guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else {
return jsonDictionary
}

View File

@ -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
}

View File

@ -853,6 +853,121 @@ 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": [
"Target${name_1}": "all",
"Target2": "testing",
"Target${name_3}": "none",
"Target4": ["testing": true],
"Target5": ["testing": false],
"Target6": ["test", "analyze"],
],
"preActions": [
[
"script": "${pre-action-name}",
"name": "Before Build ${scheme_name}",
"settingsTarget": "Target${name_1}",
],
],
],
"test": [
"config": "debug",
"targets": [
"Target${name_1}",
[
"name": "Target2",
"parallelizable": true,
"randomExecutionOrder": true,
"skippedTests": ["Test/testExample()"],
],
],
"gatherCoverageData": true,
"disableMainThreadChecker": true,
],
],
],
"schemes": [
"temp2": [
"templates": ["base_scheme"],
"templateAttributes": [
"pre-action-name": "modified-name",
"name_1": "FirstTarget",
"name_3": "ThirdTarget",
],
],
],
])
let scheme = project.schemes.first!
let expectedTargets: [Scheme.BuildTarget] = [
Scheme.BuildTarget(target: "TargetFirstTarget", buildTypes: BuildType.all),
Scheme.BuildTarget(target: "Target2", buildTypes: [.testing, .analyzing]),
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(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) == "TargetFirstTarget"
try expect(scheme.build.parallelizeBuild) == false
try expect(scheme.build.buildImplicitDependencies) == false
let expectedTest = Scheme.Test(
config: "debug",
gatherCoverageData: true,
disableMainThreadChecker: true,
targets: [
"TargetFirstTarget",
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"]