diff --git a/tools/BUILD b/tools/BUILD index e0a48b9d76..5bb0fb64e3 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -9,6 +9,7 @@ _TOOLS = { "pbxproj_prefix": "//tools/generators/pbxproj_prefix", "pbxtargetdependencies": "//tools/generators/pbxtargetdependencies", "swiftc_stub": "//tools/swiftc_stub:swiftc", + "xcschemes": "//tools/generators/xcschemes", } _TESTS = { @@ -24,6 +25,11 @@ _TESTS = { "//tools/generators/lib/PBXProj:PBXProjTests", "//tools/generators/pbxtargetdependencies:pbxtargetdependencies_tests", ], + "xcschemes": [ + "//tools/generators/lib/PBXProj:PBXProjTests", + "//tools/generators/lib/XCScheme:XCSchemeTests", + "//tools/generators/xcschemes:xcschemes_tests", + ], } _SCHEME_DIAGNOSTICS = xcode_schemes.diagnostics( @@ -191,6 +197,48 @@ _SCHEMES = [ build_configuration = "Release", ), ), + xcode_schemes.scheme( + name = "xcschemes", + launch_action = xcode_schemes.launch_action( + _TOOLS["xcschemes"], + args = [ + # outputDirectory + "/tmp/pbxproj_partials/xcschemes", + # schemeManagementOutputPath + "/tmp/pbxproj_partials/xcschememanagement.plist", + # autogenerationMode + "auto", + # defaultXcodeConfiguration + "Debug", + # workspace + "/tmp/workspace", + # installPath + "some/project.xcodeproj", + # extensionPointIdentifiersFile + "bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_extension_point_identifiers", + # executionActionsFile + "bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_pbxproj_partials/execution_actions_file", + # targetsArgsEnvFile + "bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_pbxproj_partials/targets_args_env", + # customSchemesFile + "bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_pbxproj_partials/custom_schemes_file", + # consolidationMaps + "--consolidation-maps", + "/tmp/pbxproj_partials/consolidation_maps/0", + "/tmp/pbxproj_partials/consolidation_maps/1", + # targetAndExtensionHosts + ], + diagnostics = _SCHEME_DIAGNOSTICS, + ), + profile_action = xcode_schemes.profile_action( + _TOOLS["xcschemes"], + build_configuration = "Release", + ), + test_action = xcode_schemes.test_action( + _TESTS["xcschemes"], + diagnostics = _SCHEME_DIAGNOSTICS, + ), + ), ] _XCODE_CONFIGURATIONS = { @@ -224,6 +272,9 @@ xcodeproj( "//tools/generators/pbxtargetdependencies": [ "//tools/generators/pbxtargetdependencies:README.md", ], + "//tools/generators/xcschemes": [ + "//tools/generators/xcschemes:README.md", + ], }, extra_files = [ "//tools/generators:README.md", diff --git a/tools/generators/BUILD b/tools/generators/BUILD index 2a64d5586b..7b4045d0ed 100644 --- a/tools/generators/BUILD +++ b/tools/generators/BUILD @@ -12,6 +12,7 @@ filegroup( # "//" + package_name() + "/pbxproj_prefix:release_files", # "//" + package_name() + "/pbxtargetdependencies:release_files", # "//" + package_name() + "/selected_model_versions:release_files", + # "//" + package_name() + "/xcschemes:release_files", ], tags = ["manual"], visibility = ["//:__subpackages__"], diff --git a/tools/generators/README.md b/tools/generators/README.md index 1118c475fe..b84954b8d7 100644 --- a/tools/generators/README.md +++ b/tools/generators/README.md @@ -55,7 +55,6 @@ information. - `XCBuildConfiguration` - `XCBuildConfigurationList` - and various build phases - - Creates automatic `.xcscheme`s - [`files_and_groups`](files_and_groups/README.md): - Creates three files: - A partial containing the `PBXProject.knownRegions` property @@ -68,7 +67,6 @@ information. ## Xcode schemes -[Automatic schemes](docs/bazel.md#xcodeproj-scheme_autogeneration_mode) are -generated by one of the `PBXProj` partial generators (`pbxnativetargets`). -[Custom schemes](docs/bazel.md#xcodeproj-schemes) are generated by a separate -generator. +Both [automatic schemes](docs/bazel.md#xcodeproj-scheme_autogeneration_mode) and +[custom schemes](docs/bazel.md#xcodeproj-schemes) are generated by the +[`xcschemes`](xcschemes/README.md) generator. diff --git a/tools/generators/lib/XCScheme/src/CreateBuildAction.swift b/tools/generators/lib/XCScheme/src/CreateBuildAction.swift index 5856b8a41d..00d2d82900 100644 --- a/tools/generators/lib/XCScheme/src/CreateBuildAction.swift +++ b/tools/generators/lib/XCScheme/src/CreateBuildAction.swift @@ -30,14 +30,6 @@ public struct BuildActionEntry: Equatable { public static let profiling = Self(rawValue: 1 << 3) public static let archiving = Self(rawValue: 1 << 4) - public static let all: Self = [ - .analyzing, - .testing, - .running, - .profiling, - .archiving, - ] - public let rawValue: Int public init(rawValue: Int) { diff --git a/tools/generators/lib/XCScheme/src/CreateSchemeManagement.swift b/tools/generators/lib/XCScheme/src/CreateSchemeManagement.swift index dfc4bd5d2f..a61f1f084a 100644 --- a/tools/generators/lib/XCScheme/src/CreateSchemeManagement.swift +++ b/tools/generators/lib/XCScheme/src/CreateSchemeManagement.swift @@ -8,7 +8,7 @@ public struct CreateSchemeManagement { self.callable = callable } - /// Creates the XML for an `.xcscheme` file. + /// Creates the XML for an `xcschememanagement.plist` file. public func callAsFunction(schemeNames: [String]) -> String { return callable(/*schemeNames:*/ schemeNames) } diff --git a/tools/generators/xcschemes/BUILD b/tools/generators/xcschemes/BUILD new file mode 100644 index 0000000000..95c7cc8b99 --- /dev/null +++ b/tools/generators/xcschemes/BUILD @@ -0,0 +1,95 @@ +load("@build_bazel_rules_apple//apple:apple.bzl", "apple_universal_binary") +load( + "@build_bazel_rules_apple//apple:macos.bzl", + "macos_command_line_application", + "macos_unit_test", +) +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "swift_binary", + "swift_library", +) + +exports_files(["README.md"]) + +# Generator + +swift_library( + name = "xcschemes.library", + srcs = glob(["src/**/*.swift"]), + module_name = "xcschemes", + deps = [ + "//tools/generators/lib/PBXProj", + "//tools/generators/lib/XCScheme", + "//tools/lib/ToolCommon", + "@com_github_apple_swift_collections//:OrderedCollections", + ], +) + +# This target exists to keep configurations the same between the generator +# and the tests, which makes the Xcode development experience better. If we used +# `swift_binary` or `apple_universal_binary` in `xcodeproj`, then the +# `macos_unit_test` transition (which is used to be able to set a minimum os +# version on the tests) will create slightly different configurations for our +# `swift_library`s. Maybe https://github.com/bazelbuild/bazel/issues/6526 will +# fix that for us. +macos_command_line_application( + name = "xcschemes", + minimum_os_version = "13.0", + visibility = ["//visibility:public"], + deps = [":xcschemes.library"], +) + +swift_binary( + name = "xcschemes_binary", + deps = [":xcschemes.library"], +) + +apple_universal_binary( + name = "universal_xcschemes", + binary = ":xcschemes_binary", + forced_cpus = [ + "x86_64", + "arm64", + ], + minimum_os_version = "13.0", + platform_type = "macos", + visibility = ["//visibility:public"], +) + +# Tests + +swift_library( + name = "xcschemes_tests.library", + testonly = True, + srcs = glob(["test/**/*.swift"]), + module_name = "xcschemes_tests", + deps = [ + ":xcschemes.library", + "@com_github_pointfreeco_swift_custom_dump//:CustomDump", + ], +) + +macos_unit_test( + name = "xcschemes_tests", + minimum_os_version = "13.0", + visibility = [ + "//test:__subpackages__", + "@rules_xcodeproj//xcodeproj:generated", + ], + deps = [ + ":xcschemes_tests.library", + ], +) + +# Release + +filegroup( + name = "release_files", + srcs = [ + "BUILD.release.bazel", + ":universal_xcschemes", + ], + tags = ["manual"], + visibility = ["//:__subpackages__"], +) diff --git a/tools/generators/xcschemes/BUILD.release.bazel b/tools/generators/xcschemes/BUILD.release.bazel new file mode 100644 index 0000000000..8edd5de1e0 --- /dev/null +++ b/tools/generators/xcschemes/BUILD.release.bazel @@ -0,0 +1,8 @@ +load("@bazel_skylib//rules:native_binary.bzl", "native_binary") + +native_binary( + name = "universal_xcschemes", + src = "prebuilt_universal_xcschemes", + out = "universal_xcschemes", + visibility = ["//visibility:public"], +) diff --git a/tools/generators/xcschemes/README.md b/tools/generators/xcschemes/README.md new file mode 100644 index 0000000000..75ec1871d9 --- /dev/null +++ b/tools/generators/xcschemes/README.md @@ -0,0 +1,516 @@ +# `XCScheme`s generator + +The `xcschemes` generator generates `.xcscheme` files for a project. + +## Inputs + +The generator accepts the following command-line arguments (see +[`GeneratorArguments.swift`](src/Generator/GeneratorArguments.swift) and +[`XCSchemes.swift`](src/XCSchemes.swift) for more +details): + +- Positional `output-directory` +- Positional `scheme-management-output-path` +- Positional `autogeneration-mode` +- Positional `default-xcode-configuration` +- Positional `workspace` +- Positional `install-path` +- Positional `extension-point-identifiers-file` +- Positional `execution-actions-file` +- Positional `targets-args-env-file` +- Positional `custom-schemes-file` +- Positional `transitive-preview-targets-file` +- Option `--consolidation-maps ...` +- Optional option `--target-and-extension-hosts ...` +- Flag `--colorize` + +Here is an example invocation: + +```shell +$ xcschemes \ + /tmp/pbxproj_partials/xcschemes \ + /tmp/pbxproj_partials/xcschememanagement.plist \ + auto \ + Debug \ + /tmp/workspace \ + some/project.xcodeproj \ + bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_extension_point_identifiers \ + bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_pbxproj_partials/execution_actions_file \ + bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_pbxproj_partials/targets_args_env \ + bazel-output-base/rules_xcodeproj.noindex/build_output_base/execroot/_main/bazel-out/darwin_arm64-dbg/bin/external/_main~internal~rules_xcodeproj_generated/generator/tools/xcodeproj/xcodeproj_pbxproj_partials/custom_schemes_file \ + --consolidation-maps \ + /tmp/pbxproj_partials/consolidation_maps/0 \ + /tmp/pbxproj_partials/consolidation_maps/1 +``` + +## Output + +Here is an example output: + +### `generator.xcscheme` + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `xcschememanagement.plist` + +``` + + + + + SchemeUserState + + files_and_groups.xcscheme_^#shared#^_ + + isShown + + orderHint + 0 + + import_indexstores.xcscheme_^#shared#^_ + + isShown + + orderHint + 1 + + pbxnativetargets.xcscheme_^#shared#^_ + + isShown + + orderHint + 2 + + pbxproj_prefix.xcscheme_^#shared#^_ + + isShown + + orderHint + 3 + + pbxtargetdependencies.xcscheme_^#shared#^_ + + isShown + + orderHint + 4 + + swift_debug_settings.xcscheme_^#shared#^_ + + isShown + + orderHint + 5 + + swiftc_stub.xcscheme_^#shared#^_ + + isShown + + orderHint + 6 + + target_build_settings.xcscheme_^#shared#^_ + + isShown + + orderHint + 7 + + xcschemes.xcscheme_^#shared#^_ + + isShown + + orderHint + 8 + + + + + +``` diff --git a/tools/generators/xcschemes/src/Generator/AutogenerationMode.swift b/tools/generators/xcschemes/src/Generator/AutogenerationMode.swift new file mode 100644 index 0000000000..60abaf4862 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/AutogenerationMode.swift @@ -0,0 +1,13 @@ +import ArgumentParser + +/// How automatically generated schemes should be generated. +enum AutogenerationMode: String, ExpressibleByArgument { + /// If there are no custom schemes, then `.all`, otherwise, `.none`. + case auto + + /// Schemes for every target should be automatically generated. + case all + + /// No automatically generated schemes should be generated. + case none +} diff --git a/tools/generators/xcschemes/src/Generator/CalculateSchemeReferencedContainer.swift b/tools/generators/xcschemes/src/Generator/CalculateSchemeReferencedContainer.swift new file mode 100644 index 0000000000..ad377af5e0 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CalculateSchemeReferencedContainer.swift @@ -0,0 +1,42 @@ +import ToolCommon + +extension Generator { + struct CalculateSchemeReferencedContainer { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init(callable: @escaping Callable = Self.defaultCallable) { + self.callable = callable + } + + /// Calculates the `referencedContainer` attribute on `.xcscheme` + /// `BuildableReference` elements. + func callAsFunction( + installPath: String, + workspace: String + ) -> String { + return callable( + /*installPath:*/ installPath, + /*workspace:*/ workspace + ) + } + } +} + +// MARK: - CalculateSchemeReferencedContainer.Callable + +extension Generator.CalculateSchemeReferencedContainer { + typealias Callable = ( + _ installPath: String, + _ workspace: String + ) -> String + + static func defaultCallable( + installPath: String, + workspace: String + ) -> String { + return "container:\(workspace)/\(installPath)" + } +} diff --git a/tools/generators/xcschemes/src/Generator/CalculateTargetsByKey.swift b/tools/generators/xcschemes/src/Generator/CalculateTargetsByKey.swift new file mode 100644 index 0000000000..2946c6d170 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CalculateTargetsByKey.swift @@ -0,0 +1,58 @@ +import PBXProj + +extension Generator { + struct CalculateTargetsByKey { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init(callable: @escaping Callable = Self.defaultCallable) { + self.callable = callable + } + + /// Calculates dictionaries that map `Target.Key` or `TargetID` to + /// `Target`s. + func callAsFunction( + targets: [Target] + ) -> ( + targetsByKey: [Target.Key: Target], + targetsByID: [TargetID: Target] + ) { + return callable( + /*targets:*/ targets + ) + } + } +} + +// MARK: - CalculateTargetsByKey.Callable + +extension Generator.CalculateTargetsByKey { + typealias Callable = ( + _ targets: [Target] + ) -> ( + targetsByKey: [Target.Key: Target], + targetsByID: [TargetID: Target] + ) + + static func defaultCallable( + targets: [Target] + ) -> ( + targetsByKey: [Target.Key: Target], + targetsByID: [TargetID: Target] + ) { + return ( + targetsByKey: Dictionary( + uniqueKeysWithValues: targets.map { target in + return (target.key, target) + } + ), + targetsByID: Dictionary( + uniqueKeysWithValues: targets.flatMap { target in + return target.key.sortedIds.map { ($0, target) } + } + ) + ) + } +} diff --git a/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift b/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift new file mode 100644 index 0000000000..9fa6f2351b --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfo.swift @@ -0,0 +1,191 @@ +import PBXProj +import XCScheme + +extension Generator { + struct CreateAutomaticSchemeInfo { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + callable: @escaping Callable = Self.defaultCallable + ) { + self.callable = callable + } + + /// Creates a `SchemeInfo` for an automatically generated scheme. + func callAsFunction( + commandLineArguments: [CommandLineArgument], + customSchemeNames: Set, + environmentVariables: [EnvironmentVariable], + extensionHost: Target?, + target: Target, + transitivePreviewReferences: [BuildableReference] + ) throws -> SchemeInfo? { + return try callable( + /*commandLineArguments:*/ commandLineArguments, + /*customSchemeNames:*/ customSchemeNames, + /*environmentVariables:*/ environmentVariables, + /*extensionHost:*/ extensionHost, + /*target:*/ target, + /*transitivePreviewReferences:*/ transitivePreviewReferences + ) + } + } +} + +// MARK: - CreateAutomaticSchemeInfo.Callable + +extension Generator.CreateAutomaticSchemeInfo { + typealias Callable = ( + _ commandLineArguments: [CommandLineArgument], + _ customSchemeNames: Set, + _ environmentVariables: [EnvironmentVariable], + _ extensionHost: Target?, + _ target: Target, + _ transitivePreviewReferences: [BuildableReference] + ) throws -> SchemeInfo? + + static func defaultCallable( + commandLineArguments: [CommandLineArgument], + customSchemeNames: Set, + environmentVariables: [EnvironmentVariable], + extensionHost: Target?, + target: Target, + transitivePreviewReferences: [BuildableReference] + ) throws -> SchemeInfo? { + let baseSchemeName = target.buildableReference.blueprintName.schemeName + + guard !customSchemeNames.contains(baseSchemeName) else { + return nil + } + + let productType = target.productType + let isTest = productType.isTest + + let name: String + let launchTarget: SchemeInfo.LaunchTarget? + let buildTargets: [Target] + if let extensionHost { + name = """ +\(baseSchemeName) in \ +\(extensionHost.buildableReference.blueprintName.schemeName) +""" + + launchTarget = + .init(primary: target, extensionHost: extensionHost) + buildTargets = [] + } else { + name = baseSchemeName + + if productType.isLaunchable { + launchTarget = .init(primary: target, extensionHost: nil) + buildTargets = [] + } else { + launchTarget = nil + buildTargets = [target] + } + } + + let testCommandLineArguments: [CommandLineArgument] + let testEnvironmentVariables: [EnvironmentVariable] + let testUseRunArgsAndEnv: Bool + let runCommandLineArguments: [CommandLineArgument] + let runEnvironmentVariables: [EnvironmentVariable] + if isTest { + testUseRunArgsAndEnv = false + testCommandLineArguments = commandLineArguments + testEnvironmentVariables = + .defaultEnvironmentVariables + environmentVariables + + runCommandLineArguments = [] + runEnvironmentVariables = [] + } else { + testUseRunArgsAndEnv = true + testCommandLineArguments = [] + testEnvironmentVariables = [] + + runCommandLineArguments = commandLineArguments + runEnvironmentVariables = + .defaultEnvironmentVariables + environmentVariables + } + + return SchemeInfo( + name: name, + test: .init( + buildTargets: [], + commandLineArguments: testCommandLineArguments, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: testEnvironmentVariables, + testTargets: isTest ? + [.init(target: target, enabled: true)] : [], + useRunArgsAndEnv: testUseRunArgsAndEnv, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: buildTargets, + commandLineArguments: runCommandLineArguments, + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: runEnvironmentVariables, + launchTarget: launchTarget, + transitivePreviewReferences: transitivePreviewReferences, + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: launchTarget, + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + } +} + +private extension String { + var schemeName: String { + return replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: ":", with: "_") + } +} + +private extension PBXProductType { + var isLaunchable: Bool { + switch self { + case .application, + .messagesApplication, + .onDemandInstallCapableApplication, + .watch2App, + .watch2AppContainer, + .appExtension, + .intentsServiceExtension, + .messagesExtension, + .tvExtension, + .extensionKitExtension, + .xcodeExtension, + .driverExtension, + .systemExtension, + .commandLineTool, + .xpcService: + return true + default: + return false + } + } + + var isTest: Bool { + switch self { + case .unitTestBundle, .uiTestBundle: return true + default: return false + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfos.swift b/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfos.swift new file mode 100644 index 0000000000..4e1bc5f8dc --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CreateAutomaticSchemeInfos.swift @@ -0,0 +1,176 @@ +import Foundation +import PBXProj +import XCScheme + +extension Generator { + struct CreateAutomaticSchemeInfos { + private let createTargetAutomaticSchemeInfos: + CreateTargetAutomaticSchemeInfos + + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + createTargetAutomaticSchemeInfos: + CreateTargetAutomaticSchemeInfos, + callable: @escaping Callable = Self.defaultCallable + ) { + self.createTargetAutomaticSchemeInfos = + createTargetAutomaticSchemeInfos + + self.callable = callable + } + + /// Creates `SchemeInfo`s for automatically generated schemes. + func callAsFunction( + autogenerationMode: AutogenerationMode, + commandLineArguments: [TargetID: [CommandLineArgument]], + customSchemeNames: Set, + environmentVariables: [TargetID: [EnvironmentVariable]], + extensionHostIDs: [TargetID: [TargetID]], + targets: [Target], + targetsByID: [TargetID: Target], + targetsByKey: [Target.Key: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]] + ) throws -> [SchemeInfo] { + return try callable( + /*autogenerationMode:*/ autogenerationMode, + /*commandLineArguments:*/ commandLineArguments, + /*customSchemeNames:*/ customSchemeNames, + /*environmentVariables:*/ environmentVariables, + /*extensionHostIDs:*/ extensionHostIDs, + /*targets:*/ targets, + /*targetsByID:*/ targetsByID, + /*targetsByKey:*/ targetsByKey, + /*transitivePreviewReferences:*/ transitivePreviewReferences, + /*createTargetAutomaticSchemeInfos:*/ + createTargetAutomaticSchemeInfos + ) + } + } +} + +// MARK: - CreateAutomaticSchemeInfos.Callable + +extension Generator.CreateAutomaticSchemeInfos { + typealias Callable = ( + _ autogenerationMode: AutogenerationMode, + _ commandLineArguments: [TargetID: [CommandLineArgument]], + _ customSchemeNames: Set, + _ environmentVariables: [TargetID: [EnvironmentVariable]], + _ extensionHostIDs: [TargetID: [TargetID]], + _ targets: [Target], + _ targetsByID: [TargetID: Target], + _ targetsByKey: [Target.Key: Target], + _ transitivePreviewReferences: [TargetID: [BuildableReference]], + _ createTargetAutomaticSchemeInfos: + Generator.CreateTargetAutomaticSchemeInfos + ) throws -> [SchemeInfo] + + static func defaultCallable( + autogenerationMode: AutogenerationMode, + commandLineArguments: [TargetID: [CommandLineArgument]], + customSchemeNames: Set, + environmentVariables: [TargetID: [EnvironmentVariable]], + extensionHostIDs: [TargetID: [TargetID]], + targets: [Target], + targetsByID: [TargetID: Target], + targetsByKey: [Target.Key: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]], + createTargetAutomaticSchemeInfos: + Generator.CreateTargetAutomaticSchemeInfos + ) throws -> [SchemeInfo] { + let autogenerateSchemes: Bool + switch autogenerationMode { + case .all: + autogenerateSchemes = true + case .auto: + autogenerateSchemes = customSchemeNames.isEmpty + case .none: + autogenerateSchemes = false + } + + guard autogenerateSchemes else { + return [] + } + + return try targets + .filter { $0.productType.shouldCreateScheme } + // Sort targets so resulting `SchemeInfo` is properly sorted for + // `xcschememanagement.plist` + .sorted { lhs, rhs in + let lhsSortOrder = lhs.productType.sortOrder + let rhsSortOrder = rhs.productType.sortOrder + guard lhsSortOrder == rhsSortOrder else { + return lhsSortOrder < rhsSortOrder + } + + return lhs.buildableReference.blueprintName + .localizedStandardCompare( + rhs.buildableReference.blueprintName + ) == .orderedAscending + } + .flatMap { target -> [SchemeInfo] in + let id = target.key.sortedIds.first! + + return try createTargetAutomaticSchemeInfos( + commandLineArguments: commandLineArguments[id, default: []], + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables[id, default: []], + extensionHostIDs: extensionHostIDs, + target: target, + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: + transitivePreviewReferences + ) + } + } +} + +private extension PBXProductType { + var shouldCreateScheme: Bool { + switch self { + case .messagesApplication, .watch2AppContainer, .watch2Extension: + return false + default: + return true + } + } + + var sortOrder: Int { + switch self { + // Applications + case .application, + .commandLineTool, + .messagesApplication, + .onDemandInstallCapableApplication, + .watch2App, + .watch2AppContainer, + .xpcService: + return 0 + // App extensions + case .appExtension, + .driverExtension, + .extensionKitExtension, + .intentsServiceExtension, + .messagesExtension, + .stickerPack, + .systemExtension, + .tvExtension, + .watch2Extension, + .xcodeExtension: + return 1 + // Tests + case .ocUnitTestBundle, + .uiTestBundle, + .unitTestBundle: + return 2 + // Others + default: + return 3 + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift new file mode 100644 index 0000000000..290d0cdc87 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CreateCustomSchemeInfos.swift @@ -0,0 +1,732 @@ +import Foundation +import PBXProj +import ToolCommon +import XCScheme + +extension Generator { + struct CreateCustomSchemeInfos { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + callable: @escaping Callable = Self.defaultCallable + ) { + self.callable = callable + } + + /// Creates `SchemeInfo`s for custom schemes. + func callAsFunction( + commandLineArguments: [TargetID: [CommandLineArgument]], + customSchemesFile: URL, + environmentVariables: [TargetID: [EnvironmentVariable]], + executionActionsFile: URL, + extensionHostIDs: [TargetID: [TargetID]], + targetsByID: [TargetID: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]] + ) async throws -> [SchemeInfo] { + try await callable( + /*commandLineArguments:*/ commandLineArguments, + /*customSchemesFile:*/ customSchemesFile, + /*environmentVariables:*/ environmentVariables, + /*executionActionsFile:*/ executionActionsFile, + /*extensionHostIDs:*/ extensionHostIDs, + /*targetsByID:*/ targetsByID, + /*transitivePreviewReferences:*/ transitivePreviewReferences + ) + } + } +} + +// MARK: - CreateCustomSchemeInfos.Callable + +extension Generator.CreateCustomSchemeInfos { + typealias Callable = ( + _ commandLineArguments: [TargetID: [CommandLineArgument]], + _ customSchemesFile: URL, + _ environmentVariables: [TargetID: [EnvironmentVariable]], + _ executionActionsFile: URL, + _ extensionHostIDs: [TargetID: [TargetID]], + _ targetsByID: [TargetID: Target], + _ transitivePreviewReferences: [TargetID: [BuildableReference]] + ) async throws -> [SchemeInfo] + + static func defaultCallable( + commandLineArguments: [TargetID: [CommandLineArgument]], + customSchemesFile: URL, + environmentVariables: [TargetID: [EnvironmentVariable]], + executionActionsFile: URL, + extensionHostIDs: [TargetID: [TargetID]], + targetsByID: [TargetID: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]] + ) async throws -> [SchemeInfo] { + let executionActions: [String: [SchemeInfo.ExecutionAction]] = + try await .parse( + from: executionActionsFile, + targetsByID: targetsByID + ) + + var rawArgs = ArraySlice(try await customSchemesFile.allLines.collect()) + + let schemeCount = try rawArgs.consumeArg( + "scheme-count", + as: Int.self, + in: customSchemesFile + ) + + var schemeInfos: [SchemeInfo] = [] + for _ in (0.. = [] + + let test = try rawArgs.consumeArg( + as: SchemeInfo.Test.self, + in: customSchemesFile, + allTargetIDs: &allTargetIDs, + targetCommandLineArguments: commandLineArguments, + targetEnvironmentVariables: environmentVariables, + targetsByID: targetsByID + ) + + let run = try rawArgs.consumeArg( + as: SchemeInfo.Run.self, + in: customSchemesFile, + allTargetIDs: &allTargetIDs, + extensionHostIDs: extensionHostIDs, + name: name, + targetCommandLineArguments: commandLineArguments, + targetEnvironmentVariables: environmentVariables, + targetsByID: targetsByID, + transitivePreviewReferences: transitivePreviewReferences + ) + + let profile = try rawArgs.consumeArg( + as: SchemeInfo.Profile.self, + in: customSchemesFile, + allTargetIDs: &allTargetIDs, + extensionHostIDs: extensionHostIDs, + name: name, + targetCommandLineArguments: commandLineArguments, + targetEnvironmentVariables: environmentVariables, + targetsByID: targetsByID + ) + + schemeInfos.append( + SchemeInfo( + name: name, + test: test, + run: run, + profile: profile, + executionActions: executionActions[name, default: []] + ) + ) + } + + return schemeInfos + } +} + +private extension ArraySlice where Element == String { + // MARK: - CommandLineArgument + + mutating func consumeArgs( + _ namePrefix: String, + as type: CommandLineArgument.Type, + in url: URL, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> [CommandLineArgument]? { + let count = try consumeArg( + "\(namePrefix)-arg-count", + as: Int.self, + in: url, + file: file, + line: line + ) + guard count != -1 else { + return nil + } + + var commandLineArguments: [CommandLineArgument] = [] + for _ in (0.. [EnvironmentVariable]? { + let count = try consumeArg( + "\(namePrefix)-env-var-count", + as: Int.self, + in: url, + file: file, + line: line + ) + guard count != -1 else { + return nil + } + + var environmentVariables: [EnvironmentVariable] = [] + for _ in (0.., + context: @autoclosure () -> String, + commandLineArguments: [CommandLineArgument]?, + environmentVariables: [EnvironmentVariable]?, + extensionHostIDs: [TargetID: [TargetID]], + targetCommandLineArguments: [TargetID: [CommandLineArgument]], + targetEnvironmentVariables: [TargetID: [EnvironmentVariable]], + targetsByID: [TargetID: Target], + file: StaticString = #filePath, + line: UInt = #line + ) throws -> ( + SchemeInfo.LaunchTarget?, + [CommandLineArgument], + [EnvironmentVariable] + ) { + let id = try consumeArg( + "\(namePrefix)-launch-target-id", + as: TargetID?.self, + in: url, + file: file, + line: line + ) + let extensionHostID = try consumeArg( + "\(namePrefix)-extension-host", + as: TargetID?.self, + in: url, + file: file, + line: line + ) + + guard let id else { + return (nil, commandLineArguments ?? [], environmentVariables ?? []) + } + + allTargetIDs.insert(id) + + let target = try targetsByID.value( + for: id, + context: context() + ) + + guard !target.productType.needsExtensionHost || extensionHostID != nil + else { + throw UsageError(message: """ +\(context()) (\(id)) is an app extension and requires `extension_host` to be \ +set +""") + } + + let extensionHost = try extensionHostID.flatMap { extensionHostID in + guard extensionHostIDs[id, default: []] + .contains(where: { $0 == extensionHostID }) + else { + throw UsageError(message: """ +\(context()) `extension_host` (\(extensionHostID)) does not host the extension \ +(\(id)) +""") + } + return try targetsByID.value( + for: extensionHostID, + context: "\(context()) extension host" + ) + } + + // Only set from-rule args and env if the custom scheme sets them as + // `inherit` (which is represented as `nil` here) + let finalCommandLineArguments: [CommandLineArgument] + if let commandLineArguments { + finalCommandLineArguments = commandLineArguments + } else { + finalCommandLineArguments = + targetCommandLineArguments[id, default: []] + } + + let finalEnvironmentVariables: [EnvironmentVariable] + if let environmentVariables { + finalEnvironmentVariables = environmentVariables + } else { + finalEnvironmentVariables = + targetEnvironmentVariables[id, default: []] + } + + return ( + SchemeInfo.LaunchTarget( + primary: target, + extensionHost: extensionHost + ), + finalCommandLineArguments, + finalEnvironmentVariables + ) + } + + // MARK: - SchemeInfo.Profile + + mutating func consumeArg( + as type: SchemeInfo.Profile.Type, + in url: URL, + allTargetIDs: inout Set, + extensionHostIDs: [TargetID: [TargetID]], + name: String, + targetCommandLineArguments: [TargetID: [CommandLineArgument]], + targetEnvironmentVariables: [TargetID: [EnvironmentVariable]], + targetsByID: [TargetID: Target], + file: StaticString = #filePath, + line: UInt = #line + ) throws -> SchemeInfo.Profile { + let buildTargets = try consumeArgs( + "profile-build-targets", + as: Target.self, + in: url, + transform: { id in + return try targetsByID.value( + for: TargetID(id), + context: "Profile build target" + ) + } + ) + allTargetIDs + .formUnion(buildTargets.map(\.key.sortedIds.first!)) + + let specifiedCommandLineArguments = + try consumeArgs("profile", as: CommandLineArgument.self, in: url) + let specifiedEnvironmentVariables = + try consumeArgs("profile", as: EnvironmentVariable.self, in: url) + let environmentVariablesIncludeDefaults = try consumeArg( + "profile-include-default-env", + as: Bool.self, + in: url + ) + + let useRunArgsAndEnv = try consumeArg( + "profile-use-run-args-and-env", + as: Bool.self, + in: url + ) + let xcodeConfiguration = try consumeArg( + "profile-xcode-configuration", + as: String?.self, + in: url + ) + + var ( + launchTarget, + commandLineArguments, + environmentVariables + ) = try consumeArg( + "profile", + as: SchemeInfo.LaunchTarget?.self, + in: url, + allTargetIDs: &allTargetIDs, + context: #"Custom scheme "\#(name)"'s profile launch target"#, + commandLineArguments: specifiedCommandLineArguments, + environmentVariables: specifiedEnvironmentVariables, + extensionHostIDs: extensionHostIDs, + targetCommandLineArguments: targetCommandLineArguments, + targetEnvironmentVariables: targetEnvironmentVariables, + targetsByID: targetsByID + ) + let customWorkingDirectory = try consumeArg( + "profile-custom-working-directory", + as: String?.self, + in: url + ) + + if environmentVariablesIncludeDefaults { + environmentVariables.insert( + contentsOf: Array.defaultEnvironmentVariables, + at: 0 + ) + } + + return SchemeInfo.Profile( + buildTargets: buildTargets, + commandLineArguments: commandLineArguments, + customWorkingDirectory: customWorkingDirectory, + environmentVariables: environmentVariables, + launchTarget: launchTarget, + useRunArgsAndEnv: useRunArgsAndEnv, + xcodeConfiguration: xcodeConfiguration + ) + } + + // MARK: - SchemeInfo.Run + + mutating func consumeArg( + as type: SchemeInfo.Run.Type, + in url: URL, + allTargetIDs: inout Set, + extensionHostIDs: [TargetID: [TargetID]], + name: String, + targetCommandLineArguments: [TargetID: [CommandLineArgument]], + targetEnvironmentVariables: [TargetID: [EnvironmentVariable]], + targetsByID: [TargetID: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]], + file: StaticString = #filePath, + line: UInt = #line + ) throws -> SchemeInfo.Run { + let buildTargets = try consumeArgs( + "run-build-targets", + as: Target.self, + in: url, + transform: { id in + return try targetsByID + .value(for: TargetID(id), context: "Run build target") + } + ) + allTargetIDs.formUnion(buildTargets.map(\.key.sortedIds.first!)) + + let specifiedCommandLineArguments = + try consumeArgs("run", as: CommandLineArgument.self, in: url) + let specifiedEnvironmentVariables = + try consumeArgs("run", as: EnvironmentVariable.self, in: url) + let environmentVariablesIncludeDefaults = + try consumeArg("run-include-default-env", as: Bool.self, in: url) + + let enableAddressSanitizer = try consumeArg( + "run-enable-address-sanitizer", + as: Bool.self, + in: url + ) + let enableThreadSanitizer = try consumeArg( + "run-enable-thread-sanitizer", + as: Bool.self, + in: url + ) + let enableUBSanitizer = try consumeArg( + "run-enable-undefined-behavior-sanitizer", + as: Bool.self, + in: url + ) + let xcodeConfiguration = + try consumeArg("run-xcode-configuration", as: String?.self, in: url) + + var ( + launchTarget, + commandLineArguments, + environmentVariables + ) = try consumeArg( + "run", + as: SchemeInfo.LaunchTarget?.self, + in: url, + allTargetIDs: &allTargetIDs, + context: #"Custom scheme "\#(name)"'s run launch target"#, + commandLineArguments: specifiedCommandLineArguments, + environmentVariables: specifiedEnvironmentVariables, + extensionHostIDs: extensionHostIDs, + targetCommandLineArguments: targetCommandLineArguments, + targetEnvironmentVariables: targetEnvironmentVariables, + targetsByID: targetsByID + ) + let customWorkingDirectory = try consumeArg( + "run-custom-working-directory", + as: String?.self, + in: url + ) + + if environmentVariablesIncludeDefaults { + environmentVariables.insert( + contentsOf: Array.defaultEnvironmentVariables, + at: 0 + ) + } + + let transitivePreviewReferences = Array(Set( + allTargetIDs.flatMap { id in + return transitivePreviewReferences[id, default: []] + } + )) + + return SchemeInfo.Run( + buildTargets: buildTargets, + commandLineArguments: commandLineArguments, + customWorkingDirectory: customWorkingDirectory, + enableAddressSanitizer: enableAddressSanitizer, + enableThreadSanitizer: enableThreadSanitizer, + enableUBSanitizer: enableUBSanitizer, + environmentVariables: environmentVariables, + launchTarget: launchTarget, + transitivePreviewReferences: + transitivePreviewReferences, + xcodeConfiguration: xcodeConfiguration + ) + } + + // MARK: - SchemeInfo.Test + + mutating func consumeArg( + as type: SchemeInfo.Test.Type, + in url: URL, + allTargetIDs: inout Set, + targetCommandLineArguments: [TargetID: [CommandLineArgument]], + targetEnvironmentVariables: [TargetID: [EnvironmentVariable]], + targetsByID: [TargetID: Target], + file: StaticString = #filePath, + line: UInt = #line + ) throws -> SchemeInfo.Test { + let testTargetCount = + try consumeArg("test-target-count", as: Int.self, in: url) + + var testTargets: [SchemeInfo.TestTarget] = [] + for _ in (0.. `[SchemeInfo.ExecutionAction]`. + static func parse( + from url: URL, + targetsByID: [TargetID: Target] + ) async throws -> Self { + var rawArgs = ArraySlice(try await url.allLines.collect()) + + var ret: [String: [SchemeInfo.ExecutionAction]] = [:] + + while !rawArgs.isEmpty { + let schemeName = try rawArgs.consumeArg("scheme-name", in: url) + let action = try rawArgs.consumeArg( + "action", + as: SchemeInfo.ExecutionAction.Action.self, + in: url + ) + let isPreAction = + try rawArgs.consumeArg("is-pre-action", as: Bool.self, in: url) + let title = try rawArgs.consumeArg("title", in: url).nullsToNewlines + let scriptText = + try rawArgs.consumeArg("script-text", in: url).nullsToNewlines + let id = + try rawArgs.consumeArg("target-id", as: TargetID.self, in: url) + let order = try rawArgs.consumeArg("order", as: Int?.self, in: url) + + ret[schemeName, default: []].append( + .init( + title: title, + scriptText: scriptText, + action: action, + isPreAction: isPreAction, + target: try targetsByID.value( + for: id, + context: "Execution action associated target ID" + ), + order: order + ) + ) + } + + return ret + } +} + +private extension PBXProductType { + var needsExtensionHost: Bool { + switch self { + case .appExtension, + .intentsServiceExtension, + .messagesExtension, + .tvExtension, + .extensionKitExtension: + return true + default: + return false + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/CreateScheme.swift b/tools/generators/xcschemes/src/Generator/CreateScheme.swift new file mode 100644 index 0000000000..4a1d5bae77 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CreateScheme.swift @@ -0,0 +1,506 @@ +import OrderedCollections +import PBXProj +import XCScheme + +extension Generator { + struct CreateScheme { + private let createAnalyzeAction: CreateAnalyzeAction + private let createArchiveAction: CreateArchiveAction + private let createBuildAction: CreateBuildAction + private let createLaunchAction: CreateLaunchAction + private let createProfileAction: CreateProfileAction + private let createSchemeXML: XCScheme.CreateScheme + private let createTestAction: CreateTestAction + + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + createAnalyzeAction: CreateAnalyzeAction, + createArchiveAction: CreateArchiveAction, + createBuildAction: CreateBuildAction, + createLaunchAction: CreateLaunchAction, + createProfileAction: CreateProfileAction, + createSchemeXML: XCScheme.CreateScheme, + createTestAction: CreateTestAction, + callable: @escaping Callable = Self.defaultCallable + ) { + self.createAnalyzeAction = createAnalyzeAction + self.createArchiveAction = createArchiveAction + self.createBuildAction = createBuildAction + self.createLaunchAction = createLaunchAction + self.createProfileAction = createProfileAction + self.createSchemeXML = createSchemeXML + self.createTestAction = createTestAction + + self.callable = callable + } + + /// Creates the XML for an `.xcscheme` file. + func callAsFunction( + defaultXcodeConfiguration: String, + extensionPointIdentifiers: [TargetID: ExtensionPointIdentifier], + schemeInfo: SchemeInfo + ) throws -> (name: String, scheme: String) { + return try callable( + /*defaultXcodeConfiguration:*/ defaultXcodeConfiguration, + /*extensionPointIdentifiers:*/ extensionPointIdentifiers, + /*schemeInfo:*/ schemeInfo, + /*createAnalyzeAction:*/ createAnalyzeAction, + /*createArchiveAction:*/ createArchiveAction, + /*createBuildAction:*/ createBuildAction, + /*createLaunchAction:*/ createLaunchAction, + /*createProfileAction:*/ createProfileAction, + /*createSchemeXML:*/ createSchemeXML, + /*createTestAction:*/ createTestAction + ) + } + } +} + +// MARK: - CreateScheme.Callable + +extension Generator.CreateScheme { + typealias Callable = ( + _ defaultXcodeConfiguration: String, + _ extensionPointIdentifiers: [TargetID: ExtensionPointIdentifier], + _ schemeInfo: SchemeInfo, + _ createAnalyzeAction: CreateAnalyzeAction, + _ createArchiveAction: CreateArchiveAction, + _ createBuildAction: CreateBuildAction, + _ createLaunchAction: CreateLaunchAction, + _ createProfileAction: CreateProfileAction, + _ createSchemeXML: XCScheme.CreateScheme, + _ createTestAction: CreateTestAction + ) throws -> (name: String, scheme: String) + + static func defaultCallable( + defaultXcodeConfiguration: String, + extensionPointIdentifiers: [TargetID: ExtensionPointIdentifier], + schemeInfo: SchemeInfo, + createAnalyzeAction: CreateAnalyzeAction, + createArchiveAction: CreateArchiveAction, + createBuildAction: CreateBuildAction, + createLaunchAction: CreateLaunchAction, + createProfileAction: CreateProfileAction, + createSchemeXML: XCScheme.CreateScheme, + createTestAction: CreateTestAction + ) throws -> (name: String, scheme: String) { + var buildActionEntries: + OrderedDictionary = [:] + func adjustBuildActionEntry( + for reference: BuildableReference, + include buildFor: BuildActionEntry.BuildFor + ) { + buildActionEntries[ + reference.blueprintIdentifier, + default: .init( + buildableReference: reference, + buildFor: [] + ) + ].buildFor.formUnion(buildFor) + } + + var buildPostActions: [OrderedExecutionAction] = [] + var buildPreActions: [OrderedExecutionAction] = [] + var launchPostActions: [OrderedExecutionAction] = [] + var launchPreActions: [OrderedExecutionAction] = [] + var profilePostActions: [OrderedExecutionAction] = [] + var profilePreActions: [OrderedExecutionAction] = [] + var testPostActions: [OrderedExecutionAction] = [] + var testPreActions: [OrderedExecutionAction] = [] + + func handleExecutionAction( + _ executionAction: SchemeInfo.ExecutionAction + ) { + let schemeExecutionAction = ExecutionAction( + title: executionAction.title, + escapedScriptText: + executionAction.scriptText.schemeXmlEscaped, + expandVariablesBasedOn: + executionAction.target.buildableReference + ) + + switch (executionAction.action, executionAction.isPreAction) { + case (.build, true): + buildPreActions + .append((schemeExecutionAction, executionAction.order)) + case (.build, false): + buildPostActions + .append((schemeExecutionAction, executionAction.order)) + case (.run, true): + launchPreActions + .append((schemeExecutionAction, executionAction.order)) + case (.run, false): + launchPostActions + .append((schemeExecutionAction, executionAction.order)) + case (.test, true): + testPreActions + .append((schemeExecutionAction, executionAction.order)) + case (.test, false): + testPostActions + .append((schemeExecutionAction, executionAction.order)) + case (.profile, true): + profilePreActions + .append((schemeExecutionAction, executionAction.order)) + case (.profile, false): + profilePostActions + .append((schemeExecutionAction, executionAction.order)) + } + } + + // MARK: Run + + let launchBuildConfiguration = schemeInfo.run.xcodeConfiguration ?? + defaultXcodeConfiguration + + let launchRunnable: Runnable? + let wasCreatedForAppExtension: Bool + if let launchTarget = schemeInfo.run.launchTarget { + let buildableReference = + launchTarget.primary.buildableReference + + adjustBuildActionEntry( + for: buildableReference, + include: [.running, .analyzing] + ) + + if let extensionHost = launchTarget.extensionHost { + let hostBuildableReference = extensionHost.buildableReference + + adjustBuildActionEntry( + for: hostBuildableReference, + include: [.running, .analyzing] + ) + + let extensionPointIdentifier = try extensionPointIdentifiers + .value( + for: launchTarget.primary.key.sortedIds.first!, + context: "Extension Target ID" + ) + + launchRunnable = .hosted( + buildableReference: buildableReference, + hostBuildableReference: hostBuildableReference, + debuggingMode: extensionPointIdentifier.debuggingMode, + remoteBundleIdentifier: + extensionPointIdentifier.remoteBundleIdentifier + ) + wasCreatedForAppExtension = true + } else { + launchRunnable = .plain(buildableReference: buildableReference) + wasCreatedForAppExtension = false + } + + launchPreActions + .appendUpdateLldbInitAndCopyDSYMs(for: buildableReference) + } else { + launchRunnable = nil + wasCreatedForAppExtension = false + } + + for buildOnlyTarget in schemeInfo.run.buildTargets { + adjustBuildActionEntry( + for: buildOnlyTarget.buildableReference, + include: [.running, .analyzing] + ) + } + + // MARK: Profile + + let profileRunnable: Runnable? + if let launchTarget = schemeInfo.profile.launchTarget { + let buildableReference = + launchTarget.primary.buildableReference + + adjustBuildActionEntry(for: buildableReference, include: .profiling) + + if let extensionHost = launchTarget.extensionHost { + let hostBuildableReference = extensionHost.buildableReference + + adjustBuildActionEntry( + for: hostBuildableReference, + include: .profiling + ) + + let extensionPointIdentifier = try extensionPointIdentifiers + .value( + for: launchTarget.primary.key.sortedIds.first!, + context: "Extension Target ID" + ) + + profileRunnable = .hosted( + buildableReference: buildableReference, + hostBuildableReference: hostBuildableReference, + debuggingMode: extensionPointIdentifier.debuggingMode, + remoteBundleIdentifier: + extensionPointIdentifier.remoteBundleIdentifier + ) + } else { + profileRunnable = + .plain(buildableReference: buildableReference) + } + + profilePreActions + .appendUpdateLldbInitAndCopyDSYMs(for: buildableReference) + } else { + profileRunnable = nil + } + + for buildOnlyTarget in schemeInfo.profile.buildTargets { + adjustBuildActionEntry( + for: buildOnlyTarget.buildableReference, + include: .profiling + ) + } + + // MARK: Test + + for buildOnlyTarget in schemeInfo.test.buildTargets { + adjustBuildActionEntry( + for: buildOnlyTarget.buildableReference, + include: .testing + ) + } + + // We process `testTargets` after `buildTargets` to ensure that + // test bundle icons are only used for a scheme if there are no launch + // or library targets declared + var testables: [Testable] = [] + for testTarget in schemeInfo.test.testTargets { + let buildableTarget = testTarget.target + let buildableReference = buildableTarget.buildableReference + + adjustBuildActionEntry(for: buildableReference, include: .testing) + + testables.append( + .init( + buildableReference: buildableReference, + skipped: !testTarget.enabled + ) + ) + } + + // If we have a testable, use the first one to update `.lldbinit` + if let buildableReference = testables.first?.buildableReference { + testPreActions + .appendUpdateLldbInitAndCopyDSYMs(for: buildableReference) + } + + // MARK: Execution actions + + for executionAction in schemeInfo.executionActions { + handleExecutionAction(executionAction) + } + + // MARK: Xcode Previews additional targets + + for reference in schemeInfo.run.transitivePreviewReferences { + adjustBuildActionEntry(for: reference, include: .running) + } + + // MARK: Build + + let buildActionEntryValues: [BuildActionEntry] + + let unsortedBuildActionEntries = buildActionEntries.values.elements + let restStartIndex = + buildActionEntries.values.elements.startIndex.advanced(by: 1) + let restEndIndex = buildActionEntries.values.elements.endIndex + if restStartIndex < restStartIndex { + // Keep the first element as first, then sort the test by name. + // This ensure that Run action launch targets, a library target, + // or finally a test target, is listed first. This influences the + // icon shown for the scheme in Xcode. + buildActionEntryValues = [unsortedBuildActionEntries.first!] + + (unsortedBuildActionEntries[restStartIndex ..< restEndIndex]) + .sorted { lhs, rhs in + return lhs.buildableReference.blueprintName + .localizedStandardCompare( + rhs.buildableReference.blueprintName + ) == .orderedAscending + } + } else { + buildActionEntryValues = buildActionEntries.values.elements + } + + if let firstReference = + buildActionEntryValues.first?.buildableReference + { + // Use the first build entry for our Bazel support build pre-actions + buildPreActions.appendInitializeBazelBuildOutputGroupsFile( + with: firstReference + ) + buildPreActions.appendPrepareBazelDependencies(with: firstReference) + } + + // MARK: Scheme + + let scheme = createSchemeXML( + buildAction: createBuildAction( + entries: buildActionEntryValues, + postActions: buildPostActions + .sorted(by: compareExecutionActions) + .map(\.action), + preActions: buildPreActions + .sorted(by: compareExecutionActions) + .map(\.action) + ), + testAction: createTestAction( + buildConfiguration: schemeInfo.test.xcodeConfiguration ?? + defaultXcodeConfiguration, + commandLineArguments: schemeInfo.test.commandLineArguments, + enableAddressSanitizer: schemeInfo.test.enableAddressSanitizer, + enableThreadSanitizer: schemeInfo.test.enableThreadSanitizer, + enableUBSanitizer: schemeInfo.test.enableUBSanitizer, + environmentVariables: schemeInfo.test.environmentVariables, + expandVariablesBasedOn: schemeInfo.test.useRunArgsAndEnv ? + nil : testables.first?.buildableReference, + postActions: testPostActions + .sorted(by: compareExecutionActions) + .map(\.action), + preActions: testPreActions + .sorted(by: compareExecutionActions) + .map(\.action), + testables: testables, + useLaunchSchemeArgsEnv: schemeInfo.test.useRunArgsAndEnv + ), + launchAction: createLaunchAction( + buildConfiguration: launchBuildConfiguration, + commandLineArguments: schemeInfo.run.commandLineArguments, + customWorkingDirectory: schemeInfo.run.customWorkingDirectory, + enableAddressSanitizer: schemeInfo.run.enableAddressSanitizer, + enableThreadSanitizer: schemeInfo.run.enableThreadSanitizer, + enableUBSanitizer: schemeInfo.run.enableUBSanitizer, + environmentVariables: schemeInfo.run.environmentVariables, + postActions: launchPostActions + .sorted(by: compareExecutionActions) + .map(\.action), + preActions: launchPreActions + .sorted(by: compareExecutionActions) + .map(\.action), + runnable: launchRunnable + ), + profileAction: createProfileAction( + buildConfiguration: schemeInfo.profile.xcodeConfiguration ?? + defaultXcodeConfiguration, + commandLineArguments: schemeInfo.run.commandLineArguments, + customWorkingDirectory: schemeInfo.run.customWorkingDirectory, + environmentVariables: schemeInfo.run.environmentVariables, + postActions: profilePostActions + .sorted(by: compareExecutionActions) + .map(\.action), + preActions: profilePreActions + .sorted(by: compareExecutionActions) + .map(\.action), + useLaunchSchemeArgsEnv: true, + runnable: profileRunnable + ), + analyzeAction: createAnalyzeAction( + buildConfiguration: launchBuildConfiguration + ), + archiveAction: createArchiveAction( + buildConfiguration: launchBuildConfiguration + ), + wasCreatedForAppExtension: wasCreatedForAppExtension + ) + + return (schemeInfo.name, scheme) + } +} + +private typealias OrderedExecutionAction = + (action: ExecutionAction, order: Int?) + +private func compareExecutionActions( + lhs: OrderedExecutionAction, + rhs: OrderedExecutionAction +) -> Bool { + guard let lhsOrder = lhs.order else { + return false + } + guard let rhsOrder = rhs.order else { + return true + } + return lhsOrder < rhsOrder +} + +private extension Array where Element == OrderedExecutionAction { + private static let initializeBazelBuildOutputGroupsFileScriptText = #""" +mkdir -p "${BUILD_MARKER_FILE%/*}" +touch "$BUILD_MARKER_FILE" + +"""#.schemeXmlEscaped + + mutating func appendInitializeBazelBuildOutputGroupsFile( + with buildableReference: BuildableReference + ) { + append( + ( + ExecutionAction( + title: "Initialize Bazel Build Output Groups File", + escapedScriptText: + Self.initializeBazelBuildOutputGroupsFileScriptText, + expandVariablesBasedOn: buildableReference + ), + -100 + ) + ) + } + + private static let prepareBazelDependenciesScriptText = #""" +mkdir -p "$PROJECT_DIR" + +if [[ "${ENABLE_ADDRESS_SANITIZER:-}" == "YES" || \ + "${ENABLE_THREAD_SANITIZER:-}" == "YES" || \ + "${ENABLE_UNDEFINED_BEHAVIOR_SANITIZER:-}" == "YES" ]] +then + # TODO: Support custom toolchains once clang.sh supports them + cd "$INTERNAL_DIR" || exit 1 + ln -shfF "$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib" lib +fi + +"""#.schemeXmlEscaped + + /// Symlinks `$DEVELOPER_DIR/Toolchains/XcodeDefault.xctoolchain/usr/lib` to + /// `$(BAZEL_INTEGRATION_DIR)/../lib` so that Xcode can copy sanitizers' + /// dylibs. + mutating func appendPrepareBazelDependencies( + with buildableReference: BuildableReference + ) { + append( + ( + ExecutionAction( + title: "Prepare BazelDependencies", + escapedScriptText: Self.prepareBazelDependenciesScriptText, + expandVariablesBasedOn: buildableReference + ), + 0 + ) + ) + } + + private static let updateLldbInitAndCopyDSYMsScriptText = #""" +"$BAZEL_INTEGRATION_DIR/create_lldbinit.sh" +"$BAZEL_INTEGRATION_DIR/copy_dsyms.sh" + +"""#.schemeXmlEscaped + + mutating func appendUpdateLldbInitAndCopyDSYMs( + for buildableReference: BuildableReference + ) { + append( + ( + ExecutionAction( + title: "Update .lldbinit and copy dSYMs", + escapedScriptText: + Self.updateLldbInitAndCopyDSYMsScriptText, + expandVariablesBasedOn: buildableReference + ), + 0 + ) + ) + } +} diff --git a/tools/generators/xcschemes/src/Generator/CreateTargetAutomaticSchemeInfo.swift b/tools/generators/xcschemes/src/Generator/CreateTargetAutomaticSchemeInfo.swift new file mode 100644 index 0000000000..e45cc0e21c --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/CreateTargetAutomaticSchemeInfo.swift @@ -0,0 +1,126 @@ +import Foundation +import PBXProj +import XCScheme + +extension Generator { + struct CreateTargetAutomaticSchemeInfos { + private let createAutomaticSchemeInfo: CreateAutomaticSchemeInfo + + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + createAutomaticSchemeInfo: CreateAutomaticSchemeInfo, + callable: @escaping Callable = Self.defaultCallable + ) { + self.createAutomaticSchemeInfo = createAutomaticSchemeInfo + + self.callable = callable + } + + /// Creates `SchemeInfo`s for a target's automatically generated + /// schemes. + func callAsFunction( + commandLineArguments: [CommandLineArgument], + customSchemeNames: Set, + environmentVariables: [EnvironmentVariable], + extensionHostIDs: [TargetID: [TargetID]], + target: Target, + targetsByID: [TargetID: Target], + targetsByKey: [Target.Key: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]] + ) throws -> [SchemeInfo] { + return try callable( + /*commandLineArguments:*/ commandLineArguments, + /*customSchemeNames:*/ customSchemeNames, + /*environmentVariables:*/ environmentVariables, + /*extensionHostIDs:*/ extensionHostIDs, + /*target:*/ target, + /*targetsByID:*/ targetsByID, + /*targetsByKey:*/ targetsByKey, + /*transitivePreviewReferences:*/ transitivePreviewReferences, + /*createAutomaticSchemeInfo:*/ createAutomaticSchemeInfo + ) + } + } +} + +// MARK: - CreateTargetAutomaticSchemeInfos.Callable + +extension Generator.CreateTargetAutomaticSchemeInfos { + typealias Callable = ( + _ commandLineArguments: [CommandLineArgument], + _ customSchemeNames: Set, + _ environmentVariables: [EnvironmentVariable], + _ extensionHostIDs: [TargetID: [TargetID]], + _ target: Target, + _ targetsByID: [TargetID: Target], + _ targetsByKey: [Target.Key: Target], + _ transitivePreviewReferences: [TargetID: [BuildableReference]], + _ createAutomaticSchemeInfo: Generator.CreateAutomaticSchemeInfo + ) throws -> [SchemeInfo] + + static func defaultCallable( + commandLineArguments: [CommandLineArgument], + customSchemeNames: Set, + environmentVariables: [EnvironmentVariable], + extensionHostIDs: [TargetID: [TargetID]], + target: Target, + targetsByID: [TargetID: Target], + targetsByKey: [Target.Key: Target], + transitivePreviewReferences: [TargetID: [BuildableReference]], + createAutomaticSchemeInfo: Generator.CreateAutomaticSchemeInfo + ) throws -> [SchemeInfo] { + let extensionHostKeys: Set + if extensionHostIDs.isEmpty { + extensionHostKeys = [] + } else { + extensionHostKeys = Set( + try target.key.sortedIds + .flatMap { id in + return try extensionHostIDs[id, default: []] + .map { id in + return try targetsByID.value( + for: id, + context: "Extension host target ID" + ).key + } + } + ) + } + + let id = target.key.sortedIds.first! + + let transitivePreviewReferences = transitivePreviewReferences[ + id, + default: [] + ] + + if extensionHostKeys.isEmpty { + guard let schemeInfo = try createAutomaticSchemeInfo( + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHost: nil, + target: target, + transitivePreviewReferences: transitivePreviewReferences + ) else { + return [] + } + return [schemeInfo] + } else { + return try extensionHostKeys.compactMap { key in + return try createAutomaticSchemeInfo( + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHost: targetsByKey[key]!, + target: target, + transitivePreviewReferences: transitivePreviewReferences + ) + } + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/Environment.swift b/tools/generators/xcschemes/src/Generator/Environment.swift new file mode 100644 index 0000000000..484d5383e3 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/Environment.swift @@ -0,0 +1,71 @@ +import PBXProj +import XCScheme + +extension Generator { + /// Provides the callable dependencies for `Generator`. + /// + /// The main purpose of `Environment` is to enable dependency injection, + /// allowing for different implementations to be used in tests. + struct Environment { + let calculateSchemeReferencedContainer: + CalculateSchemeReferencedContainer + + let calculateTargetsByKey: CalculateTargetsByKey + + let createAutomaticSchemeInfos: CreateAutomaticSchemeInfos + + let createCustomSchemeInfos: CreateCustomSchemeInfos + + let readExtensionPointIdentifiersFile: ReadExtensionPointIdentifiersFile + + let readTargetArgsAndEnvFile: ReadTargetArgsAndEnvFile + + let readTargetsFromConsolidationMaps: ReadTargetsFromConsolidationMaps + + let readTransitivePreviewReferencesFile: + ReadTransitivePreviewReferencesFile + + let writeSchemeManagement: WriteSchemeManagement + + let writeSchemes: WriteSchemes + } +} + +extension Generator.Environment { + static let `default` = Self( + calculateSchemeReferencedContainer: + Generator.CalculateSchemeReferencedContainer(), + calculateTargetsByKey: Generator.CalculateTargetsByKey(), + createAutomaticSchemeInfos: Generator.CreateAutomaticSchemeInfos( + createTargetAutomaticSchemeInfos: + Generator.CreateTargetAutomaticSchemeInfos( + createAutomaticSchemeInfo: + Generator.CreateAutomaticSchemeInfo() + ) + ), + createCustomSchemeInfos: Generator.CreateCustomSchemeInfos(), + readExtensionPointIdentifiersFile: + Generator.ReadExtensionPointIdentifiersFile(), + readTargetArgsAndEnvFile: Generator.ReadTargetArgsAndEnvFile(), + readTargetsFromConsolidationMaps: + Generator.ReadTargetsFromConsolidationMaps(), + readTransitivePreviewReferencesFile: + Generator.ReadTransitivePreviewReferencesFile(), + writeSchemeManagement: Generator.WriteSchemeManagement( + createSchemeManagement: CreateSchemeManagement(), + write: Write() + ), + writeSchemes: Generator.WriteSchemes( + createScheme: Generator.CreateScheme( + createAnalyzeAction: CreateAnalyzeAction(), + createArchiveAction: CreateArchiveAction(), + createBuildAction: CreateBuildAction(), + createLaunchAction: CreateLaunchAction(), + createProfileAction: CreateProfileAction(), + createSchemeXML: XCScheme.CreateScheme(), + createTestAction: CreateTestAction() + ), + write: Write() + ) + ) +} diff --git a/tools/generators/xcschemes/src/Generator/EnvironmentVariable+Extensions.swift b/tools/generators/xcschemes/src/Generator/EnvironmentVariable+Extensions.swift new file mode 100644 index 0000000000..c17835522c --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/EnvironmentVariable+Extensions.swift @@ -0,0 +1,14 @@ +import XCScheme + +extension Array where Element == EnvironmentVariable { + static let defaultEnvironmentVariables: [EnvironmentVariable] = [ + .init( + key: "BUILD_WORKING_DIRECTORY", + value: "$(BUILT_PRODUCTS_DIR)" + ), + .init( + key: "BUILD_WORKSPACE_DIRECTORY", + value: "$(BUILD_WORKSPACE_DIRECTORY)" + ), + ] +} diff --git a/tools/generators/xcschemes/src/Generator/ExtensionPointIdentifier.swift b/tools/generators/xcschemes/src/Generator/ExtensionPointIdentifier.swift new file mode 100644 index 0000000000..4cf31e909e --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/ExtensionPointIdentifier.swift @@ -0,0 +1,33 @@ +/// `NSExtension.NSExtensionPointIdentifier` from application extension +/// `Info.plist` files. +enum ExtensionPointIdentifier: String, Decodable { + case messagePayloadProvider = "com.apple.message-payload-provider" + case widgetKitExtension = "com.apple.widgetkit-extension" + case unknown = "" + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + self = .init(rawValue: value) ?? .unknown + } +} + +extension ExtensionPointIdentifier { + var debuggingMode: Int { + switch self { + case .messagePayloadProvider: + return 1 + default: + return 2 + } + } + + var remoteBundleIdentifier: String { + switch self { + case .messagePayloadProvider: + return "com.apple.MobileSMS" + default: + return "com.apple.springboard" + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/Generator.swift b/tools/generators/xcschemes/src/Generator/Generator.swift new file mode 100644 index 0000000000..f5f9c2e3ce --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/Generator.swift @@ -0,0 +1,90 @@ +import PBXProj +import XCScheme + +/// A type that generates and writes to disk `.xcscheme` files for a project. +/// +/// The `Generator` type is stateless. It can be used to generate `.xcscheme` +/// files for multiple projects. The `generate()` method is passed all the +/// inputs needed to generate the files. +struct Generator { + private let environment: Environment + + init(environment: Environment = .default) { + self.environment = environment + } + + /// Calculates the `.xcscheme` files and writes them to disk. + func generate(arguments: Arguments) async throws { + let targets = try await environment.readTargetsFromConsolidationMaps( + arguments.consolidationMaps, + referencedContainer: environment.calculateSchemeReferencedContainer( + installPath: arguments.installPath, + workspace: arguments.workspace + ) + ) + + let (targetsByKey, targetsByID) = environment.calculateTargetsByKey( + targets: targets + ) + + let ( + commandLineArguments, + environmentVariables + ) = try await environment + .readTargetArgsAndEnvFile(arguments.targetsArgsEnvFile) + let extensionHostIDs = arguments.calculateExtensionHostIDs() + let transitivePreviewReferences: [TargetID: [BuildableReference]] = + try await environment.readTransitivePreviewReferencesFile( + arguments.transitivePreviewTargetsFile, + targetsByID: targetsByID + ) + + let customSchemeInfos = try await environment.createCustomSchemeInfos( + commandLineArguments: commandLineArguments, + customSchemesFile: arguments.customSchemesFile, + environmentVariables: environmentVariables, + executionActionsFile: arguments.executionActionsFile, + extensionHostIDs: extensionHostIDs, + targetsByID: targetsByID, + transitivePreviewReferences: transitivePreviewReferences + ) + + let automaticSchemeInfos = try environment.createAutomaticSchemeInfos( + autogenerationMode: arguments.autogenerationMode, + commandLineArguments: commandLineArguments, + customSchemeNames: Set(customSchemeInfos.map(\.name)), + environmentVariables: environmentVariables, + extensionHostIDs: extensionHostIDs, + targets: targets, + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences + ) + + let schemeInfos = customSchemeInfos + automaticSchemeInfos + + let writeSchemesTask = Task { + try await environment.writeSchemes( + defaultXcodeConfiguration: + arguments.defaultXcodeConfiguration, + extensionPointIdentifiers: try environment + .readExtensionPointIdentifiersFile( + arguments.extensionPointIdentifiersFile + ), + schemeInfos: schemeInfos, + to: arguments.outputDirectory + ) + } + + let writeSchemeManagement = Task { + try await environment.writeSchemeManagement( + schemeNames: schemeInfos.map(\.name), + to: arguments.schemeManagementOutputPath + ) + } + + // Wait for all of the writes to complete + try await writeSchemeManagement.value + try await writeSchemesTask.value + } +} diff --git a/tools/generators/xcschemes/src/Generator/GeneratorArguments.swift b/tools/generators/xcschemes/src/Generator/GeneratorArguments.swift new file mode 100644 index 0000000000..f2156c4c47 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/GeneratorArguments.swift @@ -0,0 +1,133 @@ +import ArgumentParser +import Foundation +import PBXProj + +extension Generator { + struct Arguments: ParsableArguments { + @Argument( + help: """ +Path to the directory where `.xcscheme` files should be written. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: true) } + ) + var outputDirectory: URL + + @Argument( + help: """ +Path to where the `xcschememanagement.plist` file should be written. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var schemeManagementOutputPath: URL + + @Argument( + help: """ +Specifies how schemes are automatically generated: + +- `auto`: If no custom schemes are specified, a scheme will be created for \ +every buildable target. If custom schemes are provided, no autogenerated \ +schemes will be created. +- `all`: A scheme is generated for every buildable target even if custom \ +schemes are provided. +- `none`: No schemes are automatically generated. +""" + ) + var autogenerationMode: AutogenerationMode + + @Argument(help: "Name of the default Xcode configuration.") + var defaultXcodeConfiguration: String + + @Argument(help: "Absolute path to the Bazel workspace.") + var workspace: String + + @Argument(help: """ +Bazel workspace relative path to where the final `.xcodeproj` will be output. +""") + var installPath: String + + @Argument( + help: """ +Path to a file that contains a JSON representation of \ +`[TargetID: ExtensionPointIdentifier]`. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var extensionPointIdentifiersFile: URL + + @Argument( + help: """ +Path to a file containing arguments for custom execution actions. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var executionActionsFile: URL + + @Argument( + help: """ +Path to a file containing arguments for target-level command-line arguments \ +and environment variables. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var targetsArgsEnvFile: URL + + @Argument( + help: """ +Path to a file that contains arguments for custom schemes. +""", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var customSchemesFile: URL + + @Argument( + help: """ +Path to a file containing `[TargetID: [TargetID]]`, which maps a target to a \ +list of addition targets to include for Xcode Preview support. +""", + transform: { path in + guard !path.isEmpty else { + return nil + } + return URL(fileURLWithPath: path, isDirectory: false) + } + ) + var transitivePreviewTargetsFile: URL? + + @Option( + parsing: .upToNextOption, + help: "Path to the consolidation maps.", + transform: { URL(fileURLWithPath: $0, isDirectory: false) } + ) + var consolidationMaps: [URL] + + @Option( + parsing: .upToNextOption, + help: "Pairs of target IDs." + ) + private var targetAndExtensionHosts: [TargetID] = [] + + mutating func validate() throws { + guard targetAndExtensionHosts.count.isMultiple(of: 2) else { + throw ValidationError(""" + (\(targetAndExtensionHosts.count) elements) must \ +be and pairs. +""") + } + } + } +} + +extension Generator.Arguments { + func calculateExtensionHostIDs() -> [TargetID: [TargetID]] { + var ret: [TargetID: [TargetID]] = [:] + for index in stride( + from: 0, + to: targetAndExtensionHosts.count - 1, + by: 2 + ) { + ret[targetAndExtensionHosts[index], default: []] + .append(targetAndExtensionHosts[index+1]) + } + return ret + } +} diff --git a/tools/generators/xcschemes/src/Generator/ReadExtensionPointIdentifiersFile.swift b/tools/generators/xcschemes/src/Generator/ReadExtensionPointIdentifiersFile.swift new file mode 100644 index 0000000000..b86e9614dd --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/ReadExtensionPointIdentifiersFile.swift @@ -0,0 +1,53 @@ +import Foundation +import PBXProj +import ToolCommon + +extension Generator { + class ReadExtensionPointIdentifiersFile { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + callable: @escaping Callable = + ReadExtensionPointIdentifiersFile.defaultCallable + ) { + self.callable = callable + } + + /// Reads the file at `url`, returning a mapping of `TargetID` to + /// `ExtensionPointIdentifier`. + func callAsFunction( + _ url: URL + ) throws -> [TargetID: ExtensionPointIdentifier] { + return try callable(url) + } + } +} + +// MARK: - ReadExtensionPointIdentifiersFile.Callable + +extension Generator.ReadExtensionPointIdentifiersFile { + typealias Callable = ( + _ url: URL + ) throws -> [TargetID: ExtensionPointIdentifier] + + static func defaultCallable( + _ url: URL + ) throws -> [TargetID: ExtensionPointIdentifier] { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + do { + return try decoder.decode( + [TargetID: ExtensionPointIdentifier].self, + from: Data(contentsOf: url) + ) + } catch { + throw PreconditionError( + message: url.prefixMessage(error.localizedDescription) + ) + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/ReadTargetArgsAndEnvFile.swift b/tools/generators/xcschemes/src/Generator/ReadTargetArgsAndEnvFile.swift new file mode 100644 index 0000000000..cc56f2a2d0 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/ReadTargetArgsAndEnvFile.swift @@ -0,0 +1,116 @@ +import ArgumentParser +import Foundation +import PBXProj +import ToolCommon +import XCScheme + +extension Generator { + struct ReadTargetArgsAndEnvFile { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + callable: @escaping Callable = + ReadTargetArgsAndEnvFile.defaultCallable + ) { + self.callable = callable + } + + /// Reads the file at `url`, returning mappings of `TargetID` to + /// `[CommandLineArgument]` and `[EnvironmentVariable]`. + func callAsFunction( + _ url: URL + ) async throws -> ( + commandLineArguments: [TargetID: [CommandLineArgument]], + environmentVariables: [TargetID: [EnvironmentVariable]] + ) { + return try await callable(url) + } + } +} + +// MARK: - ReadTargetArgsAndEnvFile.Callable + +extension Generator.ReadTargetArgsAndEnvFile { + typealias Callable = ( + _ url: URL + ) async throws -> ( + commandLineArguments: [TargetID: [CommandLineArgument]], + environmentVariables: [TargetID: [EnvironmentVariable]] + ) + + static func defaultCallable( + _ url: URL + ) async throws -> ( + commandLineArguments: [TargetID: [CommandLineArgument]], + environmentVariables: [TargetID: [EnvironmentVariable]] + ) { + var rawArgsAndEnv = ArraySlice(try await url.allLines.collect()) + + let argsTargetCount = try rawArgsAndEnv.consumeArg( + "args-target-count", + as: Int.self, + in: url + ) + + var argsKeysWithValues: [(TargetID, [CommandLineArgument])] = [] + for _ in (0.. [Target] { + return try await callable( + /*urls:*/ urls, + /*referencedContainer:*/ referencedContainer + ) + } + } +} + +// MARK: - ReadTargetsFromConsolidationMaps.Callable + +extension Generator.ReadTargetsFromConsolidationMaps { + typealias Callable = ( + _ urls: [URL], + _ referencedContainer: String + ) async throws -> [Target] + + static func defaultCallable( + _ urls: [URL], + referencedContainer: String + ) async throws -> [Target] { + return try await withThrowingTaskGroup( + of: [Target].self + ) { group in + for url in urls { + group.addTask { + try await ConsolidationMapEntry.decode(from: url) + .map { entry in + return Target( + key: entry.key, + productType: entry.productType, + buildableReference: .init( + blueprintIdentifier: Identifiers.Targets + .idWithoutComment( + subIdentifier: entry.subIdentifier, + name: entry.name + ), + buildableName: String( + entry.productPath + .split(separator: "/").last! + ), + blueprintName: entry.name, + referencedContainer: referencedContainer + ) + ) + } + } + } + + var targets: [Target] = [] + for try await shardTargets in group { + targets.append(contentsOf: shardTargets) + } + + return targets + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/ReadTransitivePreviewReferencesFile.swift b/tools/generators/xcschemes/src/Generator/ReadTransitivePreviewReferencesFile.swift new file mode 100644 index 0000000000..fb71123953 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/ReadTransitivePreviewReferencesFile.swift @@ -0,0 +1,70 @@ +import Foundation +import PBXProj +import XCScheme + +extension Generator { + struct ReadTransitivePreviewReferencesFile { + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + callable: @escaping Callable = + ReadTransitivePreviewReferencesFile.defaultCallable + ) { + self.callable = callable + } + + /// Reads the file at `url`, returning a mapping of `TargetID` to + /// `[BuildableReference]`. + func callAsFunction( + _ url: URL?, + targetsByID: [TargetID: Target] + ) async throws -> [TargetID: [BuildableReference]] { + return try await callable(url, /*targetsByID:*/ targetsByID) + } + } +} + +// MARK: - ReadTransitivePreviewReferencesFile.Callable + +extension Generator.ReadTransitivePreviewReferencesFile { + typealias Callable = ( + _ url: URL?, + _ targetsByID: [TargetID: Target] + ) async throws -> [TargetID: [BuildableReference]] + + static func defaultCallable( + _ url: URL?, + targetsByID: [TargetID: Target] + ) async throws -> [TargetID: [BuildableReference]] { + guard let url = url else { + return [:] + } + + var rawArgs = ArraySlice(try await url.lines.collect()) + + var keysWithValues: [(TargetID, [BuildableReference])] = [] + while !rawArgs.isEmpty { + let id = + try rawArgs.consumeArg("target-id", as: TargetID.self, in: url) + let buildableReferences = try rawArgs.consumeArgs( + "buildable-references", + as: BuildableReference.self, + in: url, + transform: { id in + try targetsByID + .value( + for: TargetID(id), + context: "Additional target" + ) + .buildableReference + } + ) + keysWithValues.append((id, buildableReferences)) + } + + return Dictionary(uniqueKeysWithValues: keysWithValues) + } +} diff --git a/tools/generators/xcschemes/src/Generator/SchemeInfo.swift b/tools/generators/xcschemes/src/Generator/SchemeInfo.swift new file mode 100644 index 0000000000..43e2c855b8 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/SchemeInfo.swift @@ -0,0 +1,72 @@ +import ArgumentParser +import XCScheme + +/// Provides information needed to create a scheme. +struct SchemeInfo: Equatable { + struct ExecutionAction: Equatable { + enum Action: String, ExpressibleByArgument { + case build + case test + case run + case profile + } + + let title: String + let scriptText: String + let action: Action + let isPreAction: Bool + let target: Target + let order: Int? + } + + struct LaunchTarget: Equatable { + let primary: Target + let extensionHost: Target? + } + + struct Profile: Equatable { + let buildTargets: [Target] + let commandLineArguments: [CommandLineArgument] + let customWorkingDirectory: String? + let environmentVariables: [EnvironmentVariable] + let launchTarget: LaunchTarget? + let useRunArgsAndEnv: Bool + let xcodeConfiguration: String? + } + + struct Test: Equatable { + let buildTargets: [Target] + let commandLineArguments: [CommandLineArgument] + let enableAddressSanitizer: Bool + let enableThreadSanitizer: Bool + let enableUBSanitizer: Bool + let environmentVariables: [EnvironmentVariable] + let testTargets: [TestTarget] + let useRunArgsAndEnv: Bool + let xcodeConfiguration: String? + } + + struct TestTarget: Equatable { + let target: Target + let enabled: Bool + } + + struct Run: Equatable { + let buildTargets: [Target] + let commandLineArguments: [CommandLineArgument] + let customWorkingDirectory: String? + let enableAddressSanitizer: Bool + let enableThreadSanitizer: Bool + let enableUBSanitizer: Bool + let environmentVariables: [EnvironmentVariable] + let launchTarget: LaunchTarget? + let transitivePreviewReferences: [BuildableReference] + let xcodeConfiguration: String? + } + + let name: String + let test: Test + let run: Run + let profile: Profile + let executionActions: [ExecutionAction] +} diff --git a/tools/generators/xcschemes/src/Generator/Target.swift b/tools/generators/xcschemes/src/Generator/Target.swift new file mode 100644 index 0000000000..801d3d115f --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/Target.swift @@ -0,0 +1,11 @@ +import PBXProj +import XCScheme + +/// An Xcode target, from the point of view of a scheme. +struct Target: Equatable { + typealias Key = ConsolidationMapEntry.Key + + let key: Key + let productType: PBXProductType + let buildableReference: BuildableReference +} diff --git a/tools/generators/xcschemes/src/Generator/WriteSchemes.swift b/tools/generators/xcschemes/src/Generator/WriteSchemes.swift new file mode 100644 index 0000000000..79d3cceaea --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/WriteSchemes.swift @@ -0,0 +1,83 @@ +import Foundation +import PBXProj + +extension Generator { + struct WriteSchemes { + private let createScheme: CreateScheme + private let write: Write + + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + createScheme: CreateScheme, + write: Write, + callable: @escaping Callable = Self.defaultCallable + ) { + self.createScheme = createScheme + self.write = write + + self.callable = callable + } + + /// Creates and writes `.xcscheme`s to disk. + func callAsFunction( + defaultXcodeConfiguration: String, + extensionPointIdentifiers: [TargetID: ExtensionPointIdentifier], + schemeInfos: [SchemeInfo], + to outputDirectory: URL + ) async throws { + try await callable( + /*defaultXcodeConfiguration:*/ defaultXcodeConfiguration, + /*extensionPointIdentifiers:*/ extensionPointIdentifiers, + /*outputDirectory:*/ outputDirectory, + /*schemeInfos:*/ schemeInfos, + /*createScheme:*/ createScheme, + /*write:*/ write + ) + } + } +} + +// MARK: - WriteSchemes.Callable + +extension Generator.WriteSchemes { + typealias Callable = ( + _ defaultXcodeConfiguration: String, + _ extensionPointIdentifiers: [TargetID: ExtensionPointIdentifier], + _ outputDirectory: URL, + _ schemeInfos: [SchemeInfo], + _ createScheme: Generator.CreateScheme, + _ write: Write + ) async throws -> Void + + static func defaultCallable( + defaultXcodeConfiguration: String, + extensionPointIdentifiers: [TargetID: ExtensionPointIdentifier], + outputDirectory: URL, + schemeInfos: [SchemeInfo], + createScheme: Generator.CreateScheme, + write: Write + ) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + for schemeInfo in schemeInfos { + group.addTask { + let (name, scheme) = try createScheme( + defaultXcodeConfiguration: defaultXcodeConfiguration, + extensionPointIdentifiers: extensionPointIdentifiers, + schemeInfo: schemeInfo + ) + + try write( + scheme, + to: outputDirectory.appending(path: "\(name).xcscheme") + ) + } + } + + try await group.waitForAll() + } + } +} diff --git a/tools/generators/xcschemes/src/Generator/WriteSchemesManagement.swift b/tools/generators/xcschemes/src/Generator/WriteSchemesManagement.swift new file mode 100644 index 0000000000..8e25783793 --- /dev/null +++ b/tools/generators/xcschemes/src/Generator/WriteSchemesManagement.swift @@ -0,0 +1,62 @@ +import Foundation +import PBXProj +import XCScheme + +extension Generator { + struct WriteSchemeManagement { + private let createSchemeManagement: CreateSchemeManagement + private let write: Write + + private let callable: Callable + + /// - Parameters: + /// - callable: The function that will be called in + /// `callAsFunction()`. + init( + createSchemeManagement: CreateSchemeManagement, + write: Write, + callable: @escaping Callable = Self.defaultCallable + ) { + self.createSchemeManagement = createSchemeManagement + self.write = write + + self.callable = callable + } + + /// Creates and writes a `xcschememanagement.plist` file to disk. + func callAsFunction( + schemeNames: [String], + to outputPath: URL + ) async throws { + try await callable( + /*outputPath:*/ outputPath, + /*schemeNames:*/ schemeNames, + /*createSchemeManagement:*/ createSchemeManagement, + /*write:*/ write + ) + } + } +} + +// MARK: - WriteSchemeManagement.Callable + +extension Generator.WriteSchemeManagement { + typealias Callable = ( + _ outputPath: URL, + _ schemeNames: [String], + _ createSchemeManagement: CreateSchemeManagement, + _ write: Write + ) async throws -> Void + + static func defaultCallable( + outputPath: URL, + schemeNames: [String], + createSchemeManagement: CreateSchemeManagement, + write: Write + ) async throws { + try write( + createSchemeManagement(schemeNames: schemeNames), + to: outputPath + ) + } +} diff --git a/tools/generators/xcschemes/src/XCSchemes.swift b/tools/generators/xcschemes/src/XCSchemes.swift new file mode 100644 index 0000000000..856652ff9a --- /dev/null +++ b/tools/generators/xcschemes/src/XCSchemes.swift @@ -0,0 +1,37 @@ +import ArgumentParser +import Foundation +import ToolCommon + +@main +struct XCSchemes: AsyncParsableCommand { + static var configuration = CommandConfiguration( + commandName: "targets", + abstract: "Generates the '.xcscheme' files for a project." + ) + + @OptionGroup var arguments: Generator.Arguments + + @Flag(help: "Whether to colorize console output.") + var colorize = false + + static func main() async { + await parseAsRootSupportingParamsFile() + } + + func run() async throws { + let logger = DefaultLogger( + standardError: StderrOutputStream(), + standardOutput: StdoutOutputStream(), + colorize: colorize + ) + + let generator = Generator() + + do { + try await generator.generate(arguments: arguments) + } catch { + logger.logError(error.localizedDescription) + Darwin.exit(1) + } + } +} diff --git a/tools/generators/xcschemes/test/CalculateSchemeReferencedContainerTests.swift b/tools/generators/xcschemes/test/CalculateSchemeReferencedContainerTests.swift new file mode 100644 index 0000000000..dcc7486fde --- /dev/null +++ b/tools/generators/xcschemes/test/CalculateSchemeReferencedContainerTests.swift @@ -0,0 +1,25 @@ +import XCTest + +@testable import xcschemes + +class CalculateSchemeReferencedContainerTests: XCTestCase { + func test() { + // Arrange + + let installPath = "a/visonary.xcodeproj" + let workspace = "/Users/TimApple/Star Board" + + let expectedReferencedContainer = """ +container:/Users/TimApple/Star Board/a/visonary.xcodeproj +""" + + // Act + + let referencedContainer = Generator.CalculateSchemeReferencedContainer + .defaultCallable(installPath: installPath, workspace: workspace) + + // Assert + + XCTAssertEqual(referencedContainer, expectedReferencedContainer) + } +} diff --git a/tools/generators/xcschemes/test/CalculateTargetsByKeyTests.swift b/tools/generators/xcschemes/test/CalculateTargetsByKeyTests.swift new file mode 100644 index 0000000000..9f58db10ca --- /dev/null +++ b/tools/generators/xcschemes/test/CalculateTargetsByKeyTests.swift @@ -0,0 +1,37 @@ +import CustomDump +import PBXProj +import XCTest + +@testable import xcschemes + +class CalculateTargetsByKeyTests: XCTestCase { + func test() { + // Arrange + + let targets: [Target] = [ + .mock(key: ["B"]), + .mock(key: ["A", "C"]), + ] + + let expectedTargetsByKey: [Target.Key: Target] = [ + ["B"]: targets[0], + ["A", "C"]: targets[1], + ] + let expectedTargetsByID: [TargetID: Target] = [ + "B": targets[0], + "A": targets[1], + "C": targets[1], + ] + + + // Act + + let (targetsByKey, targetsByID) = Generator.CalculateTargetsByKey + .defaultCallable(targets: targets) + + // Assert + + XCTAssertNoDifference(targetsByKey, expectedTargetsByKey) + XCTAssertNoDifference(targetsByID, expectedTargetsByID) + } +} diff --git a/tools/generators/xcschemes/test/CreateAutomaticSchemeInfo+Testing.swift b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfo+Testing.swift new file mode 100644 index 0000000000..cd0f911db6 --- /dev/null +++ b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfo+Testing.swift @@ -0,0 +1,71 @@ +import PBXProj +import XCScheme + +@testable import xcschemes + +// MARK: - Generator.CreateAutomaticSchemeInfo.mock + +extension Generator.CreateAutomaticSchemeInfo { + final class MockTracker { + struct Called: Equatable { + let commandLineArguments: [CommandLineArgument] + let customSchemeNames: Set + let environmentVariables: [EnvironmentVariable] + let extensionHost: Target? + let target: Target + let transitivePreviewReferences: [BuildableReference] + } + + fileprivate(set) var called: [Called] = [] + + fileprivate var results: [SchemeInfo?] + + init(results: [SchemeInfo?]) { + self.results = results.reversed() + } + + func nextResult() -> SchemeInfo? { + guard let result = results.popLast() else { + preconditionFailure("Called too many times") + } + return result + } + } + + static func mock( + schemeInfos: [SchemeInfo?] + ) -> (mock: Self, tracker: MockTracker) { + let mockTracker = MockTracker(results: schemeInfos) + + let mocked = Self( + callable: { + commandLineArguments, + customSchemeNames, + environmentVariables, + extensionHost, + target, + transitivePreviewReferences in + mockTracker.called.append(.init( + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHost: extensionHost, + target: target, + transitivePreviewReferences: transitivePreviewReferences + )) + return mockTracker.nextResult() + } + ) + + return (mocked, mockTracker) + } +} + +// MARK: - Generator.CreateAutomaticSchemeInfo.stub + +extension Generator.CreateAutomaticSchemeInfo { + static func stub(schemeInfos: [SchemeInfo?]) -> Self { + let (stub, _) = mock(schemeInfos: schemeInfos) + return stub + } +} diff --git a/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift new file mode 100644 index 0000000000..688b10cf6d --- /dev/null +++ b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfoTests.swift @@ -0,0 +1,708 @@ +import CustomDump +import PBXProj +import XCScheme +import XCTest + +@testable import xcschemes + +final class CreateAutomaticSchemeInfoTests: XCTestCase { + + // MARK: - Custom schemes + + func test_customSchemeNames_match() throws { + // Arrange + + let customSchemeNames: Set = [ + "BLUEPRINT_NAME_Launchable", + "Other", + ] + let launchable = Target( + key: "Launchable", + productType: .application, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + customSchemeNames: customSchemeNames, + target: launchable + ) + + // Assert + + XCTAssertNil(schemeInfo) + } + + func test_customSchemeNames_match_hosted() throws { + // Arrange + + let customSchemeNames: Set = [ + "BLUEPRINT_NAME_Launchable", + "Other", + ] + let extensionHost = Target( + key: "Host", + productType: .appExtension, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Host", + buildableName: "BUILDABLE_NAME_Host", + blueprintName: "BLUEPRINT_NAME_Host", + referencedContainer: "REFERENCED_CONTAINER_Host" + ) + ) + let launchable = Target( + key: "Launchable", + productType: .application, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + customSchemeNames: customSchemeNames, + extensionHost: extensionHost, + target: launchable + ) + + // Assert + + XCTAssertNil(schemeInfo) + } + + func test_customSchemeNames_noMatch() throws { + // Arrange + + let customSchemeNames: Set = [ + "Other", + ] + let launchable = Target( + key: "Launchable", + productType: .application, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + customSchemeNames: customSchemeNames, + target: launchable + ) + + // Assert + + XCTAssertNotNil(schemeInfo) + } + + // MARK: - Launchable + + func test_launchable() throws { + // Arrange + + let launchable = Target( + key: "Launchable", + productType: .application, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Launchable", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + testTargets: [], + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: baseEnvironmentVariables, + launchTarget: .init( + primary: launchable, + extensionHost: nil + ), + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: .init( + primary: launchable, + extensionHost: nil + ), + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + target: launchable + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + func test_launchable_hosted() throws { + // Arrange + + let extensionHost = Target( + key: "Host", + productType: .appExtension, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Host", + buildableName: "BUILDABLE_NAME_Host", + blueprintName: "BLUEPRINT_NAME_Host", + referencedContainer: "REFERENCED_CONTAINER_Host" + ) + ) + let launchable = Target( + key: "Launchable", + productType: .appExtension, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Launchable in BLUEPRINT_NAME_Host", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + testTargets: [], + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: baseEnvironmentVariables, + launchTarget: .init( + primary: launchable, + extensionHost: extensionHost + ), + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: .init( + primary: launchable, + extensionHost: extensionHost + ), + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + extensionHost: extensionHost, + target: launchable + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + func test_launchable_commandLineArguments() throws { + // Arrange + + let launchable = Target( + key: "Launchable", + productType: .watch2App, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + let commandLineArguments: [CommandLineArgument] = [ + .init(value: "S", enabled: false), + .init(value: "A", enabled: true), + ] + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Launchable", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + testTargets: [], + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [], + commandLineArguments: commandLineArguments, + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: baseEnvironmentVariables, + launchTarget: .init( + primary: launchable, + extensionHost: nil + ), + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: .init( + primary: launchable, + extensionHost: nil + ), + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + commandLineArguments: commandLineArguments, + target: launchable + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + func test_launchable_environmentVariables() throws { + // Arrange + + let launchable = Target( + key: "Launchable", + productType: .commandLineTool, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Launchable", + buildableName: "BUILDABLE_NAME_Launchable", + blueprintName: "BLUEPRINT_NAME_Launchable", + referencedContainer: "REFERENCED_CONTAINER_Launchable" + ) + ) + let environmentVariables: [EnvironmentVariable] = [ + .init(key: "D", value: "999", enabled: true), + .init(key: "H", value: "2 2", enabled: false), + ] + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Launchable", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + testTargets: [], + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: + baseEnvironmentVariables + environmentVariables, + launchTarget: .init( + primary: launchable, + extensionHost: nil + ), + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: .init( + primary: launchable, + extensionHost: nil + ), + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + environmentVariables: environmentVariables, + target: launchable + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + // MARK: Static library + + func test_library() throws { + // Arrange + + let library = Target( + key: "Library", + productType: .staticLibrary, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Library", + buildableName: "BUILDABLE_NAME_Library", + blueprintName: "BLUEPRINT_NAME_Library", + referencedContainer: "REFERENCED_CONTAINER_Library" + ) + ) + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Library", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + testTargets: [], + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [library], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: baseEnvironmentVariables, + launchTarget: nil, + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: nil, + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + target: library + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + // MARK: - Test + + func test_test() throws { + // Arrange + + let test = Target( + key: "Test", + productType: .unitTestBundle, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Test", + buildableName: "BUILDABLE_NAME_Test", + blueprintName: "BLUEPRINT_NAME_Test", + referencedContainer: "REFERENCED_CONTAINER_Test" + ) + ) + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Test", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: baseEnvironmentVariables, + testTargets: [.init(target: test, enabled: true)], + useRunArgsAndEnv: false, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [test], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + launchTarget: nil, + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: nil, + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + target: test + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + func test_test_commandLineArguments() throws { + // Arrange + + let test = Target( + key: "Test", + productType: .unitTestBundle, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Test", + buildableName: "BUILDABLE_NAME_Test", + blueprintName: "BLUEPRINT_NAME_Test", + referencedContainer: "REFERENCED_CONTAINER_Test" + ) + ) + let commandLineArguments: [CommandLineArgument] = [ + .init(value: "B", enabled: false), + .init(value: "F", enabled: true), + ] + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Test", + test: .init( + buildTargets: [], + commandLineArguments: commandLineArguments, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: baseEnvironmentVariables, + testTargets: [.init(target: test, enabled: true)], + useRunArgsAndEnv: false, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [test], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + launchTarget: nil, + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: nil, + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + commandLineArguments: commandLineArguments, + target: test + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } + + func test_test_environmentVariables() throws { + // Arrange + + let test = Target( + key: "Test", + productType: .unitTestBundle, + buildableReference: .init( + blueprintIdentifier: "BLUEPRINT_IDENTIFIER_Test", + buildableName: "BUILDABLE_NAME_Test", + blueprintName: "BLUEPRINT_NAME_Test", + referencedContainer: "REFERENCED_CONTAINER_Test" + ) + ) + let environmentVariables: [EnvironmentVariable] = [ + .init(key: "Z", value: "1", enabled: true), + .init(key: "A", value: "2", enabled: false), + ] + + let expectedSchemeInfo = SchemeInfo( + name: "BLUEPRINT_NAME_Test", + test: .init( + buildTargets: [], + commandLineArguments: [], + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: + baseEnvironmentVariables + environmentVariables, + testTargets: [.init(target: test, enabled: true)], + useRunArgsAndEnv: false, + xcodeConfiguration: nil + ), + run: .init( + buildTargets: [test], + commandLineArguments: [], + customWorkingDirectory: nil, + enableAddressSanitizer: false, + enableThreadSanitizer: false, + enableUBSanitizer: false, + environmentVariables: [], + launchTarget: nil, + transitivePreviewReferences: [], + xcodeConfiguration: nil + ), + profile: .init( + buildTargets: [], + commandLineArguments: [], + customWorkingDirectory: nil, + environmentVariables: [], + launchTarget: nil, + useRunArgsAndEnv: true, + xcodeConfiguration: nil + ), + executionActions: [] + ) + + // Act + + let schemeInfo = try createAutomaticSchemeInfoWithDefaults( + environmentVariables: environmentVariables, + target: test + ) + + // Assert + + XCTAssertNoDifference(schemeInfo, expectedSchemeInfo) + } +} + +private let baseEnvironmentVariables: [EnvironmentVariable] = [ + .init(key: "BUILD_WORKING_DIRECTORY", value: "$(BUILT_PRODUCTS_DIR)"), + .init( + key: "BUILD_WORKSPACE_DIRECTORY", + value: "$(BUILD_WORKSPACE_DIRECTORY)" + ), +] + +private func createAutomaticSchemeInfoWithDefaults( + commandLineArguments: [CommandLineArgument] = [], + customSchemeNames: Set = [], + environmentVariables: [EnvironmentVariable] = [], + extensionHost: Target? = nil, + target: Target, + transitivePreviewReferences: [BuildableReference] = [] +) throws -> SchemeInfo? { + return try Generator.CreateAutomaticSchemeInfo.defaultCallable( + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHost: extensionHost, + target: target, + transitivePreviewReferences: transitivePreviewReferences + ) +} + +extension Target.Key: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: TargetID...) { + self.init(elements.sorted()) + } +} + +extension Target.Key: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init([TargetID(value)]) + } +} diff --git a/tools/generators/xcschemes/test/CreateAutomaticSchemeInfosTests.swift b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfosTests.swift new file mode 100644 index 0000000000..90f1fb591d --- /dev/null +++ b/tools/generators/xcschemes/test/CreateAutomaticSchemeInfosTests.swift @@ -0,0 +1,323 @@ +import CustomDump +import PBXProj +import XCScheme +import XCTest + +@testable import xcschemes + +final class CreateAutomaticSchemeInfosTests: XCTestCase { + + // MARK: - autogenerationMode + + func test_autogenerationMode_all() throws { + // Arrange + + let autogenerationMode = AutogenerationMode.all + let customSchemeNames: Set = [ + "A", + "B scheme", + ] + let targets: [Target] = [ + .mock(key: "B"), + .mock(key: "A"), + .mock(key: "C"), + ] + + let createTargetAutomaticSchemeInfos = + Generator.CreateTargetAutomaticSchemeInfos.mock( + targetSchemeInfos: [ + [.mock(name: "B scheme")], + [.mock(name: "A scheme 1"), .mock(name: "A scheme 2")], + [.mock(name: "C scheme")], + ] + ) + + let expectedSchemeInfos: [SchemeInfo] = [ + .mock(name: "B scheme"), + .mock(name: "A scheme 1"), + .mock(name: "A scheme 2"), + .mock(name: "C scheme"), + ] + + // Act + + let schemeInfos = try createAutomaticSchemeInfosWithDefaults( + autogenerationMode: autogenerationMode, + customSchemeNames: customSchemeNames, + targets: targets, + createTargetAutomaticSchemeInfos: + createTargetAutomaticSchemeInfos.mock + ) + + // Assert + + XCTAssertNoDifference(schemeInfos, expectedSchemeInfos) + } + + func test_autogenerationMode_auto_withoutCustomSchemeNames() throws { + // Arrange + + let autogenerationMode = AutogenerationMode.auto + let customSchemeNames: Set = [] + let targets: [Target] = [ + .mock(key: "B"), + .mock(key: "A"), + .mock(key: "C"), + ] + + let createTargetAutomaticSchemeInfos = + Generator.CreateTargetAutomaticSchemeInfos.mock( + targetSchemeInfos: [ + [.mock(name: "B scheme")], + [.mock(name: "A scheme 1"), .mock(name: "A scheme 2")], + [.mock(name: "C scheme")], + ] + ) + + let expectedSchemeInfos: [SchemeInfo] = [ + .mock(name: "B scheme"), + .mock(name: "A scheme 1"), + .mock(name: "A scheme 2"), + .mock(name: "C scheme"), + ] + + // Act + + let schemeInfos = try createAutomaticSchemeInfosWithDefaults( + autogenerationMode: autogenerationMode, + customSchemeNames: customSchemeNames, + targets: targets, + createTargetAutomaticSchemeInfos: + createTargetAutomaticSchemeInfos.mock + ) + + // Assert + + XCTAssertNoDifference(schemeInfos, expectedSchemeInfos) + } + + func test_autogenerationMode_auto_withCustomSchemeNames() throws { + // Arrange + + let autogenerationMode = AutogenerationMode.auto + let customSchemeNames: Set = [ + "Z", + ] + let targets: [Target] = [ + .mock(key: "B"), + .mock(key: "A"), + .mock(key: "C"), + ] + + let createTargetAutomaticSchemeInfos = + Generator.CreateTargetAutomaticSchemeInfos.mock( + targetSchemeInfos: [] + ) + + let expectedSchemeInfos: [SchemeInfo] = [] + + // Act + + let schemeInfos = try createAutomaticSchemeInfosWithDefaults( + autogenerationMode: autogenerationMode, + customSchemeNames: customSchemeNames, + targets: targets, + createTargetAutomaticSchemeInfos: + createTargetAutomaticSchemeInfos.mock + ) + + // Assert + + XCTAssertNoDifference(schemeInfos, expectedSchemeInfos) + } + + func test_autogenerationMode_none() throws { + // Arrange + + let autogenerationMode = AutogenerationMode.none + let targets: [Target] = [ + .mock(key: "B"), + .mock(key: "A"), + .mock(key: "C"), + ] + + let createTargetAutomaticSchemeInfos = + Generator.CreateTargetAutomaticSchemeInfos.mock( + targetSchemeInfos: [] + ) + + let expectedSchemeInfos: [SchemeInfo] = [] + + // Act + + let schemeInfos = try createAutomaticSchemeInfosWithDefaults( + autogenerationMode: autogenerationMode, + targets: targets, + createTargetAutomaticSchemeInfos: + createTargetAutomaticSchemeInfos.mock + ) + + // Assert + + XCTAssertNoDifference(schemeInfos, expectedSchemeInfos) + } + + // MARK: - createTargetAutomaticSchemeInfos + + func test_createTargetAutomaticSchemeInfos() throws { + // Arrange + + let commandLineArguments: [TargetID: [CommandLineArgument]] = [ + "A": [ + .init(value: "-v", enabled: true), + .init(value: "version", enabled: false), + ], + "C": [], + "Z": [.init(value: "No", enabled: false)], + ] + let customSchemeNames: Set = [ + "Z", + ] + let environmentVariables: [TargetID: [EnvironmentVariable]] = [ + "B": [ + .init(key: "VAR", value: "not enabled", enabled: false), + .init(key: "ENV VAR", value: "1", enabled: true), + ], + "Z": [.init(key: "X", value: "No", enabled: false)], + ] + let extensionHostIDs: [TargetID : [TargetID]] = [ + "XyZ": ["3", "WWW"], + ] + let targets: [Target] = [ + .mock(key: "Z", productType: .watch2Extension), + .mock(key: "B", productType: .appExtension), + .mock(key: "A", productType: .messagesExtension), + .mock(key: "C", productType: .application), + ] + let targetsByID: [TargetID: Target] = [ + "Q": .mock(key: "Q"), + ] + let targetsByKey: [Target.Key: Target] = [ + ["1", "D"]: .mock(key: ["1", "D"]), + ] + let transitivePreviewReferences: [TargetID : [BuildableReference]] = [ + "aBc": [ + .init( + blueprintIdentifier: "1", + buildableName: "2", + blueprintName: "3", + referencedContainer: "4" + ), + ], + ] + + // The order these are called is based on the sorting of `targets`, + // first on the product type, then on + // `buildableReference.blueprintName`. Also, certain product types are + // filtered out. + let expectedCreateTargetAutomaticSchemeInfosCalled: [ + Generator.CreateTargetAutomaticSchemeInfos.MockTracker.Called + ] = [ + .init( + commandLineArguments: [], + customSchemeNames: customSchemeNames, + environmentVariables: [], + extensionHostIDs: extensionHostIDs, + target: .mock(key: "C", productType: .application), + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences + ), + .init( + commandLineArguments: [ + .init(value: "-v", enabled: true), + .init(value: "version", enabled: false), + ], + customSchemeNames: customSchemeNames, + environmentVariables: [], + extensionHostIDs: extensionHostIDs, + target: .mock(key: "A", productType: .messagesExtension), + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences + ), + .init( + commandLineArguments: [], + customSchemeNames: customSchemeNames, + environmentVariables: [ + .init(key: "VAR", value: "not enabled", enabled: false), + .init(key: "ENV VAR", value: "1", enabled: true), + ], + extensionHostIDs: extensionHostIDs, + target: .mock(key: "B", productType: .appExtension), + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences + ), + ] + let createTargetAutomaticSchemeInfos = + Generator.CreateTargetAutomaticSchemeInfos.mock( + targetSchemeInfos: [ + [.mock(name: "Scheme 4")], + [.mock(name: "Scheme Z"), .mock(name: "Scheme 2")], + [.mock(name: "Scheme 1")], + ] + ) + + let expectedSchemeInfos: [SchemeInfo] = [ + .mock(name: "Scheme 4"), + .mock(name: "Scheme Z"), + .mock(name: "Scheme 2"), + .mock(name: "Scheme 1"), + ] + + // Act + + let schemeInfos = try createAutomaticSchemeInfosWithDefaults( + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHostIDs: extensionHostIDs, + targets: targets, + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences, + createTargetAutomaticSchemeInfos: + createTargetAutomaticSchemeInfos.mock + ) + + // Assert + + XCTAssertNoDifference(schemeInfos, expectedSchemeInfos) + XCTAssertNoDifference( + createTargetAutomaticSchemeInfos.tracker.called, + expectedCreateTargetAutomaticSchemeInfosCalled + ) + } +} + +private func createAutomaticSchemeInfosWithDefaults( + autogenerationMode: AutogenerationMode = .all, + commandLineArguments: [TargetID: [CommandLineArgument]] = [:], + customSchemeNames: Set = [], + environmentVariables: [TargetID: [EnvironmentVariable]] = [:], + extensionHostIDs: [TargetID : [TargetID]] = [:], + targets: [Target], + targetsByID: [TargetID : Target] = [:], + targetsByKey: [Target.Key : Target] = [:], + transitivePreviewReferences: [TargetID: [BuildableReference]] = [:], + createTargetAutomaticSchemeInfos: Generator.CreateTargetAutomaticSchemeInfos +) throws -> [SchemeInfo] { + return try Generator.CreateAutomaticSchemeInfos.defaultCallable( + autogenerationMode: autogenerationMode, + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHostIDs: extensionHostIDs, + targets: targets, + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences, + createTargetAutomaticSchemeInfos: createTargetAutomaticSchemeInfos + ) +} diff --git a/tools/generators/xcschemes/test/CreateTargetAutomaticSchemeInfos+Testing.swift b/tools/generators/xcschemes/test/CreateTargetAutomaticSchemeInfos+Testing.swift new file mode 100644 index 0000000000..cefb5ce5c8 --- /dev/null +++ b/tools/generators/xcschemes/test/CreateTargetAutomaticSchemeInfos+Testing.swift @@ -0,0 +1,71 @@ +import PBXProj +import XCScheme + +@testable import xcschemes + +// MARK: - Generator.CreateTargetAutomaticSchemeInfos.mock + +extension Generator.CreateTargetAutomaticSchemeInfos { + final class MockTracker { + struct Called: Equatable { + let commandLineArguments: [CommandLineArgument] + let customSchemeNames: Set + let environmentVariables: [EnvironmentVariable] + let extensionHostIDs: [TargetID: [TargetID]] + let target: Target + let targetsByID: [TargetID: Target] + let targetsByKey: [Target.Key: Target] + let transitivePreviewReferences: [TargetID: [BuildableReference]] + } + + fileprivate(set) var called: [Called] = [] + + fileprivate var results: [[SchemeInfo]] + + init(results: [[SchemeInfo]]) { + self.results = results.reversed() + } + + func nextResult() -> [SchemeInfo] { + guard let result = results.popLast() else { + preconditionFailure("Called too many times") + } + return result + } + } + + static func mock( + targetSchemeInfos: [[SchemeInfo]] + ) -> (mock: Self, tracker: MockTracker) { + let mockTracker = MockTracker(results: targetSchemeInfos) + + let mocked = Self( + createAutomaticSchemeInfo: + Generator.Stubs.createAutomaticSchemeInfo, + callable: { + commandLineArguments, + customSchemeNames, + environmentVariables, + extensionHostIDs, + target, + targetsByID, + targetsByKey, + transitivePreviewReferences, + _ in + mockTracker.called.append(.init( + commandLineArguments: commandLineArguments, + customSchemeNames: customSchemeNames, + environmentVariables: environmentVariables, + extensionHostIDs: extensionHostIDs, + target: target, + targetsByID: targetsByID, + targetsByKey: targetsByKey, + transitivePreviewReferences: transitivePreviewReferences + )) + return mockTracker.nextResult() + } + ) + + return (mocked, mockTracker) + } +} diff --git a/tools/generators/xcschemes/test/GeneratorStubs.swift b/tools/generators/xcschemes/test/GeneratorStubs.swift new file mode 100644 index 0000000000..af091d08e3 --- /dev/null +++ b/tools/generators/xcschemes/test/GeneratorStubs.swift @@ -0,0 +1,10 @@ +import PBXProj + +@testable import xcschemes + +extension Generator { + enum Stubs { + static let createAutomaticSchemeInfo = + CreateAutomaticSchemeInfo.stub(schemeInfos: []) + } +} diff --git a/tools/generators/xcschemes/test/SchemeInfo+Testing.swift b/tools/generators/xcschemes/test/SchemeInfo+Testing.swift new file mode 100644 index 0000000000..0264eb79d0 --- /dev/null +++ b/tools/generators/xcschemes/test/SchemeInfo+Testing.swift @@ -0,0 +1,98 @@ +import XCScheme + +@testable import xcschemes + +extension SchemeInfo { + static func mock( + name: String, + test: Test = .mock(), + run: Run = .mock(), + profile: Profile = .mock(), + executionActions: [SchemeInfo.ExecutionAction] = [] + ) -> Self { + return Self( + name: name, + test: test, + run: run, + profile: profile, + executionActions: executionActions + ) + } +} + +extension SchemeInfo.Profile { + static func mock( + buildTargets: [Target] = [], + commandLineArguments: [CommandLineArgument] = [], + customWorkingDirectory: String? = nil, + environmentVariables: [EnvironmentVariable] = [], + launchTarget: SchemeInfo.LaunchTarget? = nil, + useRunArgsAndEnv: Bool = true, + xcodeConfiguration: String? = nil + ) -> Self { + return Self( + buildTargets: buildTargets, + commandLineArguments: commandLineArguments, + customWorkingDirectory: customWorkingDirectory, + environmentVariables: environmentVariables, + launchTarget: launchTarget, + useRunArgsAndEnv: useRunArgsAndEnv, + xcodeConfiguration: xcodeConfiguration + ) + } +} + +extension SchemeInfo.Run { + static func mock( + buildTargets: [Target] = [], + commandLineArguments: [CommandLineArgument] = [], + customWorkingDirectory: String? = nil, + enableAddressSanitizer: Bool = false, + enableThreadSanitizer: Bool = false, + enableUBSanitizer: Bool = false, + environmentVariables: [EnvironmentVariable] = [], + launchTarget: SchemeInfo.LaunchTarget? = nil, + transitivePreviewReferences: [BuildableReference] = [], + xcodeConfiguration: String? = nil + ) -> Self { + return Self( + buildTargets: buildTargets, + commandLineArguments: commandLineArguments, + customWorkingDirectory: customWorkingDirectory, + enableAddressSanitizer: enableAddressSanitizer, + enableThreadSanitizer: enableThreadSanitizer, + enableUBSanitizer: enableUBSanitizer, + environmentVariables: environmentVariables, + launchTarget: launchTarget, + transitivePreviewReferences: transitivePreviewReferences, + xcodeConfiguration: xcodeConfiguration + ) + } +} + +extension SchemeInfo.Test { + static func mock( + buildTargets: [Target] = [], + commandLineArguments: [CommandLineArgument] = [], + customWorkingDirectory: String? = nil, + enableAddressSanitizer: Bool = false, + enableThreadSanitizer: Bool = false, + enableUBSanitizer: Bool = false, + environmentVariables: [EnvironmentVariable] = [], + testTargets: [SchemeInfo.TestTarget] = [], + useRunArgsAndEnv: Bool = true, + xcodeConfiguration: String? = nil + ) -> Self { + return Self( + buildTargets: buildTargets, + commandLineArguments: commandLineArguments, + enableAddressSanitizer: enableAddressSanitizer, + enableThreadSanitizer: enableThreadSanitizer, + enableUBSanitizer: enableUBSanitizer, + environmentVariables: environmentVariables, + testTargets: testTargets, + useRunArgsAndEnv: useRunArgsAndEnv, + xcodeConfiguration: xcodeConfiguration + ) + } +} diff --git a/tools/generators/xcschemes/test/Target+Testing.swift b/tools/generators/xcschemes/test/Target+Testing.swift new file mode 100644 index 0000000000..5176161122 --- /dev/null +++ b/tools/generators/xcschemes/test/Target+Testing.swift @@ -0,0 +1,31 @@ +import PBXProj +import XCScheme + +@testable import xcschemes + +extension Target { + static func mock( + key: Target.Key, + productType: PBXProductType = .staticLibrary, + buildableReference: BuildableReference? = nil + ) -> Self { + return Self( + key: key, + productType: productType, + buildableReference: buildableReference ?? .mock(targetKey: key) + ) + } +} + +private extension BuildableReference { + static func mock(targetKey: Target.Key) -> Self { + let name = targetKey.sortedIds.map(\.rawValue).joined(separator: "_") + + return Self( + blueprintIdentifier: "\(name)_blueprintIdentifier", + buildableName: "\(name)_buildableName", + blueprintName: name, + referencedContainer: "\(name)_referencedContainer" + ) + } +}