XcodeGen generates an Xcode project from a YAML spec (project.yml), so the project file isn't a 5,000-line merge-conflict generator anymore. The standard install is via Homebrew, which makes the project depend on every developer (and every CI agent) having the right Brew version of the right XcodeGen at the right time. The vendored pattern fixes this by checking the binary into the repo.
- An Xcode project (or willing to start one).
- A
project.ymlfile (or this guide will get you to one). - About 10 MB of repo space for the binary.
Step 1Pin the version, drop the binary in
Pick an XcodeGen release on GitHub and download xcodegen.artifactbundle.zip. Extract just the binary you need (macOS-arm64 or macOS-x86_64). Drop it in a .tooling/ directory at the repo root.
# From the release archive: .tooling/ └── xcodegen # the binary, ~9 MB, marked executable
Make it executable: chmod +x .tooling/xcodegen. Commit it. Yes, you're checking a binary into git — that's the whole point. It's tiny and immutable.
For solo or small teams, you only need one arch (whichever your Macs run). The Xcode Cloud build agents are Apple Silicon, so an arm64 binary works for both local dev and CI. If you have an Intel Mac in the mix, ship both and pick at runtime in the wrapper script.
Step 2Write a tiny wrapper script
Optional but worth it — a one-line shell wrapper that other scripts can call without thinking about where the binary lives.
# scripts/xcodegen #!/usr/bin/env bash set -euo pipefail exec "$(dirname "$0")/../.tooling/xcodegen" "$@"
chmod +x scripts/xcodegen. Now ./scripts/xcodegen generate works the same everywhere.
Step 3The project.yml patterns that actually matter
The XcodeGen docs cover the syntax. Here are the specific patterns that surface as gotchas later — every studio project hits all of these.
Display name in settings, not Xcode
If you set the display name in the Xcode GUI, XcodeGen wipes it on the next regeneration. The right place is settings.base.PRODUCT_NAME in project.yml. This is the bug that caused tmpo's App Review rejection of build 1.0(5).
targets:
Tmpo:
type: application
platform: iOS
settings:
base:
PRODUCT_NAME: Tmpo # NOT "tmpo" — survives regen
PRODUCT_BUNDLE_IDENTIFIER: com.kuhlman.tmpo
INFOPLIST_KEY_CFBundleDisplayName: Tmpo
Entitlements inline, not file-based
If you point XcodeGen at an .entitlements file, it occasionally wipes it to <dict/> on regeneration (this happened repeatedly on Outpost.Co). Declare entitlements inline via entitlements.properties:
targets:
OutpostCo:
entitlements:
properties:
com.apple.developer.icloud-container-identifiers:
- iCloud.com.kuhlman.outpostco
com.apple.developer.icloud-services:
- CloudDocuments
aps-environment: development
Shared scheme + xcscheme committed
Xcode Cloud only builds shared schemes. In project.yml:
schemes:
Tmpo:
build:
targets: { Tmpo: all }
test:
targets: [TmpoTests]
shared: true # required for Xcode Cloud
Then commit the generated .xcscheme file (in *.xcodeproj/xcshareddata/xcschemes/) so Cloud can find it.
Step 4Pre-build script: always regenerate
You don't want to commit the generated .xcodeproj. Instead, gitignore it and regenerate on every build. The trick is making Xcode Cloud do this too.
Create ci_scripts/ci_post_clone.sh in the repo:
#!/usr/bin/env bash set -euo pipefail cd "$CI_PRIMARY_REPOSITORY_PATH" # Trust the vendored binary chmod +x .tooling/xcodegen # Regenerate the .xcodeproj ./.tooling/xcodegen generate echo "✓ XcodeGen regenerated"
Xcode Cloud runs ci_post_clone.sh after cloning, before building. Make it executable: chmod +x ci_scripts/ci_post_clone.sh and commit.
Step 5The .gitignore
# Ignore generated project files *.xcodeproj *.xcworkspace/xcuserdata/ # Don't ignore shared schemes (we want those) !*.xcodeproj/xcshareddata/xcschemes/ # Keep the .tooling binary !.tooling/ !.tooling/xcodegen
- "command not found: xcodegen" in Xcode Cloud — your
ci_post_clone.shisn't executable.chmod +xit locally, commit, push. - Display name reverts after a regen — you set it in Xcode (GUI) instead of
project.yml. See Step 3. - Entitlements file becomes
<dict/>— file-based entitlements are flaky; switch toentitlements.properties. See Step 3. - Watch app silently dropped after regen — your
project.ymlis missing the watch target'sdependenciesentry on the iOS app. The Weather.Co project hit this repeatedly; the rule is now "never run xcodegen on Weather.Co project — the project.yml is stale." - macOS Gatekeeper quarantine. If the binary won't run after download,
xattr -d com.apple.quarantine .tooling/xcodegenclears it. Do this before committing.