Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Run Multiple Xcode Versions on One CI Mac

Running two Xcode versions on the same CI Mac is a solved problem, but every step has to be deliberate or you will quietly ship an Xcode 16 build linked against the Xcode 15 SDK. This guide installs both versions side-by-side, routes each job to the right one with DEVELOPER_DIR, and adds a short assertion script that stops the job before xcodebuild touches a source file.

for a persistent two-Xcode runner, order a Figment M-series Mac (listed from $95/month; exact chip, RAM, and disk are confirmed in the quote). Start at /mac-hosting/start. If your pipeline is Intel-compatible and you only need Xcode 26 or earlier, Figment's $29 virtual Intel macOS plan (2 vCPU, 8 GiB RAM, 80 GiB disk) can run the toolchain directly — Apple states that to run Xcode on an Intel processor you "use Xcode 26 or earlier" in Porting your macOS apps to Apple silicon. Xcode 27 targeting Intel binaries from an Apple-silicon host needs x86_64 added to ARCHS, per the same page.

Why Teams Keep Two Xcode Versions on One Runner

Three concrete reasons drive this pattern:

  1. SDK matrix testing. A library that supports iOS 16 through 18 must compile and test against the SDK that ships with each Xcode release, so the warnings and symbol availability match what the App Store accepts.
  2. Legacy app maintenance. A production app may be pinned to Xcode 15 while a vendored SDK catches up, but the same team's new app builds on Xcode 16 the following week.
  3. Beta adoption on a branch. main stays on the released toolchain while a feature branch exercises the next Xcode beta.

A single M-series Mac can cover all three roles if DEVELOPER_DIR is set per job. One Mac is cheaper than two, and derived-data caches stay warm.

Install Xcode Versions Side-by-Side

Rename each bundle before dropping it in /Applications so the installs do not clobber one another:

/Applications/Xcode_15.4.app
/Applications/Xcode_16.2.app

Apple requires you to accept each version's license once. xcode-select -s sets the global developer directory and needs administrator privileges, as documented in Apple's Technical Note TN2339. During runner provisioning, run:

sudo xcode-select -s /Applications/Xcode_15.4.app
sudo xcodebuild -license accept

sudo xcode-select -s /Applications/Xcode_16.2.app
sudo xcodebuild -license accept

This is one-time setup, not a per-job step.

Automate installs with xcodes

The xcodes CLI installs specific Xcode releases from the command line without the Mac App Store:

brew install xcodesorg/made/xcodes

xcodes install 15.4
mv "/Applications/Xcode.app" "/Applications/Xcode_15.4.app"

xcodes install 16.2
mv "/Applications/Xcode.app" "/Applications/Xcode_16.2.app"

If aria2 is present, xcodes uses it automatically; the project's README notes it can parallelize the download using up to 16 connections. Place a .xcode-version file at the repo root containing the primary version (for example 16.2) so local xcodes select picks the same toolchain developers test against, even though CI still routes with DEVELOPER_DIR.

Disk space. Measure your own installs with du -sh /Applications/Xcode_*.app. Xcode bundles plus a few simulator runtimes commonly land in the tens of gigabytes, so request a quote with your expected free-space need rather than assuming a default disk size.

Switch Versions with DEVELOPER_DIR

Inside a job, do not change the global xcode-select default — it is a sudo call that races with every other job on the runner. Set DEVELOPER_DIR in the job environment instead:

export DEVELOPER_DIR=/Applications/Xcode_16.2.app/Contents/Developer
xcodebuild -version

DEVELOPER_DIR overrides the global choice for every tool invoked from that shell — xcodebuild, xcrun, swiftc, swift — and does not leak into other jobs.

Route CI Jobs to the Right Xcode

Pass DEVELOPER_DIR at the job or step level. GitHub Actions example:

