From f7741fc479e66afa1b8918d9a9c3801c13bf6684 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Fri, 7 Feb 2025 16:18:11 +0900 Subject: [PATCH] Add a command plugin and usage instructions to run Swift Build under xcodebuild This adds a new "run-xcodebuild" command plugin similar to "launch-xcode", which will run xcodebuild pointed to the just-built Swift Build service, forwarding along any arguments passed to the plugin. Also includes some minor error handling improvements and doc tweaks to the existing launch-xcode plugin. Closes #43 --- Package.swift | 7 ++ Plugins/launch-xcode/launch-xcode.swift | 25 ++++-- Plugins/run-xcodebuild/run-xcodebuild.swift | 93 +++++++++++++++++++++ README.md | 6 +- 4 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 Plugins/run-xcodebuild/run-xcodebuild.swift diff --git a/Package.swift b/Package.swift index 1b0bd4d0..2e540040 100644 --- a/Package.swift +++ b/Package.swift @@ -371,6 +371,13 @@ let package = Package( verb: "launch-xcode", description: "Launch the currently selected Xcode configured to use the just-built build service" )) + ), + .plugin( + name: "run-xcodebuild", + capability: .command(intent: .custom( + verb: "run-xcodebuild", + description: "Run xcodebuild from the currently selected Xcode configured to use the just-built build service" + )) ) ], swiftLanguageModes: [.v6], diff --git a/Plugins/launch-xcode/launch-xcode.swift b/Plugins/launch-xcode/launch-xcode.swift index 3cd1d996..674b8be7 100644 --- a/Plugins/launch-xcode/launch-xcode.swift +++ b/Plugins/launch-xcode/launch-xcode.swift @@ -17,8 +17,7 @@ import Foundation struct LaunchXcode: CommandPlugin { func performCommand(context: PluginContext, arguments: [String]) async throws { #if !os(macOS) - print("This command is only supported on macOS") - return + throw LaunchXcodeError.unsupportedPlatform #else var args = ArgumentExtractor(arguments) var configuration: PackageManager.BuildConfiguration = .debug @@ -36,8 +35,7 @@ struct LaunchXcode: CommandPlugin { let buildResult = try packageManager.build(.all(includingTests: false), parameters: .init(configuration: configuration, echoLogs: true)) guard buildResult.succeeded else { return } guard let buildServiceURL = buildResult.builtArtifacts.map({ $0.url }).filter({ $0.lastPathComponent == "SWBBuildServiceBundle" }).first else { - print("Failed to determine path to built SWBBuildServiceBundle") - return + throw LaunchXcodeError.buildServiceURLNotFound } print("Launching Xcode...") @@ -48,12 +46,29 @@ struct LaunchXcode: CommandPlugin { process.standardError = nil try await process.run() if process.terminationStatus != 0 { - print("Launching Xcode failed, did you remember to pass `--disable-sandbox`?") + throw LaunchXcodeError.launchFailed } #endif } } +enum LaunchXcodeError: Error, CustomStringConvertible { + case unsupportedPlatform + case buildServiceURLNotFound + case launchFailed + + var description: String { + switch self { + case .unsupportedPlatform: + return "This command is only supported on macOS" + case .buildServiceURLNotFound: + return "Failed to determine path to built SWBBuildServiceBundle" + case .launchFailed: + return "Launching Xcode failed, did you remember to pass `--disable-sandbox`?" + } + } +} + extension Process { func run() async throws { try await withCheckedThrowingContinuation { continuation in diff --git a/Plugins/run-xcodebuild/run-xcodebuild.swift b/Plugins/run-xcodebuild/run-xcodebuild.swift new file mode 100644 index 00000000..710a84a2 --- /dev/null +++ b/Plugins/run-xcodebuild/run-xcodebuild.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import PackagePlugin +import Foundation + +@main +struct RunXcodebuild: CommandPlugin { + func performCommand(context: PluginContext, arguments: [String]) async throws { + #if !os(macOS) + throw RunXcodebuildError.unsupportedPlatform + #else + var args = ArgumentExtractor(arguments) + var configuration: PackageManager.BuildConfiguration = .debug + // --release + if args.extractFlag(named: "release") > 0 { + configuration = .release + } else { + // --configuration release + let configurationOptions = args.extractOption(named: "configuration") + if configurationOptions.contains("release") { + configuration = .release + } + } + + let buildResult = try packageManager.build(.all(includingTests: false), parameters: .init(configuration: configuration, echoLogs: true)) + guard buildResult.succeeded else { return } + guard let buildServiceURL = buildResult.builtArtifacts.map({ $0.url }).filter({ $0.lastPathComponent == "SWBBuildServiceBundle" }).first else { + throw RunXcodebuildError.buildServiceURLNotFound + } + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + process.arguments = ["xcodebuild"] + args.remainingArguments + process.environment = ProcessInfo.processInfo.environment.merging(["XCBBUILDSERVICE_PATH": buildServiceURL.path()]) { _, new in new } + try await process.run() + if process.terminationStatus != 0 { + throw RunXcodebuildError.xcodebuildError(terminationReason: process.terminationReason, terminationStatus: process.terminationStatus) + } + #endif + } +} + +enum RunXcodebuildError: Error, CustomStringConvertible { + case unsupportedPlatform + case buildServiceURLNotFound + case xcodebuildError(terminationReason: Process.TerminationReason, terminationStatus: Int32) + + var description: String { + switch self { + case .unsupportedPlatform: + return "This command is only supported on macOS" + case .buildServiceURLNotFound: + return "Failed to determine path to built SWBBuildServiceBundle" + case let .xcodebuildError(terminationReason, terminationStatus): + let reason = switch terminationReason { + case .exit: + "status code" + case .uncaughtSignal: + "uncaught signal" + @unknown default: + preconditionFailure() + } + return "xcodebuild exited with \(reason) \(terminationStatus), did you remember to pass `--disable-sandbox`?" + } + } +} + +extension Process { + func run() async throws { + try await withCheckedThrowingContinuation { continuation in + terminationHandler = { _ in + continuation.resume() + } + + do { + try run() + } catch { + terminationHandler = nil + continuation.resume(throwing: error) + } + } + } +} diff --git a/README.md b/README.md index d534a688..86784302 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ When building SwiftPM from sources which include Swift Build integration, passin ### With Xcode -Changes to swift-build can also be tested in Xcode using the `launch-xcode` command plugin provided by the package. Run `swift package launch-xcode --disable-sandbox` from your checkout of swift-build to launch a copy of the currently `xcode-select`ed Xcode.app configured to use your modified copy of the build system service. This workflow is currently supported when using Xcode 16.2. +Changes to swift-build can also be tested in Xcode using the `launch-xcode` command plugin provided by the package. Run `swift package --disable-sandbox launch-xcode` from your checkout of swift-build to launch a copy of the currently `xcode-select`ed Xcode.app configured to use your modified copy of the build system service. This workflow is currently supported when using Xcode 16.2. + +### With xcodebuild + +Changes to swift-build can also be tested in xcodebuild using the `run-xcodebuild` command plugin provided by the package. Run `swift package --disable-sandbox run-xcodebuild` from your checkout of swift-build to run xcodebuild from the currently `xcode-select`ed Xcode.app configured to use your modified copy of the build system service. Arguments followed by `--` will be forwarded to xcodebuild unmodified. This workflow is currently supported when using Xcode 16.2. Documentation -------------