Skip to content

Commit

Permalink
Merge pull request #231 from yonaskolb/scheme-actions
Browse files Browse the repository at this point in the history
Add support for scheme pre-actions and post-actions
  • Loading branch information
yonaskolb committed Jan 30, 2018
2 parents 431011e + d72b5d8 commit 0f87936
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Master

#### Added
- Added scheme pre-actions and post-actions [#231](https://github.com/yonaskolb/XcodeGen/pull/231) @kastiglione
- Added `options.disabledValidations` including `missingConfigs` to disable project validation errors [#220](https://github.com/yonaskolb/XcodeGen/pull/220) @keith
- Generate UI Test Target Attributes [#221](https://github.com/yonaskolb/XcodeGen/pull/221) @anreitersimon

Expand Down
11 changes: 11 additions & 0 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,17 @@ The different actions share some properties:
- `debug`: run, test, analyze
- `release`: profile, archive
- [ ] **commandLineArguments**: **[String:Bool]** - `run`, `test` and `profile` actions have a map of command line arguments to whether they are enabled
- [ ] **preActions**: **[[Execution Action](#execution-action)]** - Scheme run scripts that run *before* the action is run
- [ ] **postActions**: **[[Execution Action](#execution-action)]** - Scheme run scripts that run *after* the action is run

### Execution Action
Scheme run scripts added via **preActions** or **postActions**. They run before or after a build action, respectively, and in the order defined. Each execution action can contain:

- [x] **script**: **String** - an inline shell script
- [ ] **name**: **String** - name of a script. Defaults to `Run Script`
- [ ] **settingsTarget**: **String** - name of a build or test target whose settings will be available as environment variables.

A multiline script can be written using the various YAML multiline methods, for example with `|`. See [Build Script](#build-script).

### Test Action
- [ ] **gatherCoverageData**: **Bool** - a boolean that indicates if this scheme should gather coverage data. This defaults to false
Expand Down
110 changes: 98 additions & 12 deletions Sources/ProjectSpec/Scheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,66 @@ public struct Scheme: Equatable {
self.archive = archive
}

public struct ExecutionAction: Equatable {
public var script: String
public var name: String
public var settingsTarget: String?
public init(name: String, script: String, settingsTarget: String?) {
self.script = script
self.name = name
self.settingsTarget = settingsTarget
}

public static func == (lhs: ExecutionAction, rhs: ExecutionAction) -> Bool {
return lhs.script == rhs.script &&
lhs.name == rhs.name &&
lhs.settingsTarget == rhs.settingsTarget
}
}

public struct Build: Equatable {
public var targets: [BuildTarget]
public init(targets: [BuildTarget]) {
public var preActions: [ExecutionAction]
public var postActions: [ExecutionAction]
public init(
targets: [BuildTarget],
preActions: [ExecutionAction] = [],
postActions: [ExecutionAction] = []
) {
self.targets = targets
self.preActions = preActions
self.postActions = postActions
}

public static func == (lhs: Build, rhs: Build) -> Bool {
return lhs.targets == rhs.targets
return lhs.targets == rhs.targets &&
lhs.preActions == rhs.postActions &&
lhs.postActions == rhs.postActions
}
}

public struct Run: BuildAction {
public var config: String
public var commandLineArguments: [String: Bool]
public init(config: String, commandLineArguments: [String: Bool] = [:]) {
public var preActions: [ExecutionAction]
public var postActions: [ExecutionAction]
public init(
config: String,
commandLineArguments: [String: Bool] = [:],
preActions: [ExecutionAction] = [],
postActions: [ExecutionAction] = []
) {
self.config = config
self.commandLineArguments = commandLineArguments
self.preActions = preActions
self.postActions = postActions
}

public static func == (lhs: Run, rhs: Run) -> Bool {
return lhs.config == rhs.config &&
lhs.commandLineArguments == rhs.commandLineArguments
lhs.commandLineArguments == rhs.commandLineArguments &&
lhs.preActions == rhs.postActions &&
lhs.postActions == rhs.postActions
}
}

Expand All @@ -62,24 +100,31 @@ public struct Scheme: Equatable {
public var gatherCoverageData: Bool
public var commandLineArguments: [String: Bool]
public var targets: [String]

public var preActions: [ExecutionAction]
public var postActions: [ExecutionAction]
public init(
config: String,
gatherCoverageData: Bool = false,
commandLineArguments: [String: Bool] = [:],
targets: [String] = []
targets: [String] = [],
preActions: [ExecutionAction] = [],
postActions: [ExecutionAction] = []
) {
self.config = config
self.gatherCoverageData = gatherCoverageData
self.commandLineArguments = commandLineArguments
self.targets = targets
self.preActions = preActions
self.postActions = postActions
}

public static func == (lhs: Test, rhs: Test) -> Bool {
return lhs.config == rhs.config &&
lhs.commandLineArguments == rhs.commandLineArguments &&
lhs.gatherCoverageData == rhs.gatherCoverageData &&
lhs.targets == rhs.targets
lhs.targets == rhs.targets &&
lhs.preActions == rhs.postActions &&
lhs.postActions == rhs.postActions
}
}

Expand All @@ -97,24 +142,46 @@ public struct Scheme: Equatable {
public struct Profile: BuildAction {
public var config: String
public var commandLineArguments: [String: Bool]
public init(config: String, commandLineArguments: [String: Bool] = [:]) {
public var preActions: [ExecutionAction]
public var postActions: [ExecutionAction]
public init(
config: String,
commandLineArguments: [String: Bool] = [:],
preActions: [ExecutionAction] = [],
postActions: [ExecutionAction] = []
) {
self.config = config
self.commandLineArguments = commandLineArguments
self.preActions = preActions
self.postActions = postActions
}

public static func == (lhs: Profile, rhs: Profile) -> Bool {
return lhs.config == rhs.config && lhs.commandLineArguments == rhs.commandLineArguments
return lhs.config == rhs.config &&
lhs.commandLineArguments == rhs.commandLineArguments &&
lhs.preActions == rhs.postActions &&
lhs.postActions == rhs.postActions
}
}

public struct Archive: BuildAction {
public var config: String
public init(config: String) {
public var preActions: [ExecutionAction]
public var postActions: [ExecutionAction]
public init(
config: String,
preActions: [ExecutionAction] = [],
postActions: [ExecutionAction] = []
) {
self.config = config
self.preActions = preActions
self.postActions = postActions
}

public static func == (lhs: Archive, rhs: Archive) -> Bool {
return lhs.config == rhs.config
return lhs.config == rhs.config &&
lhs.preActions == rhs.postActions &&
lhs.postActions == rhs.postActions
}
}

Expand Down Expand Up @@ -146,11 +213,22 @@ protocol BuildAction: Equatable {
var config: String { get }
}

extension Scheme.ExecutionAction: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
script = try jsonDictionary.json(atKeyPath: "script")
name = jsonDictionary.json(atKeyPath: "name") ?? "Run Script"
settingsTarget = jsonDictionary.json(atKeyPath: "settingsTarget")
}
}

extension Scheme.Run: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
config = try jsonDictionary.json(atKeyPath: "config")
commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? []
postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? []
}
}

Expand All @@ -161,6 +239,8 @@ extension Scheme.Test: JSONObjectConvertible {
gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? false
commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
targets = jsonDictionary.json(atKeyPath: "targets") ?? []
preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? []
postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? []
}
}

Expand All @@ -169,6 +249,8 @@ extension Scheme.Profile: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
config = try jsonDictionary.json(atKeyPath: "config")
commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? []
postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? []
}
}

Expand All @@ -183,6 +265,8 @@ extension Scheme.Archive: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
config = try jsonDictionary.json(atKeyPath: "config")
preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? []
postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? []
}
}

Expand Down Expand Up @@ -224,14 +308,16 @@ extension Scheme.Build: JSONObjectConvertible {
targets.append(Scheme.BuildTarget(target: target, buildTypes: buildTypes))
}
self.targets = targets.sorted { $0.target < $1.target }
preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? []
postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? []
}
}

extension BuildType: JSONPrimitiveConvertible {

public typealias JSONType = String

public static func from(jsonValue: String) -> XCScheme.BuildAction.Entry.BuildFor? {
public static func from(jsonValue: String) -> BuildType? {
switch jsonValue {
case "test", "testing": return .testing
case "profile", "profiling": return .profiling
Expand Down
22 changes: 21 additions & 1 deletion Sources/XcodeGenKit/ProjectGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,23 @@ public class ProjectGenerator {

let buildActionEntries: [XCScheme.BuildAction.Entry] = scheme.build.targets.map(getBuildEntry)

func getExecutionAction(_ action: Scheme.ExecutionAction) -> XCScheme.ExecutionAction {
// ExecutionActions can require the use of build settings. Xcode allows the settings to come from a build or test target.
let environmentBuildable = action.settingsTarget.flatMap { settingsTarget in
return (buildActionEntries + testBuildTargetEntries)
.first { settingsTarget == ($0.buildableReference.buildableName as NSString).deletingPathExtension }?
.buildableReference
}
return XCScheme.ExecutionAction(scriptText: action.script, title: action.name, environmentBuildable: environmentBuildable)
}

let buildableReference = buildActionEntries.first!.buildableReference
let productRunable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference)

let buildAction = XCScheme.BuildAction(
buildActionEntries: buildActionEntries,
preActions: scheme.build.preActions.map(getExecutionAction),
postActions: scheme.build.postActions.map(getExecutionAction),
parallelizeBuild: true,
buildImplicitDependencies: true
)
Expand All @@ -82,6 +94,8 @@ public class ProjectGenerator {
buildConfiguration: scheme.test?.config ?? defaultDebugConfig.name,
macroExpansion: buildableReference,
testables: testables,
preActions: scheme.test?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.test?.postActions.map(getExecutionAction) ?? [],
shouldUseLaunchSchemeArgsEnv: scheme.test?.commandLineArguments.isEmpty ?? true,
codeCoverageEnabled: scheme.test?.gatherCoverageData ?? false,
commandlineArguments: testCommandLineArgs
Expand All @@ -90,12 +104,16 @@ public class ProjectGenerator {
let launchAction = XCScheme.LaunchAction(
buildableProductRunnable: productRunable,
buildConfiguration: scheme.run?.config ?? defaultDebugConfig.name,
preActions: scheme.run?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.run?.postActions.map(getExecutionAction) ?? [],
commandlineArguments: launchCommandLineArgs
)

let profileAction = XCScheme.ProfileAction(
buildableProductRunnable: productRunable,
buildConfiguration: scheme.profile?.config ?? defaultReleaseConfig.name,
preActions: scheme.profile?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.profile?.postActions.map(getExecutionAction) ?? [],
shouldUseLaunchSchemeArgsEnv: scheme.profile?.commandLineArguments.isEmpty ?? true,
commandlineArguments: profileCommandLineArgs
)
Expand All @@ -104,7 +122,9 @@ public class ProjectGenerator {

let archiveAction = XCScheme.ArchiveAction(
buildConfiguration: scheme.archive?.config ?? defaultReleaseConfig.name,
revealArchiveInOrganizer: true
revealArchiveInOrganizer: true,
preActions: scheme.archive?.preActions.map(getExecutionAction) ?? [],
postActions: scheme.archive?.postActions.map(getExecutionAction) ?? []
)

return XCScheme(
Expand Down
8 changes: 8 additions & 0 deletions Tests/Fixtures/TestProject/spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,11 @@ targets:
INFOPLIST_FILE: TestProjectUITests/Info.plist
dependencies:
- target: App_iOS
schemes:
Framework:
build:
targets:
Framework_iOS: all
preActions:
- script: echo Starting Framework Build
settingsTarget: Framework_iOS
10 changes: 10 additions & 0 deletions Tests/XcodeGenKitTests/FixtureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,15 @@ func fixtureTests() {
try expect(variableGroup.children.filter { $0 == refs.first?.reference }.count) == 1
}
}

$0.it("generates scheme execution actions") {
guard let project = project else { return }

let frameworkScheme = project.sharedData?.schemes.first { $0.name == "Framework" }
try expect(frameworkScheme?.buildAction?.preActions.first?.scriptText) == "echo Starting Framework Build"
try expect(frameworkScheme?.buildAction?.preActions.first?.title) == "Run Script"
try expect(frameworkScheme?.buildAction?.preActions.first?.environmentBuildable?.blueprintName) == "Framework"
try expect(frameworkScheme?.buildAction?.preActions.first?.environmentBuildable?.buildableName) == "Framework_iOS.framework"
}
}
}
7 changes: 6 additions & 1 deletion Tests/XcodeGenKitTests/ProjectGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,10 @@ func projectGeneratorTests() {

let buildTarget = Scheme.BuildTarget(target: application.name)
$0.it("generates scheme") {
let preAction = Scheme.ExecutionAction(name: "Script", script: "echo Starting", settingsTarget: application.name)
let scheme = Scheme(
name: "MyScheme",
build: Scheme.Build(targets: [buildTarget])
build: Scheme.Build(targets: [buildTarget], preActions: [preAction])
)
let spec = ProjectSpec(
basePath: "",
Expand All @@ -320,6 +321,10 @@ func projectGeneratorTests() {
throw failure("Scheme not found")
}
try expect(scheme.name) == "MyScheme"
try expect(xcscheme.buildAction?.preActions.first?.title) == "Script"
try expect(xcscheme.buildAction?.preActions.first?.scriptText) == "echo Starting"
try expect(xcscheme.buildAction?.preActions.first?.environmentBuildable?.buildableName) == "MyApp.app"
try expect(xcscheme.buildAction?.preActions.first?.environmentBuildable?.blueprintName) == "MyScheme"
guard let buildActionEntry = xcscheme.buildAction?.buildActionEntries.first else {
throw failure("Build Action entry not found")
}
Expand Down
7 changes: 7 additions & 0 deletions Tests/XcodeGenKitTests/SpecLoadingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ func specLoadingTests() {
"Target4": ["testing": true],
"Target5": ["testing": false],
"Target6": ["test", "analyze"],
], "preActions": [
["script": "echo Before Build",
"name": "Before Build",
"settingsTarget": "Target1"]
]],
]
let scheme = try Scheme(name: "Scheme", jsonDictionary: schemeDictionary)
Expand All @@ -176,6 +180,9 @@ func specLoadingTests() {
]
try expect(scheme.name) == "Scheme"
try expect(scheme.build.targets) == expectedTargets
try expect(scheme.build.preActions.first?.script) == "echo Before Build"
try expect(scheme.build.preActions.first?.name) == "Before Build"
try expect(scheme.build.preActions.first?.settingsTarget) == "Target1"
}

$0.it("parses settings") {
Expand Down

0 comments on commit 0f87936

Please sign in to comment.