A type-safe Swift wrapper around xcodebuild with an emphasis on a clean, ergonomic API.
brew tap InvokeDev/xc
brew install xcDownload the latest release from GitHub Releases:
# Download and extract
curl -L https://github.com/InvokeDev/xc/releases/latest/download/xc-VERSION-macos.tar.gz | tar xz
# Move to PATH
sudo mv xc /usr/local/bin/
# Verify
xc --versiongit clone https://github.com/InvokeDev/xc.git
cd xc
swift build -c release
# Binary is at .build/release/xcAdd XCBuild to your Package.swift:
dependencies: [
.package(url: "https://github.com/InvokeDev/xc.git", from: "1.0.0")
]The xc command provides a streamlined interface to xcodebuild and related tools:
# Build
xc build --scheme MyApp
# Test with coverage
xc test --scheme MyAppTests --coverage
# Clean
xc clean
# Archive
xc archive --scheme MyApp --output build/MyApp.xcarchive
# Export IPA
xc export --archive build/MyApp.xcarchive --output build/export
# Deploy (archive + export + upload)
xc deploy --scheme MyApp --method app-store
# Environment diagnostics
xc doctor
# Simulator management
xc simulator list
xc simulator boot "iPhone 16"
xc simulator ensure "iPhone 16" --runtime "iOS 18.0"
# Cache management
xc cache size
xc cache clean --older-than 7d
# SPM package management
xc packages show
xc packages graph --format dot
xc packages why SomePackage
# Build analysis
xc analyze --log path/to/build.xcactivitylogRun xc --help for all commands and options.
- Type-safe API - All options are strongly typed enums and structs
- Dual API styles - Fluent builder pattern and result builder DSL
- Comprehensive coverage - Build, test, archive, export, XCFramework creation
- Parsed output - Structured warnings, errors, and test results
- Streaming support - Real-time build and test output via AsyncSequence
- CI presets - Common workflows out of the box
- Swift 6 ready - Full Sendable compliance and strict concurrency
- macOS 13.0+
- Swift 6.0+
- Xcode 15.0+
Fluent API:
import XCBuild
let result = try await XCBuildConfig
.project("MyApp.xcodeproj")
.scheme("MyApp")
.configuration(.release)
.destination(.iOSSimulator(name: "iPhone 16"))
.build()
if result.succeeded {
print("Build succeeded in \(result.duration)")
} else {
for error in result.errors {
print("\(error.file ?? ""):\(error.line ?? 0): \(error.message)")
}
}Result Builder DSL:
let config = XCBuildConfig {
Workspace("MyApp.xcworkspace")
Scheme("MyApp")
Configuration(.release)
DerivedDataPath("build/DerivedData")
}
let result = try await config.build()let testResult = try await XCBuildConfig
.workspace("MyApp.xcworkspace")
.scheme("MyAppTests")
.destination(.iOSSimulator(name: "iPhone 16"))
.test {
CodeCoverage()
Parallel(workers: 4)
ResultBundle(path: "build/TestResults.xcresult")
RetryOnFailure(times: 2)
}
print("Passed: \(testResult.passed.count)")
print("Failed: \(testResult.failed.count)")
for failure in testResult.failed {
print("\(failure.suite)/\(failure.name): \(failure.failureMessage)")
}let archiveResult = try await XCBuildConfig
.workspace("MyApp.xcworkspace")
.scheme("MyApp")
.configuration(.release)
.destination(.generic(platform: .iOS))
.archive(to: "build/MyApp.xcarchive")let exportResult = try await exportArchive {
ArchivePath("build/MyApp.xcarchive")
ExportPath("build/export")
ExportOptions {
Method(.appStore)
TeamID("YOUR_TEAM_ID")
UploadSymbols()
ManageAppVersion()
}
}
if let ipaPath = exportResult.ipaPath {
print("IPA exported to: \(ipaPath)")
}let result = try await createXCFramework {
Framework("build/Release-iphoneos/MyLib.framework")
Framework("build/Release-iphonesimulator/MyLib.framework")
Output("build/MyLib.xcframework")
}for try await event in config.buildStream() {
switch event {
case .started(let command):
print("Running: xcodebuild \(command.joined(separator: " "))")
case .output(let line):
print(line)
case .warning(let diagnostic):
print("Warning: \(diagnostic.message)")
case .error(let diagnostic):
print("Error: \(diagnostic.message)")
case .completed(let result):
print("Build \(result.succeeded ? "succeeded" : "failed")")
}
}// Get Xcode version
let version = try await XCBuild.version()
print("Xcode \(version.version) (\(version.build))")
// List available SDKs
let sdks = try await XCBuild.showSDKs()
for sdk in sdks {
print("\(sdk.displayName): \(sdk.canonicalName)")
}
// List project schemes
let info = try await XCBuild.list {
Workspace("MyApp.xcworkspace")
}
print("Schemes: \(info.schemes.joined(separator: ", "))")
// Show available destinations
let destinations = try await XCBuild.showDestinations {
Workspace("MyApp.xcworkspace")
Scheme("MyApp")
}// Validate a PR: clean, build, test with coverage
let testResult = try await CI.validatePR(config)
// Full test suite with coverage
let testResult = try await CI.testWithCoverage(config, resultBundle: "results.xcresult")
// Release archive
let archiveResult = try await CI.releaseArchive(config, archivePath: "build/MyApp.xcarchive")
// Build for multiple destinations in parallel
let results = try await CI.matrixBuild(config, destinations: [
.iOSSimulator(name: "iPhone 16"),
.iOSSimulator(name: "iPad Pro 13-inch"),
])
// Full pipeline: clean, test, archive
let (testResult, archiveResult) = try await CI.fullPipeline(
config,
archivePath: "build/MyApp.xcarchive"
)
// App Store submission
let exportResult = try await CI.appStoreSubmission(
config,
archivePath: "build/MyApp.xcarchive",
exportPath: "build/export",
teamID: "YOUR_TEAM_ID"
)Preview the command without executing:
let dryRun = config.dryRun(action: .build)
print(dryRun.shellCommand)
// xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release build// Simulators
.destination(.iOSSimulator(name: "iPhone 16"))
.destination(.iOSSimulator(name: "iPhone 16", os: "18.0"))
.destination(.tvOSSimulator(name: "Apple TV"))
.destination(.watchOSSimulator(name: "Apple Watch Series 9"))
.destination(.visionOSSimulator(name: "Apple Vision Pro"))
// Physical devices
.destination(.iOS(id: "DEVICE_UUID"))
.destination(.iOS(name: "My iPhone"))
// Mac
.destination(.macOS())
.destination(.macOS(arch: .arm64))
.destination(.macCatalyst())
// Generic (for archives)
.destination(.generic(platform: .iOS))
// Custom
.destination(.custom("platform=iOS,name=My Device")).buildSetting("CODE_SIGN_IDENTITY", "-")
.buildSetting("PRODUCT_BUNDLE_IDENTIFIER", "com.example.app")
// Or with the DSL
let config = XCBuildConfig {
Project("MyApp.xcodeproj")
CodeSignIdentity.adHoc
DevelopmentTeam("TEAM_ID")
BundleIdentifier("com.example.app")
}.configuration(.debug) // or .release, .custom("Staging")
.sdk(.iOS) // or .iOSSimulator, .macOS, etc.
.architecture(.arm64) // or .x86_64
.derivedDataPath("build/DD")
.resultBundlePath("results.xcresult")
.jobs(8)
.verbose()
.quiet()
.parallelizeTargets()
.addressSanitizer()
.threadSanitizer()do {
let result = try await config.build()
} catch let error as XCBuildError {
switch error {
case .buildFailed(let exitCode, let errors):
print("Build failed with exit code \(exitCode)")
for error in errors {
print(" \(error)")
}
case .testsFailed(let result):
print("\(result.failureCount) tests failed")
case .schemeNotFound(let name, let available):
print("Scheme '\(name)' not found. Available: \(available)")
default:
print(error.localizedDescription)
}
}MIT