
Run the merge job on an Apple silicon host so the same CI stage can execute the arm64 slice for a smoke test. Figment's M-series plans start at $95/month; the exact chip, RAM and disk are set in the configuration quote, and physical plans collect requests until supply is confirmed. Start a Figment Mac. The $29 virtual Intel macOS VM can still produce both slices through cross-compilation with a supported Intel Xcode toolchain, but it cannot execute the arm64 output.
A universal binary holds an x86_64 slice and an arm64 slice in a single Mach-O fat file. macOS picks the matching slice at launch, so one download runs natively on every Mac. In CI that means two xcodebuild invocations, a lipo -create merge step, and a verification pass before anything is signed or packaged. This page walks the whole pipeline and ends with a Makefile you can drop into a repo.
Mach-O fat binaries have been part of macOS since the PowerPC-to-Intel transition. The current use case is Intel to Apple silicon. If only x86_64 is present on an M-series Mac, the system runs the binary under Rosetta, which Apple describes in Porting your macOS apps to Apple silicon. Rosetta works, but it adds launch overhead and prevents native arm64 performance features.
You need a universal binary when:
Apps delivered through the Mac App Store do not need a hand-merged fat binary — Xcode archives multiple architectures at submission.
Each architecture needs its own invocation and its own derived-data path so outputs do not collide. ARCHS is defined in Apple's Build Settings Reference, and command-line xcodebuild usage is in Technical Note TN2339. For projects targeting Xcode 27 on Intel, Apple's porting guide explicitly instructs adding x86_64 to ARCHS; if you still run Xcode on an Intel host, Apple says use Xcode 26 or earlier.
# arm64 slice
xcodebuild \
-scheme "MyCLITool" \
-configuration Release \
-derivedDataPath build/derived/arm64 \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=NO \
build
# x86_64 slice
xcodebuild \
-scheme "MyCLITool" \
-configuration Release \
-derivedDataPath build/derived/x86_64 \
ARCHS=x86_64 \
ONLY_ACTIVE_ARCH=NO \
build
Replace MyCLITool with the scheme name reported by xcodebuild -list in your project — this file does not know your scheme. ONLY_ACTIVE_ARCH=NO forces xcodebuild to honour ARCHS instead of defaulting to the host architecture, which is what makes cross-arch compilation work from one machine.
Most CI systems support a matrix or fan-out. Here is a GitHub Actions example that builds both architectures in parallel and merges in a dependent job:
jobs:
build-slice:
runs-on: [self-hosted, macOS, arm64] # Figment M-series runner
strategy:
fail-fast: true
matrix:
arch: [arm64, x86_64]
steps:
- uses: actions/checkout@v4
- name: Build ${{ matrix.arch }} slice
run: |
set -euo pipefail
xcodebuild \
-scheme MyCLITool \
-configuration Release \
-derivedDataPath build/derived/${{ matrix.arch }} \
ARCHS=${{ matrix.arch }} ONLY_ACTIVE_ARCH=NO \
build
cp "build/derived/${{ matrix.arch }}/Build/Products/Release/MyCLITool" \
"MyCLITool-${{ matrix.arch }}"
- uses: actions/upload-artifact@v4
with:
name: slice-${{ matrix.arch }}
path: MyCLITool-${{ matrix.arch }}
merge:
needs: build-slice
runs-on: [self-hosted, macOS, arm64]
steps:
- uses: actions/download-artifact@v4
with: { pattern: slice-*, merge-multiple: true }
- run: make universal
set -euo pipefail and fail-fast: true ensure a failed xcodebuild aborts the pipeline rather than being treated as a quick success in later timing reports. Times from this script are only meaningful when the build itself succeeds.
lipo -create takes two or more architecture-specific Mach-O files and produces a fat binary:
lipo -create \
MyCLITool-arm64 \
MyCLITool-x86_64 \
-output MyCLITool-universal
For a .app bundle, the merge target is the main executable under Contents/MacOS/. If the bundle also contains helper tools, dylibs or embedded .framework executables, every Mach-O file must be merged individually. The Makefile below handles a single-binary CLI tool; for app bundles, enumerate Mach-O files with find Contents -type f -exec file {} + and merge each one.
After the merge, assert both slices survived and that the merged file still passes codesign -v:
set -euo pipefail
lipo -info MyCLITool-universal | grep -qw "arm64" || { echo "FAIL: arm64 missing"; exit 1; }
lipo -info MyCLITool-universal | grep -qw "x86_64" || { echo "FAIL: x86_64 missing"; exit 1; }
# Re-sign the merged binary. Use your Developer ID identity for distribution;
# the keychain and private key must already be unlocked in this job via your
# CI secret manager (never commit certificate files or paste secrets into env).
codesign --force --timestamp --options=runtime --sign "$DEVELOPER_ID_IDENTITY" MyCLITool-universal
codesign -v --strict MyCLITool-universal
lipo creates a new file, so any signature placed on the individual slices is gone. Signing must happen after the merge, not before.
universal TargetDrop this at the repo root and run make universal. Set SCHEME to the value xcodebuild -list prints for your project.
# Makefile — universal binary pipeline
SCHEME := MyCLITool
CONFIG := Release
DERIVED := build/derived
PRODUCTS := build/products
.PHONY: arm64-slice x86_64-slice universal clean
$(PRODUCTS):
mkdir -p $@
arm64-slice: | $(PRODUCTS)
set -euo pipefail; \
xcodebuild \
-scheme "$(SCHEME)" \
-configuration "$(CONFIG)" \
-derivedDataPath "$(DERIVED)/arm64" \
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO \
build
cp "$(DERIVED)/arm64/Build/Products/$(CONFIG)/$(SCHEME)" \
"$(PRODUCTS)/$(SCHEME)-arm64"
x86_64-slice: | $(PRODUCTS)
set -euo pipefail; \
xcodebuild \
-scheme "$(SCHEME)" \
-configuration "$(CONFIG)" \
-derivedDataPath "$(DERIVED)/x86_64" \
ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO \
build
cp "$(DERIVED)/x86_64/Build/Products/$(CONFIG)/$(SCHEME)" \
"$(PRODUCTS)/$(SCHEME)-x86_64"
universal: arm64-slice x86_64-slice
@echo "-> Merging slices with lipo..."
lipo -create \
"$(PRODUCTS)/$(SCHEME)-arm64" \
"$(PRODUCTS)/$(SCHEME)-x86_64" \
-output "$(PRODUCTS)/$(SCHEME)"
@echo "-> Verifying both slices..."
@lipo -info "$(PRODUCTS)/$(SCHEME)" | grep -qw "arm64" \
|| { echo "FAIL: arm64 slice missing after merge"; exit 1; }
@lipo -info "$(PRODUCTS)/$(SCHEME)" | grep -qw "x86_64" \
|| { echo "FAIL: x86_64 slice missing after merge"; exit 1; }
@echo "PASS: $(PRODUCTS)/$(SCHEME) is a valid universal binary."
@echo " Next: sign with Developer ID, then package with dmgbuild."
clean:
rm -rf build
This is an example, not a benchmark. Run it on your own project before wiring it into CI; derived-data paths differ if you override CONFIGURATION_BUILD_DIR in an xcconfig.
| Figment host (listed entry price) | Builds arm64 slice | Builds x86_64 slice | Runs arm64 output |
|---|---|---|---|
| M-series Mac (from $95/mo, request-based; exact chip/RAM/disk in quote) | Yes, natively | Yes, via ARCHS=x86_64 | Yes |
| i3 Mac ($49/mo, request-based) | Yes, via ARCHS=arm64 cross-compile | Yes, natively | No |
| Virtual Intel macOS ($29/mo, 2 vCPU, 8 GiB RAM, 80 GiB disk) | Yes, via ARCHS=arm64 cross-compile with Xcode 26 or earlier | Yes, natively | No |
Prices are listed entry prices, not same-spec comparisons. The $29 plan is a virtual Intel macOS environment on Linux/KVM — a real macOS runtime without GPU acceleration or audio — reached through a browser console. If you also need to execute the arm64 output in the same CI stage (integration tests, UI smoke tests, Rosetta-vs-native perf checks), use M-series.
For a straight equal-price comparison, CloudMac publishes an M4 with 16 GB RAM and 256 GB SSD at $99.99/month, and an annual rate of $999/year (about $83.25/month effective) — $141 less than twelve months at Figment's $95 entry price if you only need an unmanaged M-series Mac. Figment's $249 Managed Build Mac is a different product: it bundles one M-series Mac with CI setup, backups, monitoring and 30 minutes of monthly support, so the comparison is managed CI work, not just the box. Confirm the exact Figment chip, RAM and disk in the quote before you compare.
The merge job only needs lipo, codesign and dmgbuild, all of which run on any Mac. Choose M-series when the same job also has to execute the arm64 binary; choose the virtual Intel macOS for cheap cross-compiled builds that stop at artifact production.
Start a Figment Mac for your universal binary pipeline → · See all Mac hosting plans
Related guides: