import Foundation import ProjectSpec import XcodeProj import PathKit private func suitableConfig(for type: ConfigType, in project: Project) -> Config { if let defaultConfig = Config.defaultConfigs.first(where: { $0.type == type }), project.configs.contains(defaultConfig) { return defaultConfig } return project.configs.first { $0.type == type }! } public class SchemeGenerator { let project: Project let pbxProj: PBXProj var defaultDebugConfig: Config { suitableConfig(for: .debug, in: project) } var defaultReleaseConfig: Config { suitableConfig(for: .release, in: project) } public init(project: Project, pbxProj: PBXProj) { self.project = project self.pbxProj = pbxProj } private var projects: [ProjectReference: PBXProj] = [:] func getPBXProj(from reference: ProjectReference) throws -> PBXProj { if let cachedProject = projects[reference] { return cachedProject } let pbxproj = try XcodeProj(path: project.basePath + Path(reference.path)).pbxproj projects[reference] = pbxproj return pbxproj } public func generateSchemes() throws -> [XCScheme] { var xcschemes: [XCScheme] = [] for scheme in project.schemes { let xcscheme = try generateScheme(scheme) xcschemes.append(xcscheme) } for target in project.targets { if let targetScheme = target.scheme { if targetScheme.configVariants.isEmpty { let schemeName = target.name let debugConfig = suitableConfig(for: .debug, in: project) let releaseConfig = suitableConfig(for: .release, in: project) let scheme = Scheme( name: schemeName, target: target, targetScheme: targetScheme, project: project, debugConfig: debugConfig.name, releaseConfig: releaseConfig.name ) let xcscheme = try generateScheme(scheme, for: target) xcschemes.append(xcscheme) } else { for configVariant in targetScheme.configVariants { let schemeName = "\(target.name) \(configVariant)" let debugConfig = project.configs .first(including: configVariant, for: .debug)! let releaseConfig = project.configs .first(including: configVariant, for: .release)! let scheme = Scheme( name: schemeName, target: target, targetScheme: targetScheme, project: project, debugConfig: debugConfig.name, releaseConfig: releaseConfig.name ) let xcscheme = try generateScheme(scheme, for: target) xcschemes.append(xcscheme) } } } } return xcschemes } public func generateScheme(_ scheme: Scheme, for target: Target? = nil) throws -> XCScheme { func getBuildableReference(_ target: TargetReference) throws -> XCScheme.BuildableReference { let pbxProj: PBXProj let projectFilePath: String switch target.location { case .project(let project): guard let projectReference = self.project.getProjectReference(project) else { throw SchemeGenerationError.missingProject(project) } pbxProj = try getPBXProj(from: projectReference) projectFilePath = projectReference.path case .local: pbxProj = self.pbxProj projectFilePath = "\(self.project.name).xcodeproj" } guard let pbxTarget = pbxProj.targets(named: target.name).first else { throw SchemeGenerationError.missingTarget(target, projectPath: projectFilePath) } let buildableName: String switch target.location { case .project: buildableName = pbxTarget.productNameWithExtension() ?? pbxTarget.name case .local: guard let _buildableName = project.getTarget(target.name)?.filename ?? project.getAggregateTarget(target.name)?.name else { fatalError("Unable to determinate \"buildableName\" for build target: \(target)") } buildableName = _buildableName } return XCScheme.BuildableReference( referencedContainer: "container:\(projectFilePath)", blueprint: pbxTarget, buildableName: buildableName, blueprintName: target.name ) } func getBuildEntry(_ buildTarget: Scheme.BuildTarget) throws -> XCScheme.BuildAction.Entry { let buildableReference = try getBuildableReference(buildTarget.target) return XCScheme.BuildAction.Entry(buildableReference: buildableReference, buildFor: buildTarget.buildTypes) } let testTargets = scheme.test?.targets ?? [] let testBuildTargets = testTargets.map { Scheme.BuildTarget(target: $0.targetReference, buildTypes: BuildType.testOnly) } let testBuildTargetEntries = try testBuildTargets.map(getBuildEntry) let buildActionEntries: [XCScheme.BuildAction.Entry] = try 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 (buildActionEntries + testBuildTargetEntries) .first { settingsTarget == $0.buildableReference.blueprintName }? .buildableReference } return XCScheme.ExecutionAction(scriptText: action.script, title: action.name, environmentBuildable: environmentBuildable) } let schemeTarget: Target? if let targetName = scheme.run?.executable { schemeTarget = project.getTarget(targetName) } else { guard let firstTarget = scheme.build.targets.first else { throw SchemeGenerationError.missingBuildTargets(scheme.name) } let name = scheme.build.targets.first { $0.buildTypes.contains(.running) }?.target.name ?? firstTarget.target.name schemeTarget = target ?? project.getTarget(name) } let shouldExecuteOnLaunch = schemeTarget?.shouldExecuteOnLaunch == true let buildableReference = buildActionEntries.first(where: { $0.buildableReference.blueprintName == schemeTarget?.name })?.buildableReference ?? buildActionEntries.first!.buildableReference let runnables = makeProductRunnables(for: schemeTarget, buildableReference: buildableReference) let buildAction = XCScheme.BuildAction( buildActionEntries: buildActionEntries, preActions: scheme.build.preActions.map(getExecutionAction), postActions: scheme.build.postActions.map(getExecutionAction), parallelizeBuild: scheme.build.parallelizeBuild, buildImplicitDependencies: scheme.build.buildImplicitDependencies, runPostActionsOnFailure: scheme.build.runPostActionsOnFailure ) let testables = zip(testTargets, testBuildTargetEntries).map { testTarget, testBuilEntries in XCScheme.TestableReference( skipped: testTarget.skipped, parallelizable: testTarget.parallelizable, randomExecutionOrdering: testTarget.randomExecutionOrder, buildableReference: testBuilEntries.buildableReference, skippedTests: testTarget.skippedTests.map(XCScheme.TestItem.init), selectedTests: testTarget.selectedTests.map(XCScheme.TestItem.init), useTestSelectionWhitelist: !testTarget.selectedTests.isEmpty ? true : nil ) } let coverageBuildableTargets = try scheme.test?.coverageTargets.map { try getBuildableReference($0) } ?? [] let testCommandLineArgs = scheme.test.map { XCScheme.CommandLineArguments($0.commandLineArguments) } let launchCommandLineArgs = scheme.run.map { XCScheme.CommandLineArguments($0.commandLineArguments) } let profileCommandLineArgs = scheme.profile.map { XCScheme.CommandLineArguments($0.commandLineArguments) } let testVariables = scheme.test.flatMap { $0.environmentVariables.isEmpty ? nil : $0.environmentVariables } let launchVariables = scheme.run.flatMap { $0.environmentVariables.isEmpty ? nil : $0.environmentVariables } let profileVariables = scheme.profile.flatMap { $0.environmentVariables.isEmpty ? nil : $0.environmentVariables } let testAction = XCScheme.TestAction( buildConfiguration: scheme.test?.config ?? defaultDebugConfig.name, macroExpansion: buildableReference, testables: testables, preActions: scheme.test?.preActions.map(getExecutionAction) ?? [], postActions: scheme.test?.postActions.map(getExecutionAction) ?? [], selectedDebuggerIdentifier: (scheme.test?.debugEnabled ?? Scheme.Test.debugEnabledDefault) ? XCScheme.defaultDebugger : "", selectedLauncherIdentifier: (scheme.test?.debugEnabled ?? Scheme.Test.debugEnabledDefault) ? XCScheme.defaultLauncher : "Xcode.IDEFoundation.Launcher.PosixSpawn", shouldUseLaunchSchemeArgsEnv: scheme.test?.shouldUseLaunchSchemeArgsEnv ?? true, codeCoverageEnabled: scheme.test?.gatherCoverageData ?? Scheme.Test.gatherCoverageDataDefault, codeCoverageTargets: coverageBuildableTargets, onlyGenerateCoverageForSpecifiedTargets: !coverageBuildableTargets.isEmpty, disableMainThreadChecker: scheme.test?.disableMainThreadChecker ?? Scheme.Test.disableMainThreadCheckerDefault, commandlineArguments: testCommandLineArgs, environmentVariables: testVariables, language: scheme.test?.language, region: scheme.test?.region, systemAttachmentLifetime: scheme.test?.systemAttachmentLifetime, customLLDBInitFile: scheme.test?.customLLDBInit ) let allowLocationSimulation = scheme.run?.simulateLocation?.allow ?? true var locationScenarioReference: XCScheme.LocationScenarioReference? if let simulateLocation = scheme.run?.simulateLocation, var identifier = simulateLocation.defaultLocation, let referenceType = simulateLocation.referenceType { if referenceType == .gpx { var path = Path(components: [project.options.schemePathPrefix, identifier]) path = path.simplifyingParentDirectoryReferences() identifier = path.string } locationScenarioReference = XCScheme.LocationScenarioReference(identifier: identifier, referenceType: referenceType.rawValue) } var storeKitConfigurationFileReference: XCScheme.StoreKitConfigurationFileReference? if let storeKitConfiguration = scheme.run?.storeKitConfiguration { let storeKitConfigurationPath = Path(components: [project.options.schemePathPrefix, storeKitConfiguration]).simplifyingParentDirectoryReferences() storeKitConfigurationFileReference = XCScheme.StoreKitConfigurationFileReference(identifier: storeKitConfigurationPath.string) } let macroExpansion: XCScheme.BuildableReference? if let macroExpansionName = scheme.run?.macroExpansion, let resolvedMacroExpansion = buildActionEntries.first(where: { $0.buildableReference.blueprintName == macroExpansionName })?.buildableReference { macroExpansion = resolvedMacroExpansion } else { macroExpansion = shouldExecuteOnLaunch ? nil : buildableReference } let launchAction = XCScheme.LaunchAction( runnable: shouldExecuteOnLaunch ? runnables.launch : nil, buildConfiguration: scheme.run?.config ?? defaultDebugConfig.name, preActions: scheme.run?.preActions.map(getExecutionAction) ?? [], postActions: scheme.run?.postActions.map(getExecutionAction) ?? [], macroExpansion: macroExpansion, selectedDebuggerIdentifier: selectedDebuggerIdentifier(for: schemeTarget, run: scheme.run), selectedLauncherIdentifier: selectedLauncherIdentifier(for: schemeTarget, run: scheme.run), askForAppToLaunch: scheme.run?.askForAppToLaunch, allowLocationSimulation: allowLocationSimulation, locationScenarioReference: locationScenarioReference, disableMainThreadChecker: scheme.run?.disableMainThreadChecker ?? Scheme.Run.disableMainThreadCheckerDefault, stopOnEveryMainThreadCheckerIssue: scheme.run?.stopOnEveryMainThreadCheckerIssue ?? Scheme.Run.stopOnEveryMainThreadCheckerIssueDefault, commandlineArguments: launchCommandLineArgs, environmentVariables: launchVariables, language: scheme.run?.language, region: scheme.run?.region, launchAutomaticallySubstyle: scheme.run?.launchAutomaticallySubstyle ?? launchAutomaticallySubstyle(for: schemeTarget), storeKitConfigurationFileReference: storeKitConfigurationFileReference, customLLDBInitFile: scheme.run?.customLLDBInit ) let profileAction = XCScheme.ProfileAction( buildableProductRunnable: runnables.profile, buildConfiguration: scheme.profile?.config ?? defaultReleaseConfig.name, preActions: scheme.profile?.preActions.map(getExecutionAction) ?? [], postActions: scheme.profile?.postActions.map(getExecutionAction) ?? [], shouldUseLaunchSchemeArgsEnv: scheme.profile?.shouldUseLaunchSchemeArgsEnv ?? true, askForAppToLaunch: scheme.profile?.askForAppToLaunch, commandlineArguments: profileCommandLineArgs, environmentVariables: profileVariables ) let analyzeAction = XCScheme.AnalyzeAction(buildConfiguration: scheme.analyze?.config ?? defaultDebugConfig.name) let archiveAction = XCScheme.ArchiveAction( buildConfiguration: scheme.archive?.config ?? defaultReleaseConfig.name, revealArchiveInOrganizer: scheme.archive?.revealArchiveInOrganizer ?? true, customArchiveName: scheme.archive?.customArchiveName, preActions: scheme.archive?.preActions.map(getExecutionAction) ?? [], postActions: scheme.archive?.postActions.map(getExecutionAction) ?? [] ) let lastUpgradeVersion = project.attributes["LastUpgradeCheck"] as? String ?? project.xcodeVersion return XCScheme( name: scheme.name, lastUpgradeVersion: lastUpgradeVersion, version: project.schemeVersion, buildAction: buildAction, testAction: testAction, launchAction: launchAction, profileAction: profileAction, analyzeAction: analyzeAction, archiveAction: archiveAction, wasCreatedForAppExtension: schemeTarget .flatMap { $0.type.isExtension ? true : nil } ) } private func launchAutomaticallySubstyle(for target: Target?) -> String? { if target?.type.isExtension == true { return "2" } else { return nil } } private func makeProductRunnables(for target: Target?, buildableReference: XCScheme.BuildableReference) -> (launch: XCScheme.Runnable, profile: XCScheme.BuildableProductRunnable) { let buildable = XCScheme.BuildableProductRunnable(buildableReference: buildableReference) if target?.type.isWatchApp == true { let remote = XCScheme.RemoteRunnable( buildableReference: buildableReference, bundleIdentifier: "com.apple.Carousel", runnableDebuggingMode: "2" ) return (remote, buildable) } else { return (buildable, buildable) } } private func selectedDebuggerIdentifier(for target: Target?, run: Scheme.Run?) -> String { if target?.type.canUseDebugLauncher != false && run?.debugEnabled ?? Scheme.Run.debugEnabledDefault { return XCScheme.defaultDebugger } else { return "" } } private func selectedLauncherIdentifier(for target: Target?, run: Scheme.Run?) -> String { if target?.type.canUseDebugLauncher != false && run?.debugEnabled ?? Scheme.Run.debugEnabledDefault { return XCScheme.defaultLauncher } else { return "Xcode.IDEFoundation.Launcher.PosixSpawn" } } } enum SchemeGenerationError: Error, CustomStringConvertible { case missingTarget(TargetReference, projectPath: String) case missingProject(String) case missingBuildTargets(String) var description: String { switch self { case .missingTarget(let target, let projectPath): return "Unable to find target named \"\(target)\" in \"\(projectPath)\"" case .missingProject(let project): return "Unable to find project reference named \"\(project)\" in project.yml" case .missingBuildTargets(let name): return "Unable to find at least one build target in scheme \"\(name)\"" } } } extension Scheme { public init(name: String, target: Target, targetScheme: TargetScheme, project: Project, debugConfig: String, releaseConfig: String) { self.init( name: name, build: .init( targets: Scheme.buildTargets(for: target, project: project), buildImplicitDependencies: targetScheme.buildImplicitDependencies, preActions: targetScheme.preActions, postActions: targetScheme.postActions ), run: .init( config: debugConfig, commandLineArguments: targetScheme.commandLineArguments, environmentVariables: targetScheme.environmentVariables, disableMainThreadChecker: targetScheme.disableMainThreadChecker, stopOnEveryMainThreadCheckerIssue: targetScheme.stopOnEveryMainThreadCheckerIssue, language: targetScheme.language, region: targetScheme.region, storeKitConfiguration: targetScheme.storeKitConfiguration ), test: .init( config: debugConfig, gatherCoverageData: targetScheme.gatherCoverageData, disableMainThreadChecker: targetScheme.disableMainThreadChecker, commandLineArguments: targetScheme.commandLineArguments, targets: targetScheme.testTargets, environmentVariables: targetScheme.environmentVariables, language: targetScheme.language, region: targetScheme.region ), profile: .init( config: releaseConfig, commandLineArguments: targetScheme.commandLineArguments, environmentVariables: targetScheme.environmentVariables ), analyze: .init( config: debugConfig ), archive: .init( config: releaseConfig ) ) } private static func buildTargets(for target: Target, project: Project) -> [BuildTarget] { let buildTarget = Scheme.BuildTarget(target: TargetReference.local(target.name)) switch target.type { case .watchApp, .watch2App: let hostTarget = project.targets .first { projectTarget in projectTarget.dependencies.contains { $0.reference == target.name } } .map { BuildTarget(target: TargetReference.local($0.name)) } return hostTarget.map { [buildTarget, $0] } ?? [buildTarget] default: return [buildTarget] } } } extension PBXProductType { var canUseDebugLauncher: Bool { // Extensions don't use the lldb launcher return !isExtension } var isWatchApp: Bool { switch self { case .watchApp, .watch2App: return true default: return false } } } extension Scheme.Test { var systemAttachmentLifetime: XCScheme.TestAction.AttachmentLifetime? { switch (captureScreenshotsAutomatically, deleteScreenshotsWhenEachTestSucceeds) { case (false, _): return .keepNever case (true, false): return .keepAlways case (true, true): return nil } } }