xcodebuild Might Be Building Release Without Telling You
Published 24 September 2026
Quick answer:
xcodebuild clean buildwithout-configurationuses whatever the scheme's build action specifies, which is frequently Release. Your.appthen appears underRelease-iphonesimulator, notDebug-iphonesimulator. Pass-configuration Debugexplicitly in any script that goes looking for the product afterwards.
A script builds an app, then goes to find the product:
xcodebuild -scheme MyApp -destination "$DEST" clean build
APP="$DD/Build/Products/Debug-iphonesimulator/MyApp.app"
xcrun simctl install "$SIM" "$APP" # No such file or directory
The build succeeded. The directory is empty. The app is one directory over, in Release-iphonesimulator.
Why
-configuration is optional, and when you omit it xcodebuild doesn't default to Debug — it uses whatever the scheme's build action specifies. That's a per-scheme, per-action setting, and there's no rule saying it must be Debug.
It's easy for a scheme to end up on Release without anyone deciding to do that: a shared scheme edited long ago, one generated by a template, one where someone changed the Run action for a performance check and the Build action came along. Nothing surfaces it, because building Release isn't an error.
The first symptom is usually a path that doesn't exist, which reads like a build failure rather than a configuration surprise.
The fix
Be explicit anywhere a script depends on the output path:
xcodebuild -scheme MyApp \
-configuration Debug \
-destination "$DEST" \
-derivedDataPath "$DD" \
clean build
Or, better, don't guess the path at all — ask for it:
APP=$(xcodebuild -scheme MyApp -configuration Debug -showBuildSettings \
| awk -F' = ' '/ BUILT_PRODUCTS_DIR/ {print $2}')/MyApp.app
-showBuildSettings reports the settings that will actually be used, which removes the guesswork entirely and survives someone changing the scheme later.
Why it matters beyond a missing file
If the script has a fallback — checks Debug, then tries Release — you get something worse than a failure: it works, and you're testing the wrong build.
Release differs in ways that matter for exactly the things scripts tend to do:
- Optimisations are on, so timing and any performance assertion mean something different
DEBUGis not defined, so debug-only menus, test seams and screenshot hooks compile out- Assertions may be disabled
- Symbols and logging differ
Screenshot automation is the common casualty. Those flows usually rely on a #if DEBUG seeding path to populate demo data. Build Release and the seeder isn't in the binary — the app launches to an empty state and you capture perfect screenshots of nothing.
Worth checking in your own scripts
Two greps:
grep -rn "xcodebuild" scripts/ | grep -v -- "-configuration"
grep -rn "Debug-iphonesimulator" scripts/
The first finds invocations that inherit whatever the scheme says. The second finds paths hardcoded to a configuration that may not be the one being built.
The general point
xcodebuild has several arguments that are optional but not defaulted the way you'd expect — -configuration, -derivedDataPath, -destination. Each falls back to something contextual: the scheme, a shared global location, the first matching device.
In interactive use that's convenient. In a script it means the behaviour depends on state living outside the script — a scheme file, another process's DerivedData, whichever simulators happen to be installed. Anything a script depends on should be stated in the script, even when a default exists, because the default is what changes silently.
Related posts
- Your Seeded Simulation Isn't Reproducible If It Iterates a Dictionary Swift randomises Dictionary iteration order per process. Draw from a seeded RNG inside that loop and the same seed produces different results on every launch.
- Approved IAP Localisations Can't Be Changed by API Once an in-app purchase is approved, its display name and description are ACTIVE and immutable through the App Store Connect API. What you can still change, and the 55-character limit that isn't 255.
- Never Gate Access on willRenew A subscriber who turns off auto-renew still has paid access until the period ends. Treating willRenew as the paid check locks them out of an app they're still paying for.