Weather.Co is now free for every pilot — always free for CFIs and flight schools.
Lab · Guides · Vendored XcodeGen
Lab guide · Xcode tooling

Vendored XcodeGen.

Ship the XcodeGen binary inside your repo. No Homebrew, no "works on my machine," no Xcode Cloud "command not found." Every Xcode project from the studio uses this pattern.

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.

Before you start
  • An Xcode project (or willing to start one).
  • A project.yml file (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.

Tip — pick one arch

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
Common gotchas
  • "command not found: xcodegen" in Xcode Cloud — your ci_post_clone.sh isn't executable. chmod +x it 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 to entitlements.properties. See Step 3.
  • Watch app silently dropped after regen — your project.yml is missing the watch target's dependencies entry 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/xcodegen clears it. Do this before committing.

The payoff: a new developer (or a new CI agent) clones the repo and runs nothing extra. open *.xcodeproj produces a clean working project. The version of XcodeGen is exactly the version everyone else is using. No Homebrew drift, no "works on my machine."

Every iOS app from the studio — Weather.Co, tmpo, Outpost.Co, Tailwind, Driver.Co, Cicada — uses this exact pattern.

Sources: XcodeGen — yonaskolb/XcodeGen on GitHub; ~/.claude/projects/-Users-ethankuhlman/memory/reference_xcode_cloud_xcodegen.md; tmpo + Outpost project memory. Tested against XcodeGen 2.43+ and Xcode 16+.