Cong Le
← All posts

xcodebuild Might Be Building Release Without Telling You

Published 24 September 2026

Quick answer: xcodebuild clean build without -configuration uses whatever the scheme's build action specifies, which is frequently Release. Your .app then appears under Release-iphonesimulator, not Debug-iphonesimulator. Pass -configuration Debug explicitly 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
  • DEBUG is 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