diff --git a/Sources/Basics/Concurrency/ThreadSafeBox.swift b/Sources/Basics/Concurrency/ThreadSafeBox.swift index 75b9a1542a9..f719068b440 100644 --- a/Sources/Basics/Concurrency/ThreadSafeBox.swift +++ b/Sources/Basics/Concurrency/ThreadSafeBox.swift @@ -43,6 +43,18 @@ public final class ThreadSafeBox { return value } + @discardableResult + public func memoize(body: () async throws -> Value) async rethrows -> Value { + if let value = self.get() { + return value + } + let value = try await body() + self.lock.withLock { + self.underlying = value + } + return value + } + public func clear() { self.lock.withLock { self.underlying = nil diff --git a/Sources/Basics/FileSystem/AbsolutePath.swift b/Sources/Basics/FileSystem/AbsolutePath.swift index 4f4034d08f3..86f9ea2b9f1 100644 --- a/Sources/Basics/FileSystem/AbsolutePath.swift +++ b/Sources/Basics/FileSystem/AbsolutePath.swift @@ -40,7 +40,7 @@ public struct AbsolutePath: Hashable, Sendable { /// Root directory (whose string representation is just a path separator). public static let root = Self(TSCAbsolutePath.root) - internal let underlying: TSCAbsolutePath + package let underlying: TSCAbsolutePath // public for transition public init(_ underlying: TSCAbsolutePath) { diff --git a/Sources/Build/BuildOperation.swift b/Sources/Build/BuildOperation.swift index 4aa2efec7e0..85ea409137b 100644 --- a/Sources/Build/BuildOperation.swift +++ b/Sources/Build/BuildOperation.swift @@ -144,7 +144,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS private let config: LLBuildSystemConfiguration /// The closure for loading the package graph. - let packageGraphLoader: () throws -> ModulesGraph + let packageGraphLoader: () async throws -> ModulesGraph /// the plugin configuration for build plugins let pluginConfiguration: PluginConfiguration? @@ -186,7 +186,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } public var builtTestProducts: [BuiltTestProduct] { - (try? getBuildDescription())?.builtTestProducts ?? [] + get async { + (try? await getBuildDescription())?.builtTestProducts ?? [] + } } /// File rules to determine resource handling behavior. @@ -205,7 +207,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS productsBuildParameters: BuildParameters, toolsBuildParameters: BuildParameters, cacheBuildManifest: Bool, - packageGraphLoader: @escaping () throws -> ModulesGraph, + packageGraphLoader: @escaping () async throws -> ModulesGraph, pluginConfiguration: PluginConfiguration? = .none, scratchDirectory: AbsolutePath, additionalFileRules: [FileRuleDescription], @@ -280,9 +282,11 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS self.rootPackageIdentityByTargetName = (try? Dictionary(throwingUniqueKeysWithValues: targetsByRootPackageIdentity.lazy.flatMap { e in e.value.map { ($0, e.key) } })) ?? [:] } - public func getPackageGraph() throws -> ModulesGraph { - try self.packageGraph.memoize { - try self.packageGraphLoader() + public var modulesGraph: ModulesGraph { + get async throws { + try await self.packageGraph.memoize { + try await self.packageGraphLoader() + } } } @@ -290,8 +294,8 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS /// /// This will try skip build planning if build manifest caching is enabled /// and the package structure hasn't changed. - public func getBuildDescription(subset: BuildSubset? = nil) throws -> BuildDescription { - return try self.buildDescription.memoize { + public func getBuildDescription(subset: BuildSubset? = nil) async throws -> BuildDescription { + return try await self.buildDescription.memoize { if self.cacheBuildManifest { do { // if buildPackageStructure returns a valid description we use that, otherwise we perform full planning @@ -320,12 +324,12 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } } // We need to perform actual planning if we reach here. - return try self.plan(subset: subset).description + return try await self.plan(subset: subset).description } } - public func getBuildManifest() throws -> LLBuildManifest { - return try self.plan().manifest + public func getBuildManifest() async throws -> LLBuildManifest { + return try await self.plan().manifest } /// Cancel the active build operation. @@ -475,7 +479,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Perform a build using the given build description and subset. - public func build(subset: BuildSubset) throws { + public func build(subset: BuildSubset) async throws { guard !self.config.shouldSkipBuilding(for: .target) else { return } @@ -485,7 +489,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // Get the build description (either a cached one or newly created). // Get the build description - let buildDescription = try getBuildDescription(subset: subset) + let buildDescription = try await getBuildDescription(subset: subset) // Verify dependency imports on the described targets try verifyTargetImports(in: buildDescription) @@ -500,7 +504,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS // If any plugins are part of the build set, compile them now to surface // any errors up-front. Returns true if we should proceed with the build // or false if not. It will already have thrown any appropriate error. - guard try self.compilePlugins(in: subset) else { + guard try await self.compilePlugins(in: subset) else { return } @@ -509,7 +513,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS progressTracker.buildStart(configuration: configuration) // Perform the build. - let llbuildTarget = try computeLLBuildTargetName(for: subset) + let llbuildTarget = try await computeLLBuildTargetName(for: subset) let success = buildSystem.build(target: llbuildTarget) let duration = buildStartTime.distance(to: .now()) @@ -566,10 +570,10 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS /// true if the build should proceed. Throws an error in case of failure. A /// reason why the build might not proceed even on success is if only plugins /// should be compiled. - func compilePlugins(in subset: BuildSubset) throws -> Bool { + func compilePlugins(in subset: BuildSubset) async throws -> Bool { // Figure out what, if any, plugin descriptions to compile, and whether // to continue building after that based on the subset. - let allPlugins = try getBuildDescription().pluginDescriptions + let allPlugins = try await getBuildDescription().pluginDescriptions let pluginsToCompile: [PluginBuildDescription] let continueBuilding: Bool switch subset { @@ -662,7 +666,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Compute the llbuild target name using the given subset. - func computeLLBuildTargetName(for subset: BuildSubset) throws -> String { + func computeLLBuildTargetName(for subset: BuildSubset) async throws -> String { switch subset { case .allExcludingTests: return LLBuildManifestBuilder.TargetKind.main.targetName @@ -670,7 +674,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS return LLBuildManifestBuilder.TargetKind.test.targetName case .product(let productName, let destination): // FIXME: This is super unfortunate that we might need to load the package graph. - let graph = try getPackageGraph() + let graph = try await self.modulesGraph let buildTriple: BuildTriple? = if let destination { destination == .host ? .tools : .destination @@ -704,7 +708,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS return try product.getLLBuildTargetName(buildParameters: buildParameters) case .target(let targetName, let destination): // FIXME: This is super unfortunate that we might need to load the package graph. - let graph = try getPackageGraph() + let graph = try await self.modulesGraph let buildTriple: BuildTriple? = if let destination { destination == .host ? .tools : .destination @@ -731,9 +735,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS } /// Create the build plan and return the build description. - private func plan(subset: BuildSubset? = nil) throws -> (description: BuildDescription, manifest: LLBuildManifest) { + private func plan(subset: BuildSubset? = nil) async throws -> BuildManifestDescription { // Load the package graph. - let graph = try getPackageGraph() + let graph = try await self.modulesGraph let buildToolPluginInvocationResults: [ResolvedModule.ID: (target: ResolvedModule, results: [BuildToolPluginInvocationResult])] let prebuildCommandResults: [ResolvedModule.ID: [PrebuildCommandResult]] // Invoke any build tool plugins in the graph to generate prebuild commands and build commands. @@ -748,7 +752,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS hostTriple: try pluginConfiguration.scriptRunner.hostTriple ) - buildToolPluginInvocationResults = try graph.invokeBuildToolPlugins( + buildToolPluginInvocationResults = try await graph.invokeBuildToolPlugins( pluginsPerTarget: pluginsPerModule, pluginTools: pluginTools, outputDir: pluginConfiguration.workDirectory.appending("outputs"), @@ -859,7 +863,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS ) // Finally create the llbuild manifest from the plan. - return (buildDescription, buildManifest) + return .init(description: buildDescription, manifest: buildManifest) } /// Build the package structure target. @@ -987,7 +991,16 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS public func packageStructureChanged() -> Bool { do { - _ = try self.plan() + _ = try temp_await { (callback: @escaping (Result) -> Void) in + _Concurrency.Task { + do { + let value = try await self.plan() + callback(.success(value)) + } catch { + callback(.failure(error)) + } + } + } } catch Diagnostics.fatalError { return false diff --git a/Sources/Commands/PackageCommands/APIDiff.swift b/Sources/Commands/PackageCommands/APIDiff.swift index aba5117002a..8bb505285a6 100644 --- a/Sources/Commands/PackageCommands/APIDiff.swift +++ b/Sources/Commands/PackageCommands/APIDiff.swift @@ -32,7 +32,7 @@ struct DeprecatedAPIDiff: ParsableCommand { } } -struct APIDiff: SwiftCommand { +struct APIDiff: AsyncSwiftCommand { static let configuration = CommandConfiguration( commandName: "diagnose-api-breaking-changes", abstract: "Diagnose API-breaking changes to Swift modules in a package", @@ -77,7 +77,7 @@ struct APIDiff: SwiftCommand { @Flag(help: "Regenerate the API baseline, even if an existing one is available.") var regenerateBaseline: Bool = false - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { let apiDigesterPath = try swiftCommandState.getTargetToolchain().getSwiftAPIDigester() let apiDigesterTool = SwiftAPIDigester(fileSystem: swiftCommandState.fileSystem, tool: apiDigesterPath) @@ -92,14 +92,14 @@ struct APIDiff: SwiftCommand { cacheBuildManifest: false ) - let packageGraph = try buildSystem.getPackageGraph() + let packageGraph = try await buildSystem.modulesGraph let modulesToDiff = try determineModulesToDiff( packageGraph: packageGraph, observabilityScope: swiftCommandState.observabilityScope ) // Build the current package. - try buildSystem.build() + try await buildSystem.build() // Dump JSON for the baseline package. let baselineDumper = try APIDigesterBaselineDumper( @@ -111,7 +111,7 @@ struct APIDiff: SwiftCommand { observabilityScope: swiftCommandState.observabilityScope ) - let baselineDir = try baselineDumper.emitAPIBaseline( + let baselineDir = try await baselineDumper.emitAPIBaseline( for: modulesToDiff, at: overrideBaselineDir, force: regenerateBaseline, diff --git a/Sources/Commands/PackageCommands/DumpCommands.swift b/Sources/Commands/PackageCommands/DumpCommands.swift index 920e8c9c575..d6ab1ae144e 100644 --- a/Sources/Commands/PackageCommands/DumpCommands.swift +++ b/Sources/Commands/PackageCommands/DumpCommands.swift @@ -17,7 +17,7 @@ import Foundation import PackageModel import XCBuildSupport -struct DumpSymbolGraph: SwiftCommand { +struct DumpSymbolGraph: AsyncSwiftCommand { static let configuration = CommandConfiguration( abstract: "Dump Symbol Graph") static let defaultMinimumAccessLevel = SymbolGraphExtract.AccessLevel.public @@ -43,7 +43,7 @@ struct DumpSymbolGraph: SwiftCommand { @Flag(help: "Emit extension block symbols for extensions to external types or directly associate members and conformances with the extended nominal.") var extensionBlockSymbolBehavior: ExtensionBlockSymbolBehavior = .omitExtensionBlockSymbols - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { // Build the current package. // // We turn build manifest caching off because we need the build plan. @@ -53,7 +53,7 @@ struct DumpSymbolGraph: SwiftCommand { traitConfiguration: .init(enableAllTraits: true), cacheBuildManifest: false ) - try buildSystem.build() + try await buildSystem.build() // Configure the symbol graph extractor. let symbolGraphExtractor = try SymbolGraphExtract( @@ -71,7 +71,7 @@ struct DumpSymbolGraph: SwiftCommand { // Run the tool once for every library and executable target in the root package. let buildPlan = try buildSystem.buildPlan let symbolGraphDirectory = buildPlan.destinationBuildParameters.dataPath.appending("symbolgraph") - let targets = try buildSystem.getPackageGraph().rootPackages.flatMap{ $0.modules }.filter{ $0.type == .library } + let targets = try await buildSystem.getPackageGraph().rootPackages.flatMap{ $0.modules }.filter{ $0.type == .library } for target in targets { print("-- Emitting symbol graph for", target.name) let result = try symbolGraphExtractor.extractSymbolGraph( diff --git a/Sources/Commands/PackageCommands/InstalledPackages.swift b/Sources/Commands/PackageCommands/InstalledPackages.swift index 95d0047cb6f..7187b27b015 100644 --- a/Sources/Commands/PackageCommands/InstalledPackages.swift +++ b/Sources/Commands/PackageCommands/InstalledPackages.swift @@ -18,7 +18,7 @@ import PackageModel import TSCBasic extension SwiftPackageCommand { - struct Install: SwiftCommand { + struct Install: AsyncSwiftCommand { static let configuration = CommandConfiguration( commandName: "experimental-install", abstract: "Offers the ability to install executable products of the current package." @@ -30,7 +30,7 @@ extension SwiftPackageCommand { @Option(help: "The name of the executable product to install") var product: String? - func run(_ tool: SwiftCommandState) throws { + func run(_ tool: SwiftCommandState) async throws { let swiftpmBinDir = try tool.fileSystem.getOrCreateSwiftPMInstalledBinariesDirectory() let env = Environment.current @@ -81,7 +81,7 @@ extension SwiftPackageCommand { throw StringError("\(productToInstall.name) is already installed at \(existingPkg.path)") } - try tool.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init()) + try await tool.createBuildSystem(explicitProduct: productToInstall.name, traitConfiguration: .init()) .build(subset: .product(productToInstall.name)) let binPath = try tool.productsBuildParameters.buildPath.appending(component: productToInstall.name) diff --git a/Sources/Commands/PackageCommands/Learn.swift b/Sources/Commands/PackageCommands/Learn.swift index 3632c297d6e..017c29904dc 100644 --- a/Sources/Commands/PackageCommands/Learn.swift +++ b/Sources/Commands/PackageCommands/Learn.swift @@ -17,8 +17,7 @@ import PackageGraph import PackageModel extension SwiftPackageCommand { - struct Learn: SwiftCommand { - + struct Learn: AsyncSwiftCommand { @OptionGroup() var globalOptions: GlobalOptions @@ -90,7 +89,7 @@ extension SwiftPackageCommand { return snippetGroups.filter { !$0.snippets.isEmpty } } - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { let graph = try swiftCommandState.loadPackageGraph() let package = graph.rootPackages[graph.rootPackages.startIndex] print(package.products.map { $0.description }) @@ -99,7 +98,7 @@ extension SwiftPackageCommand { var cardStack = CardStack(package: package, snippetGroups: snippetGroups, swiftCommandState: swiftCommandState) - cardStack.run() + await cardStack.run() } } } diff --git a/Sources/Commands/PackageCommands/PluginCommand.swift b/Sources/Commands/PackageCommands/PluginCommand.swift index 57b78c67b89..dd7fb066b54 100644 --- a/Sources/Commands/PackageCommands/PluginCommand.swift +++ b/Sources/Commands/PackageCommands/PluginCommand.swift @@ -19,7 +19,7 @@ import PackageGraph import PackageModel -struct PluginCommand: SwiftCommand { +struct PluginCommand: AsyncSwiftCommand { static let configuration = CommandConfiguration( commandName: "plugin", abstract: "Invoke a command plugin or perform other actions on command plugins" @@ -137,7 +137,7 @@ struct PluginCommand: SwiftCommand { ) var arguments: [String] = [] - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { // Check for a missing plugin command verb. if self.command == "" && !self.listCommands { throw ValidationError("Missing expected plugin command") @@ -166,7 +166,7 @@ struct PluginCommand: SwiftCommand { return } - try Self.run( + try await Self.run( command: self.command, options: self.pluginOptions, arguments: self.arguments, @@ -179,7 +179,7 @@ struct PluginCommand: SwiftCommand { options: PluginOptions, arguments: [String], swiftCommandState: SwiftCommandState - ) throws { + ) async throws { // Load the workspace and resolve the package graph. let packageGraph = try swiftCommandState.loadPackageGraph() @@ -203,7 +203,7 @@ struct PluginCommand: SwiftCommand { .shouldDisableSandbox // At this point we know we found exactly one command plugin, so we run it. In SwiftPM CLI, we have only one root package. - try PluginCommand.run( + try await PluginCommand.run( plugin: matchingPlugins[0], package: packageGraph.rootPackages[packageGraph.rootPackages.startIndex], packageGraph: packageGraph, @@ -220,7 +220,7 @@ struct PluginCommand: SwiftCommand { options: PluginOptions, arguments: [String], swiftCommandState: SwiftCommandState - ) throws { + ) async throws { let pluginTarget = plugin.underlying as! PluginModule swiftCommandState.observabilityScope @@ -339,7 +339,7 @@ struct PluginCommand: SwiftCommand { for: try pluginScriptRunner.hostTriple ) { name, _ in // Build the product referenced by the tool, and add the executable to the tool map. Product dependencies are not supported within a package, so if the tool happens to be from the same package, we instead find the executable that corresponds to the product. There is always one, because of autogeneration of implicit executables with the same name as the target if there isn't an explicit one. - try buildSystem.build(subset: .product(name, for: .host)) + try await buildSystem.build(subset: .product(name, for: .host)) if let builtTool = try buildSystem.buildPlan.buildProducts.first(where: { $0.product.name == name && $0.buildParameters.destination == .host }) { diff --git a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift index c73d695d50a..26fe0f2c034 100644 --- a/Sources/Commands/PackageCommands/SwiftPackageCommand.swift +++ b/Sources/Commands/PackageCommands/SwiftPackageCommand.swift @@ -86,7 +86,7 @@ public struct SwiftPackageCommand: AsyncParsableCommand { extension SwiftPackageCommand { // This command is the default when no other subcommand is passed. It is not shown in the help and is never invoked // directly. - struct DefaultCommand: SwiftCommand { + struct DefaultCommand: AsyncSwiftCommand { static let configuration = CommandConfiguration( commandName: nil, shouldDisplay: false @@ -101,7 +101,7 @@ extension SwiftPackageCommand { @Argument(parsing: .captureForPassthrough) var remaining: [String] = [] - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { // See if have a possible plugin command. guard let command = remaining.first else { print(SwiftPackageCommand.helpMessage()) @@ -116,7 +116,7 @@ extension SwiftPackageCommand { } // Otherwise see if we can find a plugin. - try PluginCommand.run( + try await PluginCommand.run( command: command, options: self.pluginOptions, arguments: self.remaining, diff --git a/Sources/Commands/Snippets/Card.swift b/Sources/Commands/Snippets/Card.swift index 5176b554f86..cb12114149f 100644 --- a/Sources/Commands/Snippets/Card.swift +++ b/Sources/Commands/Snippets/Card.swift @@ -18,7 +18,7 @@ protocol Card { /// Accept a line of input from the user's terminal and provide /// an optional ``CardEvent`` which can alter the card stack. - func acceptLineInput(_ line: S) -> CardEvent? + func acceptLineInput(_ line: some StringProtocol) async -> CardEvent? /// The input prompt to present to the user when accepting a line of input. var inputPrompt: String? { get } diff --git a/Sources/Commands/Snippets/CardStack.swift b/Sources/Commands/Snippets/CardStack.swift index 72d9c5d0c5b..0b7c24f5b94 100644 --- a/Sources/Commands/Snippets/CardStack.swift +++ b/Sources/Commands/Snippets/CardStack.swift @@ -67,7 +67,7 @@ struct CardStack { return readLine(strippingNewline: true) } - mutating func run() { + mutating func run() async { var inputFinished = false while !inputFinished { guard let top = cards.last else { @@ -91,7 +91,7 @@ struct CardStack { .reversed() .drop { $0.isWhitespace } .reversed()) - let response = top.acceptLineInput(trimmedLine) + let response = await top.acceptLineInput(trimmedLine) switch response { case .none: continue askForLine diff --git a/Sources/Commands/Snippets/Cards/SnippetCard.swift b/Sources/Commands/Snippets/Cards/SnippetCard.swift index 441cc14080a..eb660c999be 100644 --- a/Sources/Commands/Snippets/Cards/SnippetCard.swift +++ b/Sources/Commands/Snippets/Cards/SnippetCard.swift @@ -67,7 +67,7 @@ struct SnippetCard: Card { return "\nRun this snippet? [R: run, or press Enter to return]" } - func acceptLineInput(_ line: S) -> CardEvent? where S : StringProtocol { + func acceptLineInput(_ line: some StringProtocol) async -> CardEvent? { let trimmed = line.drop { $0.isWhitespace }.prefix { !$0.isWhitespace }.lowercased() guard !trimmed.isEmpty else { return .pop() @@ -76,7 +76,7 @@ struct SnippetCard: Card { switch trimmed { case "r", "run": do { - try runExample() + try await runExample() } catch { return .pop(SnippetCard.Error.cantRunSnippet(reason: error.localizedDescription)) } @@ -91,12 +91,12 @@ struct SnippetCard: Card { return .pop() } - func runExample() throws { + func runExample() async throws { print("Building '\(snippet.path)'\n") let buildSystem = try swiftCommandState.createBuildSystem(explicitProduct: snippet.name, traitConfiguration: .init()) try buildSystem.build(subset: .product(snippet.name)) let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: snippet.name) - if let exampleTarget = try buildSystem.getPackageGraph().module(for: snippet.name, destination: .destination) { + if let exampleTarget = try await buildSystem.getPackageGraph().module(for: snippet.name, destination: .destination) { try ProcessEnv.chdir(exampleTarget.sources.paths[0].parentDirectory) } try exec(path: executablePath.pathString, args: []) diff --git a/Sources/Commands/SwiftBuildCommand.swift b/Sources/Commands/SwiftBuildCommand.swift index f071a9e599e..a4d4a360317 100644 --- a/Sources/Commands/SwiftBuildCommand.swift +++ b/Sources/Commands/SwiftBuildCommand.swift @@ -150,7 +150,7 @@ public struct SwiftBuildCommand: AsyncSwiftCommand { ) as? BuildOperation else { throw StringError("asked for native build system but did not get it") } - let buildManifest = try buildOperation.getBuildManifest() + let buildManifest = try await buildOperation.getBuildManifest() var serializer = DOTManifestSerializer(manifest: buildManifest) // print to stdout let outputStream = stdoutStream @@ -190,10 +190,10 @@ public struct SwiftBuildCommand: AsyncSwiftCommand { for library in try options.testLibraryOptions.enabledTestingLibraries(swiftCommandState: swiftCommandState) { updateTestingParameters(of: &productsBuildParameters, library: library) updateTestingParameters(of: &toolsBuildParameters, library: library) - try build(swiftCommandState, subset: subset, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters) + try await build(swiftCommandState, subset: subset, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters) } } else { - try build(swiftCommandState, subset: subset, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters) + try await build(swiftCommandState, subset: subset, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters) } } @@ -202,7 +202,7 @@ public struct SwiftBuildCommand: AsyncSwiftCommand { subset: BuildSubset, productsBuildParameters: BuildParameters, toolsBuildParameters: BuildParameters - ) throws { + ) async throws { let buildSystem = try swiftCommandState.createBuildSystem( explicitProduct: options.product, traitConfiguration: .init(traitOptions: self.options.traits), @@ -214,7 +214,7 @@ public struct SwiftBuildCommand: AsyncSwiftCommand { outputStream: TSCBasic.stdoutStream ) do { - try buildSystem.build(subset: subset) + try await buildSystem.build(subset: subset) } catch _ as Diagnostics { throw ExitCode.failure } diff --git a/Sources/Commands/SwiftRunCommand.swift b/Sources/Commands/SwiftRunCommand.swift index 4dc33e4b8d6..a93e5fc716e 100644 --- a/Sources/Commands/SwiftRunCommand.swift +++ b/Sources/Commands/SwiftRunCommand.swift @@ -140,7 +140,7 @@ public struct SwiftRunCommand: AsyncSwiftCommand { ) // Perform build. - try buildSystem.build() + try await buildSystem.build() // Execute the REPL. let arguments = try buildSystem.buildPlan.createREPLArguments() @@ -158,11 +158,11 @@ public struct SwiftRunCommand: AsyncSwiftCommand { explicitProduct: options.executable, traitConfiguration: .init(traitOptions: self.options.traits) ) - let productName = try findProductName(in: buildSystem.getPackageGraph()) + let productName = try await findProductName(in: buildSystem.getPackageGraph()) if options.shouldBuildTests { - try buildSystem.build(subset: .allIncludingTests) + try await buildSystem.build(subset: .allIncludingTests) } else if options.shouldBuild { - try buildSystem.build(subset: .product(productName)) + try await buildSystem.build(subset: .product(productName)) } let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: productName) @@ -203,11 +203,11 @@ public struct SwiftRunCommand: AsyncSwiftCommand { explicitProduct: options.executable, traitConfiguration: .init(traitOptions: self.options.traits) ) - let productName = try findProductName(in: buildSystem.getPackageGraph()) + let productName = try await findProductName(in: buildSystem.getPackageGraph()) if options.shouldBuildTests { - try buildSystem.build(subset: .allIncludingTests) + try await buildSystem.build(subset: .allIncludingTests) } else if options.shouldBuild { - try buildSystem.build(subset: .product(productName)) + try await buildSystem.build(subset: .product(productName)) } let executablePath = try swiftCommandState.productsBuildParameters.buildPath.appending(component: productName) diff --git a/Sources/Commands/SwiftTestCommand.swift b/Sources/Commands/SwiftTestCommand.swift index 6c8befaec01..60018b2a10d 100644 --- a/Sources/Commands/SwiftTestCommand.swift +++ b/Sources/Commands/SwiftTestCommand.swift @@ -244,7 +244,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { _ = try? localFileSystem.removeFileTree(productsBuildParameters.testOutputPath) } - let testProducts = try buildTestsIfNeeded(swiftCommandState: swiftCommandState, library: .xctest) + let testProducts = try await buildTestsIfNeeded(swiftCommandState: swiftCommandState, library: .xctest) if !self.options.shouldRunInParallel { let xctestArgs = try xctestArgs(for: testProducts, swiftCommandState: swiftCommandState) try await runTestProducts( @@ -368,7 +368,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { private func swiftTestingRun(_ swiftCommandState: SwiftCommandState) async throws { let (productsBuildParameters, _) = try swiftCommandState.buildParametersForTest(options: self.options, library: .swiftTesting) - let testProducts = try buildTestsIfNeeded(swiftCommandState: swiftCommandState, library: .swiftTesting) + let testProducts = try await buildTestsIfNeeded(swiftCommandState: swiftCommandState, library: .swiftTesting) let additionalArguments = Array(CommandLine.arguments.dropFirst()) try await runTestProducts( testProducts, @@ -395,7 +395,7 @@ public struct SwiftTestCommand: AsyncSwiftCommand { } else if self.options._deprecated_shouldListTests { // backward compatibility 6/2022 for deprecation of flag into a subcommand let command = try List.parse() - try command.run(swiftCommandState) + try await command.run(swiftCommandState) } else { if try options.testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) { try await swiftTestingRun(swiftCommandState) @@ -567,9 +567,9 @@ public struct SwiftTestCommand: AsyncSwiftCommand { private func buildTestsIfNeeded( swiftCommandState: SwiftCommandState, library: BuildParameters.Testing.Library - ) throws -> [BuiltTestProduct] { + ) async throws -> [BuiltTestProduct] { let (productsBuildParameters, toolsBuildParameters) = try swiftCommandState.buildParametersForTest(options: self.options, library: library) - return try Commands.buildTestsIfNeeded( + return try await Commands.buildTestsIfNeeded( swiftCommandState: swiftCommandState, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters, @@ -641,7 +641,7 @@ extension SwiftTestCommand { } } - struct List: SwiftCommand { + struct List: AsyncSwiftCommand { static let configuration = CommandConfiguration( abstract: "Lists test methods in specifier format" ) @@ -665,13 +665,13 @@ extension SwiftTestCommand { // MARK: - XCTest - private func xctestRun(_ swiftCommandState: SwiftCommandState) throws { + private func xctestRun(_ swiftCommandState: SwiftCommandState) async throws { let (productsBuildParameters, toolsBuildParameters) = try swiftCommandState.buildParametersForTest( enableCodeCoverage: false, shouldSkipBuilding: sharedOptions.shouldSkipBuilding, library: .xctest ) - let testProducts = try buildTestsIfNeeded( + let testProducts = try await buildTestsIfNeeded( swiftCommandState: swiftCommandState, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters @@ -693,13 +693,13 @@ extension SwiftTestCommand { // MARK: - swift-testing - private func swiftTestingRun(_ swiftCommandState: SwiftCommandState) throws { + private func swiftTestingRun(_ swiftCommandState: SwiftCommandState) async throws { let (productsBuildParameters, toolsBuildParameters) = try swiftCommandState.buildParametersForTest( enableCodeCoverage: false, shouldSkipBuilding: sharedOptions.shouldSkipBuilding, library: .swiftTesting ) - let testProducts = try buildTestsIfNeeded( + let testProducts = try await buildTestsIfNeeded( swiftCommandState: swiftCommandState, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters @@ -737,12 +737,12 @@ extension SwiftTestCommand { // MARK: - Common implementation - func run(_ swiftCommandState: SwiftCommandState) throws { + func run(_ swiftCommandState: SwiftCommandState) async throws { if try testLibraryOptions.enableSwiftTestingLibrarySupport(swiftCommandState: swiftCommandState) { - try swiftTestingRun(swiftCommandState) + try await swiftTestingRun(swiftCommandState) } if testLibraryOptions.enableXCTestSupport { - try xctestRun(swiftCommandState) + try await xctestRun(swiftCommandState) } } @@ -750,8 +750,8 @@ extension SwiftTestCommand { swiftCommandState: SwiftCommandState, productsBuildParameters: BuildParameters, toolsBuildParameters: BuildParameters - ) throws -> [BuiltTestProduct] { - return try Commands.buildTestsIfNeeded( + ) async throws -> [BuiltTestProduct] { + return try await Commands.buildTestsIfNeeded( swiftCommandState: swiftCommandState, productsBuildParameters: productsBuildParameters, toolsBuildParameters: toolsBuildParameters, @@ -1383,7 +1383,7 @@ private func buildTestsIfNeeded( toolsBuildParameters: BuildParameters, testProduct: String?, traitConfiguration: TraitConfiguration -) throws -> [BuiltTestProduct] { +) async throws -> [BuiltTestProduct] { let buildSystem = try swiftCommandState.createBuildSystem( traitConfiguration: traitConfiguration, productsBuildParameters: productsBuildParameters, @@ -1396,10 +1396,10 @@ private func buildTestsIfNeeded( .allIncludingTests } - try buildSystem.build(subset: subset) + try await buildSystem.build(subset: subset) // Find the test product. - let testProducts = buildSystem.builtTestProducts + let testProducts = await buildSystem.builtTestProducts guard !testProducts.isEmpty else { if let testProduct { throw TestError.productIsNotTest(productName: testProduct) diff --git a/Sources/Commands/Utilities/APIDigester.swift b/Sources/Commands/Utilities/APIDigester.swift index 49cba05eced..6692d74464d 100644 --- a/Sources/Commands/Utilities/APIDigester.swift +++ b/Sources/Commands/Utilities/APIDigester.swift @@ -74,7 +74,7 @@ struct APIDigesterBaselineDumper { force: Bool, logLevel: Basics.Diagnostic.Severity, swiftCommandState: SwiftCommandState - ) throws -> AbsolutePath { + ) async throws -> AbsolutePath { var modulesToDiff = modulesToDiff let apiDiffDir = productsBuildParameters.apiDiff let baselineDir = (baselineDir ?? apiDiffDir).appending(component: baselineRevision.identifier) @@ -145,7 +145,7 @@ struct APIDigesterBaselineDumper { toolsBuildParameters: toolsBuildParameters, packageGraphLoader: { graph } ) - try buildSystem.build() + try await buildSystem.build() // Dump the SDK JSON. try swiftCommandState.fileSystem.createDirectory(baselineDir, recursive: true) diff --git a/Sources/Commands/Utilities/PluginDelegate.swift b/Sources/Commands/Utilities/PluginDelegate.swift index c9e1fbd69a9..ccb04d4cb4e 100644 --- a/Sources/Commands/Utilities/PluginDelegate.swift +++ b/Sources/Commands/Utilities/PluginDelegate.swift @@ -61,15 +61,9 @@ final class PluginDelegate: PluginInvocationDelegate { func pluginRequestedBuildOperation( subset: PluginInvocationBuildSubset, - parameters: PluginInvocationBuildParameters, - completion: @escaping (Result) -> Void - ) { - // Run the build in the background and call the completion handler when done. - DispatchQueue.sharedConcurrent.async { - completion(Result { - return try self.performBuildForPlugin(subset: subset, parameters: parameters) - }) - } + parameters: PluginInvocationBuildParameters + ) async throws -> PluginInvocationBuildResult { + try await self.performBuildForPlugin(subset: subset, parameters: parameters) } class TeeOutputByteStream: OutputByteStream { @@ -109,7 +103,7 @@ final class PluginDelegate: PluginInvocationDelegate { private func performBuildForPlugin( subset: PluginInvocationBuildSubset, parameters: PluginInvocationBuildParameters - ) throws -> PluginInvocationBuildResult { + ) async throws -> PluginInvocationBuildResult { // Configure the build parameters. var buildParameters = try self.swiftCommandState.productsBuildParameters switch parameters.configuration { @@ -169,7 +163,7 @@ final class PluginDelegate: PluginInvocationDelegate { ) // Run the build. This doesn't return until the build is complete. - let success = buildSystem.buildIgnoringError(subset: buildSubset) + let success = await buildSystem.buildIgnoringError(subset: buildSubset) // Create and return the build result record based on what the delegate collected and what's in the build plan. let builtProducts = try buildSystem.buildPlan.buildProducts.filter { @@ -203,21 +197,15 @@ final class PluginDelegate: PluginInvocationDelegate { func pluginRequestedTestOperation( subset: PluginInvocationTestSubset, - parameters: PluginInvocationTestParameters, - completion: @escaping (Result - ) -> Void) { - // Run the test in the background and call the completion handler when done. - DispatchQueue.sharedConcurrent.async { - completion(Result { - return try self.performTestsForPlugin(subset: subset, parameters: parameters) - }) - } + parameters: PluginInvocationTestParameters + ) async throws -> PluginInvocationTestResult { + try await self.performTestsForPlugin(subset: subset, parameters: parameters) } func performTestsForPlugin( subset: PluginInvocationTestSubset, parameters: PluginInvocationTestParameters - ) throws -> PluginInvocationTestResult { + ) async throws -> PluginInvocationTestResult { // Build the tests. Ideally we should only build those that match the subset, but we don't have a way to know // which ones they are until we've built them and can examine the binaries. let toolchain = try swiftCommandState.getHostToolchain() @@ -228,7 +216,7 @@ final class PluginDelegate: PluginInvocationDelegate { traitConfiguration: .init(), toolsBuildParameters: toolsBuildParameters ) - try buildSystem.build(subset: .allIncludingTests) + try await buildSystem.build(subset: .allIncludingTests) // Clean out the code coverage directory that may contain stale `profraw` files from a previous run of // the code coverage tool. @@ -247,7 +235,7 @@ final class PluginDelegate: PluginInvocationDelegate { // Iterate over the tests and run those that match the filter. var testTargetResults: [PluginInvocationTestResult.TestTarget] = [] var numFailedTests = 0 - for testProduct in buildSystem.builtTestProducts { + for testProduct in await buildSystem.builtTestProducts { // Get the test suites in the bundle. Each is just a container for test cases. let testSuites = try TestingSupport.getTestSuites( fromTestAt: testProduct.bundlePath, @@ -334,17 +322,17 @@ final class PluginDelegate: PluginInvocationDelegate { llvmProfCommand.append(filePath.pathString) } llvmProfCommand += ["-o", mergedCovFile.pathString] - try AsyncProcess.checkNonZeroExit(arguments: llvmProfCommand) + try await AsyncProcess.checkNonZeroExit(arguments: llvmProfCommand) // Use `llvm-cov` to export the merged `.profdata` file contents in JSON form. var llvmCovCommand = [try toolchain.getLLVMCov().pathString] llvmCovCommand += ["export", "-instr-profile=\(mergedCovFile.pathString)"] - for product in buildSystem.builtTestProducts { + for product in await buildSystem.builtTestProducts { llvmCovCommand.append("-object") llvmCovCommand.append(product.binaryPath.pathString) } // We get the output on stdout, and have to write it to a JSON ourselves. - let jsonOutput = try AsyncProcess.checkNonZeroExit(arguments: llvmCovCommand) + let jsonOutput = try await AsyncProcess.checkNonZeroExit(arguments: llvmCovCommand) let jsonCovFile = toolsBuildParameters.codeCovDataFile.parentDirectory.appending( component: toolsBuildParameters.codeCovDataFile.basenameWithoutExt + ".json" ) @@ -365,22 +353,16 @@ final class PluginDelegate: PluginInvocationDelegate { } func pluginRequestedSymbolGraph( - forTarget targetName: String, - options: PluginInvocationSymbolGraphOptions, - completion: @escaping (Result) -> Void - ) { - // Extract the symbol graph in the background and call the completion handler when done. - DispatchQueue.sharedConcurrent.async { - completion(Result { - return try self.createSymbolGraphForPlugin(forTarget: targetName, options: options) - }) - } + forTarget name: String, + options: PluginInvocationSymbolGraphOptions + ) async throws -> PluginInvocationSymbolGraphResult { + try await self.createSymbolGraphForPlugin(forTarget: name, options: options) } private func createSymbolGraphForPlugin( forTarget targetName: String, options: PluginInvocationSymbolGraphOptions - ) throws -> PluginInvocationSymbolGraphResult { + ) async throws -> PluginInvocationSymbolGraphResult { // Current implementation uses `SymbolGraphExtract()`, but in the future we should emit the symbol graph // while building. @@ -392,7 +374,7 @@ final class PluginDelegate: PluginInvocationDelegate { ) // Find the target in the build operation's package graph; it's an error if we don't find it. - let packageGraph = try buildSystem.getPackageGraph() + let packageGraph = try await buildSystem.getPackageGraph() guard let target = packageGraph.module(for: targetName) else { throw StringError("could not find a target named “\(targetName)”") } @@ -409,7 +391,7 @@ final class PluginDelegate: PluginInvocationDelegate { } // Build the target, if needed. - try buildSystem.build(subset: .target(target.name, for: buildParameters.destination)) + try await buildSystem.build(subset: .target(target.name, for: buildParameters.destination)) // Configure the symbol graph extractor. var symbolGraphExtractor = try SymbolGraphExtract( @@ -465,9 +447,9 @@ final class PluginDelegate: PluginInvocationDelegate { } extension BuildSystem { - fileprivate func buildIgnoringError(subset: BuildSubset) -> Bool { + fileprivate func buildIgnoringError(subset: BuildSubset) async -> Bool { do { - try self.build(subset: subset) + try await self.build(subset: subset) return true } catch { return false diff --git a/Sources/CoreCommands/BuildSystemSupport.swift b/Sources/CoreCommands/BuildSystemSupport.swift index 3ea89cf2f49..161fb7b5d1b 100644 --- a/Sources/CoreCommands/BuildSystemSupport.swift +++ b/Sources/CoreCommands/BuildSystemSupport.swift @@ -29,7 +29,7 @@ private struct NativeBuildSystemFactory: BuildSystemFactory { cacheBuildManifest: Bool, productsBuildParameters: BuildParameters?, toolsBuildParameters: BuildParameters?, - packageGraphLoader: (() throws -> ModulesGraph)?, + packageGraphLoader: (() async throws -> ModulesGraph)?, outputStream: OutputByteStream?, logLevel: Diagnostic.Severity?, observabilityScope: ObservabilityScope? @@ -74,7 +74,7 @@ private struct XcodeBuildSystemFactory: BuildSystemFactory { cacheBuildManifest: Bool, productsBuildParameters: BuildParameters?, toolsBuildParameters: BuildParameters?, - packageGraphLoader: (() throws -> ModulesGraph)?, + packageGraphLoader: (() async throws -> ModulesGraph)?, outputStream: OutputByteStream?, logLevel: Diagnostic.Severity?, observabilityScope: ObservabilityScope? diff --git a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift index 3fa47caa50b..00578deb01c 100644 --- a/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift +++ b/Sources/SPMBuildCore/BuildSystem/BuildSystem.swift @@ -40,23 +40,23 @@ public protocol BuildSystem: Cancellable { var delegate: BuildSystemDelegate? { get } /// The test products that this build system will build. - var builtTestProducts: [BuiltTestProduct] { get } + var builtTestProducts: [BuiltTestProduct] { get async } - /// Returns the package graph used by the build system. - func getPackageGraph() throws -> ModulesGraph + /// Returns the modules graph used by the build system. + var modulesGraph: ModulesGraph { get async throws } /// Builds a subset of the package graph. /// - Parameters: /// - subset: The subset of the package graph to build. - func build(subset: BuildSubset) throws + func build(subset: BuildSubset) async throws var buildPlan: BuildPlan { get throws } } extension BuildSystem { /// Builds the default subset: all targets excluding tests. - public func build() throws { - try build(subset: .allExcludingTests) + public func build() async throws { + try await build(subset: .allExcludingTests) } } @@ -104,7 +104,7 @@ public protocol BuildSystemFactory { cacheBuildManifest: Bool, productsBuildParameters: BuildParameters?, toolsBuildParameters: BuildParameters?, - packageGraphLoader: (() throws -> ModulesGraph)?, + packageGraphLoader: (() async throws -> ModulesGraph)?, outputStream: OutputByteStream?, logLevel: Diagnostic.Severity?, observabilityScope: ObservabilityScope? @@ -131,7 +131,7 @@ public struct BuildSystemProvider { cacheBuildManifest: Bool = true, productsBuildParameters: BuildParameters? = .none, toolsBuildParameters: BuildParameters? = .none, - packageGraphLoader: (() throws -> ModulesGraph)? = .none, + packageGraphLoader: (() async throws -> ModulesGraph)? = .none, outputStream: OutputByteStream? = .none, logLevel: Diagnostic.Severity? = .none, observabilityScope: ObservabilityScope? = .none diff --git a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift index 536f1558a14..4f7ac3f36d5 100644 --- a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift +++ b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift @@ -234,10 +234,10 @@ extension PluginModule { } /// Invoked when the plugin emits a message. The `responder` closure can be used to send any reply messages. - func handleMessage(data: Data, responder: @escaping (Data) -> Void) throws { + func handleMessage(data: Data, responder: @escaping (Data) -> Void) async throws { let message = try PluginToHostMessage(data) switch message { - + case .emitDiagnostic(let severity, let message, let file, let line): let metadata: ObservabilityMetadata? = file.map { var metadata = ObservabilityMetadata() @@ -291,49 +291,40 @@ extension PluginModule { } case .buildOperationRequest(let subset, let parameters): - self.invocationDelegate.pluginRequestedBuildOperation(subset: .init(subset), parameters: .init(parameters)) { + do { do { - switch $0 { - case .success(let result): - responder(try HostToPluginMessage.buildOperationResponse(result: .init(result)).toData()) - case .failure(let error): - responder(try HostToPluginMessage.errorResponse(error: String(describing: error)).toData()) - } - } - catch { - self.observabilityScope.emit(debug: "couldn't send reply to plugin", underlyingError: error) + let result = try await self.invocationDelegate.pluginRequestedBuildOperation(subset: .init(subset), parameters: .init(parameters)) + responder(try HostToPluginMessage.buildOperationResponse(result: .init(result)).toData()) + } catch { + responder(try HostToPluginMessage.errorResponse(error: String(describing: error)).toData()) } + } catch { + self.observabilityScope.emit(debug: "couldn't send reply to plugin", underlyingError: error) } case .testOperationRequest(let subset, let parameters): - self.invocationDelegate.pluginRequestedTestOperation(subset: .init(subset), parameters: .init(parameters)) { + do { do { - switch $0 { - case .success(let result): - responder(try HostToPluginMessage.testOperationResponse(result: .init(result)).toData()) - case .failure(let error): - responder(try HostToPluginMessage.errorResponse(error: String(describing: error)).toData()) - } - } - catch { - self.observabilityScope.emit(debug: "couldn't send reply to plugin", underlyingError: error) + let result = try await self.invocationDelegate.pluginRequestedTestOperation(subset: .init(subset), parameters: .init(parameters)) + responder(try HostToPluginMessage.testOperationResponse(result: .init(result)).toData()) + } catch { + responder(try HostToPluginMessage.errorResponse(error: String(describing: error)).toData()) } + } catch { + self.observabilityScope.emit(debug: "couldn't send reply to plugin", underlyingError: error) } case .symbolGraphRequest(let targetName, let options): // The plugin requested symbol graph information for a target. We ask the delegate and then send a response. - self.invocationDelegate.pluginRequestedSymbolGraph(forTarget: .init(targetName), options: .init(options)) { + do { do { - switch $0 { - case .success(let result): - responder(try HostToPluginMessage.symbolGraphResponse(result: .init(result)).toData()) - case .failure(let error): - responder(try HostToPluginMessage.errorResponse(error: String(describing: error)).toData()) - } - } - catch { - self.observabilityScope.emit(debug: "couldn't send reply to plugin", underlyingError: error) + let result = try await self.invocationDelegate.pluginRequestedSymbolGraph(forTarget: .init(targetName), options: .init(options)) + responder(try HostToPluginMessage.symbolGraphResponse(result: .init(result)).toData()) + } catch { + responder(try HostToPluginMessage.errorResponse(error: String(describing: error)).toData()) } + } catch { + self.observabilityScope.emit(debug: "couldn't send reply to plugin", underlyingError: error) } } } @@ -341,7 +332,7 @@ extension PluginModule { let runnerDelegate = ScriptRunnerDelegate(invocationDelegate: delegate, observabilityScope: observabilityScope) // Call the plugin script runner to actually invoke the plugin. - scriptRunner.runPluginScript( + await scriptRunner.runPluginScript( sourceFiles: sources.paths, pluginName: self.name, initialMessage: initialMessage, @@ -844,13 +835,22 @@ public protocol PluginInvocationDelegate { func pluginDefinedPrebuildCommand(displayName: String?, executable: AbsolutePath, arguments: [String], environment: [String: String], workingDirectory: AbsolutePath?, outputFilesDirectory: AbsolutePath) -> Bool /// Called when a plugin requests a build operation through the PackagePlugin APIs. - func pluginRequestedBuildOperation(subset: PluginInvocationBuildSubset, parameters: PluginInvocationBuildParameters, completion: @escaping (Result) -> Void) + func pluginRequestedBuildOperation( + subset: PluginInvocationBuildSubset, + parameters: PluginInvocationBuildParameters + ) async throws -> PluginInvocationBuildResult /// Called when a plugin requests a test operation through the PackagePlugin APIs. - func pluginRequestedTestOperation(subset: PluginInvocationTestSubset, parameters: PluginInvocationTestParameters, completion: @escaping (Result) -> Void) + func pluginRequestedTestOperation( + subset: PluginInvocationTestSubset, + parameters: PluginInvocationTestParameters + ) async throws -> PluginInvocationTestResult /// Called when a plugin requests that the host computes and returns symbol graph information for a particular target. - func pluginRequestedSymbolGraph(forTarget name: String, options: PluginInvocationSymbolGraphOptions, completion: @escaping (Result) -> Void) + func pluginRequestedSymbolGraph( + forTarget name: String, + options: PluginInvocationSymbolGraphOptions + ) async throws -> PluginInvocationSymbolGraphResult } public struct PluginInvocationSymbolGraphOptions { @@ -970,14 +970,24 @@ public extension PluginInvocationDelegate { func pluginDefinedPrebuildCommand(displayName: String?, executable: AbsolutePath, arguments: [String], environment: [String: String], workingDirectory: AbsolutePath?, outputFilesDirectory: AbsolutePath) -> Bool { return true } - func pluginRequestedBuildOperation(subset: PluginInvocationBuildSubset, parameters: PluginInvocationBuildParameters, completion: @escaping (Result) -> Void) { - DispatchQueue.sharedConcurrent.async { completion(Result.failure(StringError("unimplemented"))) } + func pluginRequestedBuildOperation( + subset: PluginInvocationBuildSubset, + parameters: PluginInvocationBuildParameters + ) async throws -> PluginInvocationBuildResult { + throw StringError("unimplemented") } - func pluginRequestedTestOperation(subset: PluginInvocationTestSubset, parameters: PluginInvocationTestParameters, completion: @escaping (Result) -> Void) { - DispatchQueue.sharedConcurrent.async { completion(Result.failure(StringError("unimplemented"))) } + func pluginRequestedTestOperation( + subset: PluginInvocationTestSubset, + parameters: PluginInvocationTestParameters + ) async throws -> PluginInvocationTestResult { + throw StringError("unimplemented") } - func pluginRequestedSymbolGraph(forTarget name: String, options: PluginInvocationSymbolGraphOptions, completion: @escaping (Result) -> Void) { - DispatchQueue.sharedConcurrent.async { completion(Result.failure(StringError("unimplemented"))) } + + func pluginRequestedSymbolGraph( + forTarget name: String, + options: PluginInvocationSymbolGraphOptions + ) async throws -> PluginInvocationSymbolGraphResult { + throw StringError("unimplemented") } } diff --git a/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift b/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift index 1dab3468673..0d552a72fc4 100644 --- a/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift +++ b/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift @@ -20,16 +20,14 @@ import PackageGraph public protocol PluginScriptRunner { /// Public protocol function that starts compiling the plugin script to an executable. The name is used as the basename for the executable and auxiliary files. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the completion handler on the callback queue when compilation ends. - @available(*, noasync, message: "Use the async alternative") func compilePluginScript( sourceFiles: [AbsolutePath], pluginName: String, toolsVersion: ToolsVersion, observabilityScope: ObservabilityScope, callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate, - completion: @escaping (Result) -> Void - ) + delegate: PluginScriptCompilerDelegate + ) async throws -> PluginCompilationResult /// Implements the mechanics of running a plugin script implemented as a set of Swift source files, for use /// by the package graph when it is evaluating package plugins. @@ -53,38 +51,14 @@ public protocol PluginScriptRunner { fileSystem: FileSystem, observabilityScope: ObservabilityScope, callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate, - completion: @escaping (Result) -> Void - ) + delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate + ) async throws -> Int32 /// Returns the Triple that represents the host for which plugin script tools should be built, or for which binary /// tools should be selected. var hostTriple: Triple { get throws } } -public extension PluginScriptRunner { - func compilePluginScript( - sourceFiles: [AbsolutePath], - pluginName: String, - toolsVersion: ToolsVersion, - observabilityScope: ObservabilityScope, - callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate - ) async throws -> PluginCompilationResult { - try await safe_async { - self.compilePluginScript( - sourceFiles: sourceFiles, - pluginName: pluginName, - toolsVersion: toolsVersion, - observabilityScope: observabilityScope, - callbackQueue: callbackQueue, - delegate: delegate, - completion: $0 - ) - } - } -} - /// Protocol by which `PluginScriptRunner` communicates back to the caller as it compiles plugins. public protocol PluginScriptCompilerDelegate { /// Called immediately before compiling a plugin. Will not be called if the plugin didn't have to be compiled. This call is always followed by a `didCompilePlugin()` but is mutually exclusive with a `skippedCompilingPlugin()` call. @@ -103,7 +77,7 @@ public protocol PluginScriptRunnerDelegate { func handleOutput(data: Data) /// Called for each length-delimited message received from the plugin. The `responder` is closure that can be used to send one or more messages in reply. - func handleMessage(data: Data, responder: @escaping (Data) -> Void) throws + func handleMessage(data: Data, responder: @escaping (Data) -> Void) async throws } /// The result of compiling a plugin. The executable path will only be present if the compilation succeeds, while the other properties are present in all cases. diff --git a/Sources/Workspace/DefaultPluginScriptRunner.swift b/Sources/Workspace/DefaultPluginScriptRunner.swift index 0dc7ab0037f..b8ea68563e3 100644 --- a/Sources/Workspace/DefaultPluginScriptRunner.swift +++ b/Sources/Workspace/DefaultPluginScriptRunner.swift @@ -64,45 +64,34 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { fileSystem: FileSystem, observabilityScope: ObservabilityScope, callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate, - completion: @escaping (Result) -> Void - ) { + delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate + ) async throws -> Int32 { // If needed, compile the plugin script to an executable (asynchronously). Compilation is skipped if the plugin hasn't changed since it was last compiled. - self.compilePluginScript( + let result = try await self.compilePluginScript( sourceFiles: sourceFiles, pluginName: pluginName, toolsVersion: toolsVersion, observabilityScope: observabilityScope, callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: { - dispatchPrecondition(condition: .onQueue(DispatchQueue.sharedConcurrent)) - switch $0 { - case .success(let result): - if result.succeeded { - // Compilation succeeded, so run the executable. We are already running on an asynchronous queue. - self.invoke( - compiledExec: result.executableFile, - workingDirectory: workingDirectory, - writableDirectories: writableDirectories, - readOnlyDirectories: readOnlyDirectories, - allowNetworkConnections: allowNetworkConnections, - initialMessage: initialMessage, - observabilityScope: observabilityScope, - callbackQueue: callbackQueue, - delegate: delegate, - completion: completion) - } - else { - // Compilation failed, so throw an error. - callbackQueue.async { completion(.failure(DefaultPluginScriptRunnerError.compilationFailed(result))) } - } - case .failure(let error): - // Compilation failed, so just call the callback block on the appropriate queue. - callbackQueue.async { completion(.failure(error)) } - } - } + delegate: delegate ) + if result.succeeded { + // Compilation succeeded, so run the executable. We are already running on an asynchronous queue. + return try await self.invoke( + compiledExec: result.executableFile, + workingDirectory: workingDirectory, + writableDirectories: writableDirectories, + readOnlyDirectories: readOnlyDirectories, + allowNetworkConnections: allowNetworkConnections, + initialMessage: initialMessage, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue, + delegate: delegate + ) + } else { + // Compilation failed, so throw an error. + throw DefaultPluginScriptRunnerError.compilationFailed(result) + } } public var hostTriple: Triple { @@ -116,9 +105,8 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { toolsVersion: ToolsVersion, observabilityScope: ObservabilityScope, callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate, - completion: @escaping (Result) -> Void - ) { + delegate: PluginScriptCompilerDelegate + ) async throws -> PluginCompilationResult { // Determine the path of the executable and other produced files. let execName = pluginName.spm_mangledToC99ExtendedIdentifier() #if os(Windows) @@ -241,9 +229,7 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { } catch { // Bail out right away if we didn't even get this far. - return callbackQueue.async { - completion(.failure(DefaultPluginScriptRunnerError.compilationPreparationFailed(error: error))) - } + throw DefaultPluginScriptRunnerError.compilationPreparationFailed(error: error) } // Hash the compiler inputs to decide whether we really need to recompile. @@ -330,11 +316,10 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { executableFile: execFilePath, diagnosticsFile: diagFilePath, compilerOutput: compilationState.output, - cached: true) + cached: true + ) delegate.skippedCompilingPlugin(cachedResult: result) - return callbackQueue.async { - completion(.success(result)) - } + return result } // Otherwise we need to recompile. We start by telling the delegate. @@ -351,47 +336,43 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { } // Now invoke the compiler asynchronously. - AsyncProcess.popen(arguments: commandLine, environment: environment, queue: callbackQueue) { - // We are now on our caller's requested callback queue, so we just call the completion handler directly. - dispatchPrecondition(condition: .onQueue(callbackQueue)) - completion($0.tryMap { process in - // Emit the compiler output as observable info. - let compilerOutput = ((try? process.utf8Output()) ?? "") + ((try? process.utf8stderrOutput()) ?? "") - if !compilerOutput.isEmpty { - observabilityScope.emit(info: compilerOutput) - } + let process = try await AsyncPorcess.Process.popen(arguments: commandLine, environment: environment) - // Save the persisted compilation state for possible reuse next time. - let compilationState = PersistedCompilationState( - commandLine: commandLine, - environment: toolchain.swiftCompilerEnvironment.cachable, - inputHash: compilerInputHash, - output: compilerOutput, - result: .init(process.exitStatus)) - do { - try JSONEncoder.makeWithDefaults().encode(path: stateFilePath, fileSystem: self.fileSystem, compilationState) - } - catch { - // We couldn't write out the `.state` file. We warn about it but proceed. - observabilityScope.emit(debug: "Couldn't save plugin compilation state", underlyingError: error) - } + // Emit the compiler output as observable info. + let compilerOutput = ((try? process.utf8Output()) ?? "") + ((try? process.utf8stderrOutput()) ?? "") + if !compilerOutput.isEmpty { + observabilityScope.emit(info: compilerOutput) + } - // Construct a PluginCompilationResult for both the successful and unsuccessful cases (to convey diagnostics, etc). - let result = PluginCompilationResult( - succeeded: compilationState.succeeded, - commandLine: commandLine, - executableFile: execFilePath, - diagnosticsFile: diagFilePath, - compilerOutput: compilerOutput, - cached: false) - - // Tell the delegate that we're done compiling the plugin, passing it the result. - delegate.didCompilePlugin(result: result) - - // Also return the result to the caller. - return result - }) + // Save the persisted compilation state for possible reuse next time. + let newCompilationState = PersistedCompilationState( + commandLine: commandLine, + environment: toolchain.swiftCompilerEnvironment.cachable, + inputHash: compilerInputHash, + output: compilerOutput, + result: .init(process.exitStatus)) + do { + try JSONEncoder.makeWithDefaults().encode(path: stateFilePath, fileSystem: self.fileSystem, newCompilationState) } + catch { + // We couldn't write out the `.state` file. We warn about it but proceed. + observabilityScope.emit(debug: "Couldn't save plugin compilation state", underlyingError: error) + } + + // Construct a PluginCompilationResult for both the successful and unsuccessful cases (to convey diagnostics, etc). + let result = PluginCompilationResult( + succeeded: newCompilationState.succeeded, + commandLine: commandLine, + executableFile: execFilePath, + diagnosticsFile: diagFilePath, + compilerOutput: compilerOutput, + cached: false) + + // Tell the delegate that we're done compiling the plugin, passing it the result. + delegate.didCompilePlugin(result: result) + + // Also return the result to the caller. + return result } /// Returns path to the sdk, if possible. @@ -418,7 +399,34 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { return sdkRootPath } - + + private func invoke( + compiledExec: Basics.AbsolutePath, + workingDirectory: Basics.AbsolutePath, + writableDirectories: [Basics.AbsolutePath], + readOnlyDirectories: [Basics.AbsolutePath], + allowNetworkConnections: [SandboxNetworkPermission], + initialMessage: Data, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + delegate: PluginScriptRunnerDelegate + ) async throws -> Int32 { + try await withCheckedThrowingContinuation { + self.invoke( + compiledExec: compiledExec, + workingDirectory: workingDirectory, + writableDirectories: writableDirectories, + readOnlyDirectories: readOnlyDirectories, + allowNetworkConnections: allowNetworkConnections, + initialMessage: initialMessage, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue, + delegate: delegate, + completion: $0.resume(with:) + ) + } + } + /// Private function that invokes a compiled plugin executable and communicates with it until it finishes. fileprivate func invoke( compiledExec: Basics.AbsolutePath, @@ -489,31 +497,30 @@ public struct DefaultPluginScriptRunner: PluginScriptRunner, Cancellable { stdoutLock.withLock { do { while let message = try fileHandle.readPluginMessage() { - // FIXME: We should handle errors here. - callbackQueue.async { - do { - try delegate.handleMessage(data: message, responder: { data in - outputQueue.async { - do { - try outputHandle.writePluginMessage(data) - } - catch { - print("error while trying to send message to plugin: \(error.interpolationDescription)") - } + // FIXME: We should handle backpressure and errors here. + do { + try await delegate.handleMessage(data: message, responder: { data in + outputQueue.async { + do { + try outputHandle.writePluginMessage(data) + } + catch { + print("error while trying to send message to plugin: \(error.interpolationDescription)") } - }) - } - catch DecodingError.keyNotFound(let key, _) where key.stringValue == "version" { - print("message from plugin did not contain a 'version' key, likely an incompatible plugin library is being loaded by the plugin") - } - catch { - print("error while trying to handle message from plugin: \(error.interpolationDescription)") - } + } + }) + } + catch DecodingError.keyNotFound(let key, _) where key.stringValue == "version" { + observabilityScope.emit(error: "message from plugin did not contain a 'version' key, likely an incompatible plugin library is being loaded by the plugin") } + catch { + observabilityScope.emit(error: "error while trying to handle message from plugin: \(error.interpolationDescription)") + } + } } catch { - print("error while trying to read message from plugin: \(error.interpolationDescription)") + observabilityScope.emit(error: "error while trying to read message from plugin: \(error.interpolationDescription)") } } } diff --git a/Sources/XCBuildSupport/XcodeBuildSystem.swift b/Sources/XCBuildSupport/XcodeBuildSystem.swift index 5b8803a0481..15860793a58 100644 --- a/Sources/XCBuildSupport/XcodeBuildSystem.swift +++ b/Sources/XCBuildSupport/XcodeBuildSystem.swift @@ -22,7 +22,6 @@ import PackageModel @_spi(SwiftPMInternal) import SPMBuildCore -import func TSCBasic.memoize import protocol TSCBasic.OutputByteStream import class Basics.AsyncProcess import func TSCBasic.withTemporaryFile @@ -31,7 +30,7 @@ import enum TSCUtility.Diagnostics public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { private let buildParameters: BuildParameters - private let packageGraphLoader: () throws -> ModulesGraph + private let packageGraphLoader: () async throws -> ModulesGraph private let logLevel: Basics.Diagnostic.Severity private let xcbuildPath: AbsolutePath private var packageGraph: ModulesGraph? @@ -46,29 +45,31 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { public weak var delegate: SPMBuildCore.BuildSystemDelegate? public var builtTestProducts: [BuiltTestProduct] { - do { - let graph = try getPackageGraph() - - var builtProducts: [BuiltTestProduct] = [] - - for package in graph.rootPackages { - for product in package.products where product.type == .test { - let binaryPath = try buildParameters.binaryPath(for: product) - builtProducts.append( - BuiltTestProduct( - productName: product.name, - binaryPath: binaryPath, - packagePath: package.path, - library: buildParameters.testingParameters.library + get async { + do { + let graph = try await self.modulesGraph + + var builtProducts: [BuiltTestProduct] = [] + + for package in graph.rootPackages { + for product in package.products where product.type == .test { + let binaryPath = try buildParameters.binaryPath(for: product) + builtProducts.append( + BuiltTestProduct( + productName: product.name, + binaryPath: binaryPath, + packagePath: package.path, + library: buildParameters.testingParameters.library + ) ) - ) + } } - } - return builtProducts - } catch { - self.observabilityScope.emit(error) - return [] + return builtProducts + } catch { + self.observabilityScope.emit(error) + return [] + } } } @@ -80,7 +81,7 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { public init( buildParameters: BuildParameters, - packageGraphLoader: @escaping () throws -> ModulesGraph, + packageGraphLoader: @escaping () async throws -> ModulesGraph, outputStream: OutputByteStream, logLevel: Basics.Diagnostic.Severity, fileSystem: FileSystem, @@ -145,12 +146,12 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { return [] } - public func build(subset: BuildSubset) throws { + public func build(subset: BuildSubset) async throws { guard !buildParameters.shouldSkipBuilding else { return } - let pifBuilder = try getPIFBuilder() + let pifBuilder = try await getPIFBuilder() let pif = try pifBuilder.generatePIF() try self.fileSystem.writeIfChanged(path: buildParameters.pifManifest, string: pif) @@ -207,7 +208,7 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { outputRedirection: redirection ) try process.launch() - let result = try process.waitUntilExit() + let result = try await process.waitUntilExit() if let buildParamsFile { try? self.fileSystem.removeFileTree(buildParamsFile) @@ -309,9 +310,9 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { return delegate } - private func getPIFBuilder() throws -> PIFBuilder { - try memoize(to: &pifBuilder) { - let graph = try getPackageGraph() + private func getPIFBuilder() async throws -> PIFBuilder { + try await memoize(to: &pifBuilder) { + let graph = try await self.modulesGraph let pifBuilder = try PIFBuilder( graph: graph, parameters: .init(buildParameters, supportedSwiftVersions: supportedSwiftVersions()), @@ -325,9 +326,11 @@ public final class XcodeBuildSystem: SPMBuildCore.BuildSystem { /// Returns the package graph using the graph loader closure. /// /// First access will cache the graph. - public func getPackageGraph() throws -> ModulesGraph { - try memoize(to: &packageGraph) { - try packageGraphLoader() + public var modulesGraph: ModulesGraph { + get async throws { + try await memoize(to: &packageGraph) { + try await packageGraphLoader() + } } } } @@ -398,3 +401,14 @@ extension Basics.Diagnostic.Severity { self <= .info } } + +/// Memoizes a costly computation to a cache variable. +func memoize(to cache: inout T?, build: () async throws -> T) async rethrows -> T { + if let value = cache { + return value + } else { + let value = try await build() + cache = value + return value + } +} diff --git a/Sources/swift-bootstrap/main.swift b/Sources/swift-bootstrap/main.swift index c9d6958c50f..bb201e14125 100644 --- a/Sources/swift-bootstrap/main.swift +++ b/Sources/swift-bootstrap/main.swift @@ -35,7 +35,7 @@ import struct TSCUtility.Version SwiftBootstrapBuildTool.main() -struct SwiftBootstrapBuildTool: ParsableCommand { +struct SwiftBootstrapBuildTool: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "swift-bootstrap", abstract: "Bootstrapping build tool, only use in the context of bootstrapping SwiftPM itself", @@ -158,7 +158,7 @@ struct SwiftBootstrapBuildTool: ParsableCommand { public init() {} - public func run() throws { + public func run() async throws { do { let fileSystem = localFileSystem @@ -187,7 +187,7 @@ struct SwiftBootstrapBuildTool: ParsableCommand { observabilityScope: observabilityScope, logLevel: self.logLevel ) - try builder.build( + try await builder.build( packagePath: packagePath, scratchDirectory: scratchDirectory, buildSystem: self.buildSystem, @@ -246,7 +246,7 @@ struct SwiftBootstrapBuildTool: ParsableCommand { useIntegratedSwiftDriver: Bool, explicitTargetDependencyImportCheck: TargetDependencyImportCheckingMode, shouldDisableLocalRpath: Bool - ) throws { + ) async throws { let buildSystem = try createBuildSystem( packagePath: packagePath, scratchDirectory: scratchDirectory, @@ -260,7 +260,7 @@ struct SwiftBootstrapBuildTool: ParsableCommand { shouldDisableLocalRpath: shouldDisableLocalRpath, logLevel: logLevel ) - try buildSystem.build(subset: .allExcludingTests) + try await buildSystem.build(subset: .allExcludingTests) } func createBuildSystem( diff --git a/Tests/SPMBuildCoreTests/PluginInvocationTests.swift b/Tests/SPMBuildCoreTests/PluginInvocationTests.swift index f6f5b5c42ea..6dd09710111 100644 --- a/Tests/SPMBuildCoreTests/PluginInvocationTests.swift +++ b/Tests/SPMBuildCoreTests/PluginInvocationTests.swift @@ -30,7 +30,7 @@ import class TSCBasic.InMemoryFileSystem import struct TSCUtility.SerializedDiagnostics final class PluginInvocationTests: XCTestCase { - func testBasics() throws { + func testBasics() async throws { // Construct a canned file system and package graph with a single package and a library that uses a build tool plugin that invokes a tool. let fileSystem = InMemoryFileSystem(emptyFiles: "/Foo/Plugins/FooPlugin/source.swift", @@ -121,12 +121,9 @@ final class PluginInvocationTests: XCTestCase { toolsVersion: ToolsVersion, observabilityScope: ObservabilityScope, callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate, - completion: @escaping (Result) -> Void - ) { - callbackQueue.sync { - completion(.failure(StringError("unimplemented"))) - } + delegate: PluginScriptCompilerDelegate + ) async throws -> PluginCompilationResult { + throw StringError("unimplemented") } func runPluginScript( @@ -141,9 +138,8 @@ final class PluginInvocationTests: XCTestCase { fileSystem: FileSystem, observabilityScope: ObservabilityScope, callbackQueue: DispatchQueue, - delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate, - completion: @escaping (Result) -> Void - ) { + delegate: PluginScriptCompilerDelegate & PluginScriptRunnerDelegate + ) async throws -> Int32 { // Check that we were given the right sources. XCTAssertEqual(sourceFiles, ["/Foo/Plugins/FooPlugin/source.swift"]) @@ -154,55 +150,44 @@ final class PluginInvocationTests: XCTestCase { } // Pretend it emitted a warning. - try callbackQueue.sync { - let message = Data(""" - { "emitDiagnostic": { - "severity": "warning", - "message": "A warning", - "file": "/Foo/Sources/Foo/SomeFile.abc", - "line": 42 - } + var message = Data(""" + { "emitDiagnostic": { + "severity": "warning", + "message": "A warning", + "file": "/Foo/Sources/Foo/SomeFile.abc", + "line": 42 } - """.utf8) - try delegate.handleMessage(data: message, responder: { _ in }) } + """.utf8) + try await delegate.handleMessage(data: message, responder: { _ in }) // Pretend it defined a build command. - try callbackQueue.sync { - let message = Data(""" - { "defineBuildCommand": { - "configuration": { - "version": 2, - "displayName": "Do something", - "executable": "/bin/FooTool", - "arguments": [ - "-c", "/Foo/Sources/Foo/SomeFile.abc" - ], - "workingDirectory": "/Foo/Sources/Foo", - "environment": { - "X": "Y" - }, - }, - "inputFiles": [ + message = Data(""" + { "defineBuildCommand": { + "configuration": { + "version": 2, + "displayName": "Do something", + "executable": "/bin/FooTool", + "arguments": [ + "-c", "/Foo/Sources/Foo/SomeFile.abc" ], - "outputFiles": [ - ] - } + "workingDirectory": "/Foo/Sources/Foo", + "environment": { + "X": "Y" + }, + }, + "inputFiles": [ + ], + "outputFiles": [ + ] } - """.utf8) - try delegate.handleMessage(data: message, responder: { _ in }) - } - } - catch { - callbackQueue.sync { - completion(.failure(error)) } + """.utf8) + try await delegate.handleMessage(data: message, responder: { _ in }) } // If we get this far we succeeded, so invoke the completion handler. - callbackQueue.sync { - completion(.success(0)) - } + return 0 } }