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
- A .DS_Store File Will Break Your Screenshot Upload Upload tools that read image dimensions try to decode every file in the directory — so a Finder artefact you can't see aborts the whole batch with 'image: unknown format'.
- AttributedString(markdown:) Throws Away Your Paragraphs With default options, AttributedString's markdown parser joins paragraphs with no separator and drops list bullets — turning model output into run-on text. The option that fixes it.
- 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.