Skip to content

InvokeDev/xc

XCBuild

A type-safe Swift wrapper around xcodebuild with an emphasis on a clean, ergonomic API.

Installation

CLI Tool (xc)

Homebrew (recommended)

brew tap InvokeDev/xc
brew install xc

Manual Download

Download 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 --version

Build from Source

git clone https://github.com/InvokeDev/xc.git
cd xc
swift build -c release
# Binary is at .build/release/xc

Library (XCBuild)

Add XCBuild to your Package.swift:

dependencies: [
    .package(url: "https://github.com/InvokeDev/xc.git", from: "1.0.0")
]

CLI Usage

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.xcactivitylog

Run xc --help for all commands and options.

Features

  • 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

Requirements

  • macOS 13.0+
  • Swift 6.0+
  • Xcode 15.0+

Library Usage

Basic Build

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()

Running Tests

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)")
}

Creating an Archive

let archiveResult = try await XCBuildConfig
    .workspace("MyApp.xcworkspace")
    .scheme("MyApp")
    .configuration(.release)
    .destination(.generic(platform: .iOS))
    .archive(to: "build/MyApp.xcarchive")

Exporting an Archive

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)")
}

Creating an XCFramework

let result = try await createXCFramework {
    Framework("build/Release-iphoneos/MyLib.framework")
    Framework("build/Release-iphonesimulator/MyLib.framework")
    Output("build/MyLib.xcframework")
}

Streaming Build Output

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")")
    }
}

Query Commands

// 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")
}

CI Presets

// 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"
)

Dry Run

Preview the command without executing:

let dryRun = config.dryRun(action: .build)
print(dryRun.shellCommand)
// xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release build

Configuration Options

Destinations

// 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"))

Build Settings

.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")
}

Other Options

.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()

Error Handling

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)
    }
}

License

MIT

About

Type-safe Swift wrapper around xcodebuild

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages