diff --git a/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj b/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj index 0875d8a67..7fa0aeae8 100644 --- a/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj +++ b/Fixtures/TestProject/GeneratedProject.xcodeproj/project.pbxproj @@ -10,8 +10,8 @@ BF1073850101 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR1332263601 /* AppDelegate.swift */; settings = {}; }; BF1744565901 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR6218091901 /* ViewController.swift */; settings = {}; }; BF2753556301 /* MyFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FR2993497801 /* MyFramework.framework */; }; - BF2753556301 /* MyFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FR2993497801 /* MyFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF2753556301 /* MyFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FR2993497801 /* MyFramework.framework */; }; + BF2753556302 = {isa = PBXBuildFile; fileRef = FR2993497801 /* MyFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF3154421201 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FR5980633301 /* Assets.xcassets */; settings = {}; }; BF3515549501 /* MyFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = FR7740960501 /* MyFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF5986511201 = {isa = PBXBuildFile; fileRef = FR6523263101 /* TestProject.app */; }; @@ -44,7 +44,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - BF2753556301 /* MyFramework.framework in CopyFiles */, + BF2753556302 /* MyFramework.framework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -53,10 +53,10 @@ /* Begin PBXFileReference section */ FR1332263601 /* AppDelegate.swift */ = {isa = PBXFileReference; path = AppDelegate.swift; sourceTree = ""; }; FR1345298501 /* Info.plist */ = {isa = PBXFileReference; path = Info.plist; sourceTree = ""; }; - FR1345298501 /* Info.plist */ = {isa = PBXFileReference; path = Info.plist; sourceTree = ""; }; + FR1345298502 /* Info.plist */ = {isa = PBXFileReference; path = Info.plist; sourceTree = ""; }; FR2993497801 /* MyFramework.framework */ = {isa = PBXFileReference; explicitFileType = framework; includeInIndex = 0; path = MyFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FR3676338401 /* Base */ = {isa = PBXFileReference; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - FR3676338401 /* Base */ = {isa = PBXFileReference; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + FR3676338402 /* Base */ = {isa = PBXFileReference; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; FR5980633301 /* Assets.xcassets */ = {isa = PBXFileReference; path = Assets.xcassets; sourceTree = ""; }; FR6218091901 /* ViewController.swift */ = {isa = PBXFileReference; path = ViewController.swift; sourceTree = ""; }; FR6523263101 /* TestProject.app */ = {isa = PBXFileReference; explicitFileType = app; includeInIndex = 0; path = TestProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -87,7 +87,7 @@ isa = PBXGroup; children = ( FR7078510801 /* FrameworkFile.swift */, - FR1345298501 /* Info.plist */, + FR1345298502 /* Info.plist */, FR7740960501 /* MyFramework.h */, ); name = MyFramework; @@ -156,6 +156,7 @@ HBP299349701 /* Headers */, FBP299349701 /* Frameworks */, CFBP29934901 /* Copy Files */, + SSBP25860702 /* Run Script */, ); buildRules = ( ); @@ -174,6 +175,7 @@ HBP652326301 /* Headers */, FBP652326301 /* Frameworks */, CFBP65232601 /* Copy Files */, + SSBP25860701 /* Run Script */, ); buildRules = ( ); @@ -224,6 +226,37 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + SSBP25860701 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; + SSBP25860702 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ SBP299349701 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -256,7 +289,7 @@ VG1473702401 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - FR3676338401 /* Base */, + FR3676338402 /* Base */, ); name = Main.storyboard; sourceTree = ""; @@ -323,7 +356,7 @@ }; name = Debug; }; - XCBC47994501 /* Debug */ = { + XCBC47994502 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -336,7 +369,7 @@ }; name = Debug; }; - XCBC47994501 /* Debug */ = { + XCBC47994503 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -397,7 +430,7 @@ }; name = Release; }; - XCBC88111401 /* Release */ = { + XCBC88111402 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -410,7 +443,7 @@ }; name = Release; }; - XCBC88111401 /* Release */ = { + XCBC88111403 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -433,8 +466,8 @@ XCCL29934901 /* Build configuration list for PBXNativeTarget "MyFramework" */ = { isa = XCConfigurationList; buildConfigurations = ( - XCBC88111401 /* Release */, - XCBC47994501 /* Debug */, + XCBC88111403 /* Release */, + XCBC47994503 /* Debug */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = ""; @@ -442,8 +475,8 @@ XCCL65232601 /* Build configuration list for PBXNativeTarget "TestProject" */ = { isa = XCConfigurationList; buildConfigurations = ( - XCBC88111401 /* Release */, - XCBC47994501 /* Debug */, + XCBC47994502 /* Debug */, + XCBC88111402 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = ""; diff --git a/Fixtures/TestProject/TestProject.xcodeproj/project.pbxproj b/Fixtures/TestProject/TestProject.xcodeproj/project.pbxproj index 9afe5e527..0d7bab288 100644 --- a/Fixtures/TestProject/TestProject.xcodeproj/project.pbxproj +++ b/Fixtures/TestProject/TestProject.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ CCA4B6071F1FC00500DF34A1 /* Frameworks */, CCA4B6081F1FC00500DF34A1 /* Resources */, CCA71FF01F225E4C00F772C1 /* Embed Frameworks */, + CC2DB89A1F30E29600B4B0FA /* Swiftlint */, ); buildRules = ( ); @@ -157,6 +158,7 @@ CCA71FE41F225E4C00F772C1 /* Frameworks */, CCA71FE51F225E4C00F772C1 /* Headers */, CCA71FE61F225E4C00F772C1 /* Resources */, + CC2DB89B1F30E2AC00B4B0FA /* Swiftlint */, ); buildRules = ( ); @@ -229,6 +231,37 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + CC2DB89A1F30E29600B4B0FA /* Swiftlint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; + CC2DB89B1F30E2AC00B4B0FA /* Swiftlint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = Swiftlint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ CCA4B6061F1FC00500DF34A1 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -475,6 +508,7 @@ CCA71FF31F225E4C00F772C1 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; diff --git a/Fixtures/TestProject/scripts/swiftlint.sh b/Fixtures/TestProject/scripts/swiftlint.sh new file mode 100644 index 000000000..ee2d21783 --- /dev/null +++ b/Fixtures/TestProject/scripts/swiftlint.sh @@ -0,0 +1,5 @@ +if which swiftlint >/dev/null; then + swiftlint +else + echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" +fi diff --git a/Fixtures/TestProject/spec.yml b/Fixtures/TestProject/spec.yml index d3af7ff2a..12b57d374 100644 --- a/Fixtures/TestProject/spec.yml +++ b/Fixtures/TestProject/spec.yml @@ -9,9 +9,20 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: com.test dependencies: - target: MyFramework + postbuildScripts: + - name: Swiftlint + script: | + if which swiftlint >/dev/null; then + swiftlint + else + echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" + fi - name: MyFramework type: framework platform: iOS sources: MyFramework settings: INFOPLIST_FILE: MyFramework/Info.plist + postbuildScripts: + - name: Swiftlint + path: scripts/swiftlint.sh diff --git a/Sources/ProjectSpec/RunScript.swift b/Sources/ProjectSpec/RunScript.swift new file mode 100644 index 000000000..84d781047 --- /dev/null +++ b/Sources/ProjectSpec/RunScript.swift @@ -0,0 +1,66 @@ +// +// RunScript.swift +// XcodeGen +// +// Created by Yonas Kolb on 1/8/17. +// +// + +import Foundation +import JSONUtilities + +public struct RunScript: Equatable { + + public var script: ScriptType + public var name: String? + public var shell: String? + public var inputFiles: [String] + public var outputFiles: [String] + + public enum ScriptType: Equatable { + case path(String) + case script(String) + + public static func ==(lhs: ScriptType, rhs: ScriptType) -> Bool { + switch (lhs, rhs) { + case let (.path(lhs), .path(rhs)): return lhs == rhs + case let (.script(lhs), .script(rhs)): return lhs == rhs + default: return false + } + } + } + + public init(script: ScriptType, name: String? = nil, inputFiles: [String] = [], outputFiles: [String] = [], shell: String? = nil) { + self.script = script + self.name = name + self.inputFiles = inputFiles + self.outputFiles = outputFiles + self.shell = shell + } + + public static func ==(lhs: RunScript, rhs: RunScript) -> Bool { + return lhs.script == rhs.script && + lhs.name == rhs.name && + lhs.script == rhs.script && + lhs.inputFiles == rhs.inputFiles && + lhs.outputFiles == rhs.outputFiles && + lhs.shell == rhs.shell + } +} + +extension RunScript: JSONObjectConvertible { + + public init(jsonDictionary: JSONDictionary) throws { + name = jsonDictionary.json(atKeyPath: "name") + inputFiles = jsonDictionary.json(atKeyPath: "inputFiles") ?? [] + outputFiles = jsonDictionary.json(atKeyPath: "outputFiles") ?? [] + + if let string: String = jsonDictionary.json(atKeyPath: "script") { + script = .script(string) + } else { + let path: String = try jsonDictionary.json(atKeyPath: "path") + script = .path(path) + } + shell = jsonDictionary.json(atKeyPath: "shell") + } +} diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index eb4755e84..44e67314f 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -18,8 +18,8 @@ public struct Target { public var sources: [String] public var sourceExludes: [String] public var dependencies: [Dependency] - public var prebuildScripts: [String] - public var postbuildScripts: [String] + public var prebuildScripts: [RunScript] + public var postbuildScripts: [RunScript] public var configFiles: [String: String] public var generateSchemes: [String] @@ -31,7 +31,7 @@ public struct Target { return name } - public init(name: String, type: PBXProductType, platform: Platform, settings: Settings = .empty, configFiles: [String: String] = [:], sources: [String] = [], sourceExludes: [String] = [], dependencies: [Dependency] = [], prebuildScripts: [String] = [], postbuildScripts: [String] = [], generateSchemes: [String] = []) { + public init(name: String, type: PBXProductType, platform: Platform, settings: Settings = .empty, configFiles: [String: String] = [:], sources: [String] = [], sourceExludes: [String] = [], dependencies: [Dependency] = [], prebuildScripts: [RunScript] = [], postbuildScripts: [RunScript] = [], generateSchemes: [String] = []) { self.name = name self.type = type self.platform = platform diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index ef47ebc34..957ca443f 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -52,6 +52,7 @@ public class PBXProjGenerator { counter += 1 uuid = "\(classAcronym)\(stringID)\(String(format: "%02d", counter))" } while (uuids.contains(uuid)) + uuids.insert(uuid) return uuid } @@ -223,6 +224,32 @@ public class PBXProjGenerator { return Set(files.map { $0.buildFile.reference }) } + func getRunScript(runScript: RunScript) throws -> PBXShellScriptBuildPhase { + + let shellScript: String + switch runScript.script { + case let .path(path): + shellScript = try (basePath + path).read() + case let .script(script): + shellScript = script + } + let escapedScript = shellScript.replacingOccurrences(of: "\"", with: "\\\"").replacingOccurrences(of: "\n", with: "\\n") + let shellScriptPhase = PBXShellScriptBuildPhase( + reference: generateUUID(PBXShellScriptBuildPhase.self, String(describing: runScript.name) + shellScript), + files: [], + name: runScript.name ?? "Run Script", + inputPaths: Set(runScript.inputFiles), + outputPaths: Set(runScript.outputFiles), + shellPath: runScript.shell ?? "/bin/sh", + shellScript: escapedScript) + + objects.append(.pbxShellScriptBuildPhase(shellScriptPhase)) + buildPhases.append(shellScriptPhase.reference) + return shellScriptPhase + } + + _ = try target.prebuildScripts.map(getRunScript) + let sourcesBuildPhase = PBXSourcesBuildPhase(reference: generateUUID(PBXSourcesBuildPhase.self, target.name), files: getBuildFilesForPhase(.sources)) objects.append(.pbxSourcesBuildPhase(sourcesBuildPhase)) buildPhases.append(sourcesBuildPhase.reference) @@ -268,6 +295,8 @@ public class PBXProjGenerator { } } + _ = try target.postbuildScripts.map(getRunScript) + let nativeTarget = PBXNativeTarget( reference: targetNativeReferences[target.name]!, buildConfigurationList: buildConfigList.reference, diff --git a/Sources/XcodeGenKit/ProjectGenerator.swift b/Sources/XcodeGenKit/ProjectGenerator.swift index 59c4587b6..72b7e82a2 100644 --- a/Sources/XcodeGenKit/ProjectGenerator.swift +++ b/Sources/XcodeGenKit/ProjectGenerator.swift @@ -33,7 +33,7 @@ public class ProjectGenerator { return spec.configs.first { $0.type == .release }! } - func validate() throws { + public func validate() throws { if spec.configs.isEmpty { spec.configs = [Config(name: "Debug", type: .debug), Config(name: "Release", type: .release)] @@ -86,6 +86,16 @@ public class ProjectGenerator { } } + let scripts = target.prebuildScripts + target.postbuildScripts + for script in scripts { + if case let .path(pathString) = script.script { + let scriptPath = path + pathString + if !scriptPath.exists { + errors.append(.invalidRunScriptPath(target: target.name, path: pathString)) + } + } + } + errors += validateSettings(target.settings) } @@ -208,6 +218,7 @@ public struct SpecValidationError: Error, CustomStringConvertible { case invalidSettingsPreset(String) case missingTargetSource(target: String, source: String) case invalidTargetGeneratedSchema(target: String, scheme: String, configType: ConfigType) + case invalidRunScriptPath(target: String, path: String) public var description: String { switch self { @@ -217,6 +228,7 @@ public struct SpecValidationError: Error, CustomStringConvertible { case let .invalidBuildSettingConfig(config): return "Build setting has invalid build configuration \(config.quoted)" case let .missingTargetSource(target, source): return "Target \(target.quoted) has a missing source directory \(source.quoted)" case let .invalidSettingsPreset(preset): return "Invalid settings preset \(preset.quoted)" + case let .invalidRunScriptPath(target, path): return "Target \(target.quoted) has a script path that doesn't exist \(path.quoted)" case let .invalidTargetGeneratedSchema(target, scheme, configType): return "Target \(target.quoted) has an invalid schema generation name which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(scheme.quoted)" } } diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index ca558b554..b007f36f1 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -24,6 +24,8 @@ func projectGeneratorTests() { let framework = Target(name: "MyFramework", type: .framework, platform: .iOS, settings: Settings(buildSettings: BuildSettings(dictionary: ["SETTING_2": "VALUE"]))) + let targets = [application, framework] + $0.describe("Config") { $0.it("generates config defaults") { @@ -74,7 +76,7 @@ func projectGeneratorTests() { $0.describe("Targets") { - let spec = ProjectSpec(name: "test", targets: [application, framework]) + let spec = ProjectSpec(name: "test", targets: targets) $0.it("generates targets") { let pbxProject = try getPbxProj(spec) @@ -91,6 +93,33 @@ func projectGeneratorTests() { try expect(dependencies.count) == 1 try expect(dependencies.first!.target) == nativeTargets.first { $0.name == framework.name }!.reference } + + $0.it("generates dependencies") { + let pbxProject = try getPbxProj(spec) + let nativeTargets = pbxProject.objects.nativeTargets + let dependencies = pbxProject.objects.targetDependencies + try expect(dependencies.count) == 1 + try expect(dependencies.first!.target) == nativeTargets.first { $0.name == framework.name }!.reference + } + + $0.it("generates run scripts") { + var scriptSpec = spec + scriptSpec.targets[0].prebuildScripts = [RunScript(script: .script("script1"))] + scriptSpec.targets[0].postbuildScripts = [RunScript(script: .script("script2"))] + let pbxProject = try getPbxProj(scriptSpec) + + guard let buildPhases = pbxProject.objects.nativeTargets.first?.buildPhases else { throw failure("Build phases not found") } + + let scripts = pbxProject.objects.shellScriptBuildPhases + let script1 = scripts[0] + let script2 = scripts[1] + try expect(scripts.count) == 2 + try expect(buildPhases.first) == script1.reference + try expect(buildPhases.last) == script2.reference + + try expect(script1.shellScript) == "script1" + try expect(script2.shellScript) == "script2" + } } $0.describe("Schemes") { diff --git a/Tests/XcodeGenKitTests/SpecLoadingTests.swift b/Tests/XcodeGenKitTests/SpecLoadingTests.swift index f56321065..7cacfb156 100644 --- a/Tests/XcodeGenKitTests/SpecLoadingTests.swift +++ b/Tests/XcodeGenKitTests/SpecLoadingTests.swift @@ -100,5 +100,24 @@ func specLoadingTests() { try expect(spec.settingPresets["preset7"]) == preset7 try expect(spec.settingPresets["preset8"]) == preset8 } + + $0.it("parses run scripts") { + var target = validTarget + let scripts: [[String: Any]] = [ + ["path": "script.sh"], + ["script": "shell script\ndo thing", "name": "myscript", "inputFiles": ["file","file2"], "outputFiles": ["file","file2"], "shell": "bin/customshell"], + ] + target["prebuildScripts"] = scripts + target["postbuildScripts"] = scripts + + let expectedScripts = [ + RunScript(script: .path("script.sh")), + RunScript(script: .script("shell script\ndo thing"), name: "myscript", inputFiles: ["file","file2"], outputFiles: ["file","file2"], shell: "bin/customshell"), + ] + + let parsedTarget = try Target(jsonDictionary: target) + try expect(parsedTarget.prebuildScripts) == expectedScripts + try expect(parsedTarget.postbuildScripts) == expectedScripts + } } } diff --git a/docs/ProjectSpec.md b/docs/ProjectSpec.md index 9d1ab65f2..4ae0e3702 100644 --- a/docs/ProjectSpec.md +++ b/docs/ProjectSpec.md @@ -146,8 +146,42 @@ targets: Release: config_files/release.xcconfig ``` +#### Run Scripts +Run script phases can be added via **prebuildScripts** or **postBuildScripts** which run before or after any other build phases respectively. They run in the order defined. Each script can contain + +- **path:** a relative or absolute path to a shell script +- **script:** an inline shell script +- **name:** optional name of a script. Defaults to `Run Script` +- **inputFiles:** optional list of input files +- **outputFiles:** optional list of output files +- **shell:** optional shell used for the script. Defaults to `/bin/sh` + +Either a **path** or **script** must be defined, the rest are optional. + +A multiline script can be written using the various YAML multiline methods, for example with `|` as below: + +```yaml +targets: + - name: MyTarget + prebuildScripts: + - path: myscripts/my_script.sh + name: My Script + inputFiles: + - $(SRCROOT)/file1 + - $(SRCROOT)/file2 + outputFiles: + - $(DERIVED_FILE_DIR)/file1 + - $(DERIVED_FILE_DIR)/file2 + postbuildScripts: + - script: swiftlint + name: Swiftlint + - script: | + command do + othercommand +``` + #### generateSchemes -This is a conveniance used to automatically generate schemes for a target based on large amount of configs. A list of names is provided, then for each of these names a scheme is created, using configs that contain the name with debug and release variants. This is useful for having different environment schemes. +This is a convenience used to automatically generate schemes for a target based on large amount of configs. A list of names is provided, then for each of these names a scheme is created, using configs that contain the name with debug and release variants. This is useful for having different environment schemes. For example, the following spec would create 3 schemes called: