mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2024-12-11 07:16:40 +03:00
Merge pull request #672 from yonaskolb/BC-SchemeTemplates
Scheme Templates
This commit is contained in:
commit
27564f9a28
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
```
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
78
Sources/ProjectSpec/Template.swift
Normal file
78
Sources/ProjectSpec/Template.swift
Normal 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
|
||||
}
|
@ -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"]
|
||||
|
Loading…
Reference in New Issue
Block a user