A better way to run xcodebuild. Stop typing flags. Start shipping.
That's it. No more copying 200-character xcodebuild invocations from your wiki. Define your commands once in xc.yaml, use them forever.
# Before
xcodebuild build \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-destination "platform=iOS Simulator,name=iPhone 17 Pro" \
-derivedDataPath ./DerivedData
# After
xc build:releaseBuilt for fast, repeatable Xcode workflows.
- One config, shared by all: Keep commands in
xc.yamlso everyone runs the same builds. - Variants over flags: Use
build:releaseortest:coverageinstead of long flag strings. - Named destinations: Use
sim,mac, ordevicealiases instead of full destination specs. - Script support: Run tools like
swiftlintortuist generateas first-class commands. - Env var support: Use
${VAR}and${VAR:-default}to keep configs portable. - Hooks built in: Run pre/post steps for linting, setup, or notifications.
- Readable output: Use xcbeautify by default, switch to raw logs with
--raw.
brew install alexmx/tools/xcTo update:
brew upgrade alexmx/tools/xcmise use --global github:alexmx/xcOr in mise.toml for a project-scoped install:
[tools]
"github:alexmx/xc" = "latest"cd your-xcode-project
xc init # auto-detects your project, generates xc.yaml
xc build # build with defaults
xc test # run tests
xc build:release # switch to release in one word
xc doctor # verify everything is set up correctlyxc.yaml at the root of your project:
workspace: App.xcworkspace
destinations:
sim: "platform=iOS Simulator,name=${IOS_SIMULATOR:-iPhone 17 Pro}"
mac: "platform=macOS"
defaults:
scheme: App
configuration: Debug
destination: sim
commands:
build:
hooks:
pre: "tuist generate"
variants:
release:
configuration: Release
test:
scheme: AppTests
variants:
ci:
result-bundle-path: "./build/tests.xcresult"
extra-args:
- "-enableCodeCoverage"
- "YES"
clean: {}
archive:
configuration: Release
archive-path: "./build/App.xcarchive"
lint:
run: "swiftlint lint --quiet"
variants:
fix:
run: "swiftlint lint --fix"That config gives you:
xc build # debug build (runs tuist generate first)
xc build:release # release build
xc test # run tests
xc test:ci # tests with coverage + result bundle
xc archive # create release archive
xc clean # clean build
xc lint # run swiftlint
xc lint:fix # autofix lint issuesxc <command>[:<variant>] [options] [-- extra-xcodebuild-args...]
| Command | |
|---|---|
xc <command> |
Run a configured command |
xc list |
Show available commands and variants |
xc init |
Generate xc.yaml from your project |
xc doctor |
Validate setup and diagnose issues |
xc destinations |
List available simulators and platforms |
| Option | |
|---|---|
--dest <name> |
Override destination by name or raw string |
--raw |
Skip xcbeautify, show raw xcodebuild output |
-v, --verbose |
Print the resolved xcodebuild invocation |
--dry-run |
Print the command without executing it |
--version |
Show version |
xc test --dest mac # test on macOS
xc build --verbose # see what xcodebuild gets
xc build --dry-run # inspect without running
xc test --raw -- -enableAddressSanitizer YES # raw output + extra flagsGive short names to long destination strings:
destinations:
sim: "platform=iOS Simulator,name=iPhone 17 Pro"
sim-ipad: "platform=iOS Simulator,name=iPad Pro 13-inch (M5)"
mac: "platform=macOS"
defaults:
destination: simTest on multiple destinations at once:
commands:
test:
destination:
- sim
- sim-ipadRun xc destinations to see what's available on your machine.
A variant inherits everything from its parent command and overrides only what it specifies:
commands:
build:
scheme: App
configuration: Debug
variants:
release:
configuration: Release # only this changes
core:
scheme: Core # different scheme, same configxc build # scheme: App, configuration: Debug
xc build:release # scheme: App, configuration: Release
xc build:core # scheme: Core, configuration: DebugAdd a run field to execute shell scripts instead of xcodebuild:
commands:
generate:
run: "tuist generate"
lint:
run: "swiftlint lint --quiet"
variants:
fix:
run: "swiftlint lint --fix"
loc:
run: "find Sources -name '*.swift' | xargs wc -l | tail -1"Scripts support hooks, variants, extra-args, and --dry-run like any other command.
Use ${VAR} or ${VAR:-default} anywhere in the config:
destinations:
sim: "platform=iOS Simulator,name=${IOS_SIMULATOR:-iPhone 17 Pro}"
commands:
archive:
archive-path: "${BUILD_DIR:-./build}/App.xcarchive"IOS_SIMULATOR="iPhone SE" xc test # override from envRun scripts before and after any command:
commands:
build:
hooks:
pre: "swiftlint lint"
post: "say 'build complete'"
variants:
release:
hooks:
pre: "swiftlint lint --strict" # overrides the command hooks
quick:
hooks: {} # disables hooks for this variantHooks defined on a command run for all its variants. A variant can override hooks or disable them entirely with hooks: {}.
Settings layer from most to least specific:
- CLI flags (
--dest,-- extra-args) - Variant config
- Command config
- Project defaults (
defaultsinxc.yaml) - Global defaults (
~/.config/xc/config.yaml)
Shared defaults across all your projects at ~/.config/xc/config.yaml:
defaults:
destination: "platform=iOS Simulator,name=iPhone 17 Pro"
settings:
formatter: xcbeautify
verbose: falseThe formatter setting controls how xcodebuild output is processed. Any value is run as a shell command with xcodebuild output piped into it:
settings:
formatter: xcbeautify # default — auto-detected from PATH
formatter: "xcbeautify --disable-logging" # custom flags
formatter: xcpretty # different tool
formatter: raw # no formattingUse --raw on any command to skip formatting for a single invocation.
Root fields:
| Field | Type | Description |
|---|---|---|
project |
string | Path to .xcodeproj (mutually exclusive with workspace) |
workspace |
string | Path to .xcworkspace |
destinations |
map | Named destination aliases |
defaults |
object | Default settings applied to all commands |
commands |
map | Command definitions (required) |
Command fields:
| Field | Type | Description |
|---|---|---|
run |
string | Shell script (makes this a script command) |
scheme |
string | Xcode scheme |
configuration |
string | Build configuration |
destination |
string or list | Destination name(s) or raw string(s) |
xcconfig |
string | Path to .xcconfig file |
test-plan |
string | Test plan (test commands only) |
result-bundle-path |
string | Result bundle path (test commands only) |
derived-data-path |
string | Custom derived data path |
archive-path |
string | Archive path (archive command only) |
extra-args |
list | Additional xcodebuild arguments |
hooks |
object | pre and post shell commands |
variants |
map | Named variant overrides |
$ xc doctor
OK xc.yaml
OK Workspace App.xcworkspace
OK Scheme: App
OK Scheme: AppTests
OK Dest: sim iPhone 17 Pro
OK Dest: mac macOS
OK xcbeautify
OK Global config ~/.config/xc/config.yaml
$ xc list
defaults scheme: App, configuration: Debug, destination: sim
archive configuration: Release, archive-path: ./build/App.xcarchive
build
:release configuration: Release
clean
lint $ swiftlint lint --quiet
:fix $ swiftlint lint --fix
test scheme: AppTests
:ci result-bundle-path: ./build/tests.xcresult, extra-args: -enableCodeCoverage YES
MIT