# .github/workflows/build.yml
jobs:
  build_xcode15:
    runs-on: [self-hosted, macOS]
    env:
      DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer
    steps:
      - uses: actions/checkout@v4
      - name: Assert toolchain
        run: bash ci/assert-xcode-version.sh "15.4"
      - name: Build
        run: xcodebuild build -scheme "$APP_SCHEME" -destination "platform=iOS Simulator,name=iPhone 15"

  build_xcode16:
    runs-on: [self-hosted, macOS]
    env:
      DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
    steps:
      - uses: actions/checkout@v4
      - name: Assert toolchain
        run: bash ci/assert-xcode-version.sh "16.2"
      - name: Build
        run: xcodebuild build -scheme "$APP_SCHEME" -destination "platform=iOS Simulator,name=iPhone 16"

Set APP_SCHEME from your project's real scheme — do not hard-code a placeholder. If your CI uses runner tags, dedicate xcode-15 and xcode-16 labels instead; on one shared runner, DEVELOPER_DIR is simpler.

The Assert Script

Keep ci/assert-xcode-version.sh in source control and call it before any xcodebuild step. It reads the active version and exits non-zero if it does not match the expected argument:

#!/usr/bin/env bash
# Usage: bash ci/assert-xcode-version.sh "16.2"
set -euo pipefail

EXPECTED="${1:?expected Xcode version required, e.g. 16.2}"
ACTIVE=$(xcodebuild -version | awk '/^Xcode /{print $2}')

if [[ "${ACTIVE}" != "${EXPECTED}" ]]; then
  echo "ERROR: expected Xcode ${EXPECTED}, active ${ACTIVE}"
  echo "       DEVELOPER_DIR=${DEVELOPER_DIR:-<unset>}"
  exit 1
fi

echo "OK: Xcode ${ACTIVE} active"
echo "    SDK: $(xcrun --show-sdk-path)"

set -euo pipefail guarantees the job stops on the first failure — the script's exit status is the signal, nothing counts a failed build as a pass.

Verify the Active Toolchain

Beyond xcodebuild -version, keep a short health step on every push to main:

xcodebuild -version
xcrun --find xcodebuild
xcrun --show-sdk-path
xcrun simctl list runtimes

A few-second step catches a corrupted DEVELOPER_DIR before it wastes a 10-minute build.

Figment Plan Picker for This Job

NeedFigment planMonthly list
Intel-only toolchain, Xcode 26 or earlier, small teamVirtual Intel macOS$29
ARM targets, two Xcode versions on one runnerM-series (request)from $95
Fully managed CI on one M-series Mac: setup, backups, monitoring, 30 min/mo supportManaged Build Mac$249

A twelve-month comparison against one competitor, so you can judge entry price honestly: Figment M-series at $95/month lists at $1,140 per year; CloudMac's pricing page shows an M4 (16 GB RAM, 256 GB SSD) at $99.99/month, and cloudmac.org advertises $999/year for the same tier. That annual rate works out to $83.25 effective monthly and sits $141 below 12 × $95. Figment's $95 entry is an unspecified M-series — chip and RAM are confirmed in your quote, not equivalent to a listed M4. Where Figment earns the budget is the Managed Build Mac tier at $249/month: the two-Xcode DEVELOPER_DIR wiring, backups, and monitoring described above are the work included. If you only need an unmanaged M4, read both pricing pages before you decide.

Pre-Go-Live Checklist

  • Both Xcode bundles live in /Applications with distinct names
  • Each license has been accepted with xcodebuild -license accept
  • DEVELOPER_DIR is set at the job level, not via global sudo xcode-select -s
  • assert-xcode-version.sh runs before the first xcodebuild call in every job
  • Required simulator runtimes are installed under each Xcode
  • A disk-space alert fires before the volume reaches single-digit GB free
  • .xcode-version at the repo root matches the primary CI toolchain

Ready to provision the runner? Review plans at /mac-hosting and open a configuration quote at /mac-hosting/start.

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms