Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a command plugin and usage instructions to run Swift Build under xcodebuild #113

Merged
merged 1 commit into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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