Skip to content

Commit

Permalink
Add a command plugin and usage instructions to run Swift Build under …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
jakepetroules committed Feb 8, 2025
1 parent 4bc4e5b commit f7741fc
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 6 deletions.
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
25 changes: 20 additions & 5 deletions Plugins/launch-xcode/launch-xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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...")
Expand All @@ -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
Expand Down
93 changes: 93 additions & 0 deletions Plugins/run-xcodebuild/run-xcodebuild.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------
Expand Down

0 comments on commit f7741fc

Please sign in to comment.