HireVM Engineering Journal

Diagnosing Xcode build graph cycles and duplicate outputs

Diagnosing Xcode build graph cycles and duplicate outputs

When an Xcode build on a remote cloud Mac suddenly reports Cycle inside ... or Multiple commands produce ..., the easiest mistake is to delete DerivedData immediately. That may temporarily change the order of the errors, but it also erases useful diagnostic evidence. A more reliable approach is to hold the code, Scheme, configuration, and Xcode path constant, preserve the complete log, and then determine whether the problem is a Target dependency cycle or multiple Build Phases competing for the same output path.

Capture a stable reproduction first

Before troubleshooting, pause automatic code pulls, dependency upgrades, and concurrent builds so the workspace does not change between experiments. Record the current commit, the Xcode version, and the developer directory that is actually selected:

set -o pipefail
git rev-parse HEAD
xcodebuild -version
xcode-select -p

mkdir -p .diagnostics
xcodebuild \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -showBuildTimingSummary \
  build 2>&1 | tee .diagnostics/build.log

If the project uses a Workspace, replace -project App.xcodeproj with -workspace App.xcworkspace. Do not pass both options. Next, extract the relevant locations while retaining the original log for context:

grep -nE \
  'Cycle inside|cycle in dependencies|Multiple commands produce|will be run during every build' \
  .diagnostics/build.log

The final command shown in the log is usually where the conflict was detected, not necessarily where the cycle began. Follow the dependency chain reported by Xcode upward until you find the first repeated Target, script, or output path.

Distinguish between two similar-looking errors

A dependency cycle means A must wait for B while B, directly or indirectly, must wait for A. A duplicate output means two commands both claim responsibility for the same file. The two errors can occur together, but they require different fixes.

Log symptom Check first Common root cause
Cycle inside Target Dependencies, implicit dependencies App and Framework depend on each other
Multiple commands produce Copy Files, Compile Sources, Run Script The same file is copied or generated twice
Script runs on every build Run Script inputs and outputs Dependencies are undeclared, or dependency analysis is disabled
Failure occurs only during Archive Embed phase, archive scripts Debug and Release phases are configured differently

Use commands to confirm the Targets and configuration actually used for this build rather than guessing from the options currently displayed in the Xcode interface:

xcodebuild -list -json -project App.xcodeproj \
  > .diagnostics/project-list.json

xcodebuild -showBuildSettings \
  -project App.xcodeproj \
  -scheme App \
  -configuration Debug \
  > .diagnostics/build-settings.txt

Trace the dependency chain through Targets and Build Phases

In Xcode, begin under Target Dependencies with the Targets at the start and end of the reported chain. An App can depend on a Framework, but the Framework should not depend on the App merely to access types defined there. Move shared code into a separate module, or remove the reverse reference by using protocols and dependency injection.

Then inspect each Build Phase in turn, paying particular attention to the following relationships:

Check for duplicate ownership

The same source file should not appear in two Compile Sources entries that generate the same object file. Likewise, a Framework should not be handled both by an automatically generated embed phase and copied again by a custom Copy Files phase. For code generators, define a specific output directory and avoid writing generated output back into a source directory that another Target already compiles.

Check implicit dependencies

If a script reads another Target's build product from a fixed path without declaring an explicit dependency, the build order may change with the level of parallelism. Do not hide the problem by disabling parallel builds. That only makes the error harder to trigger; it does not repair the graph.

Declare Run Script inputs and outputs explicitly

Run Script phases are a common source of cycles. When a script reads configuration files, generates Swift files, or copies resources, specify its Input Files and Output Files in the phase, or use file lists. The script must also ensure that each output is owned by exactly one phase.

For example, a script that generates a version file can first validate the required variables, then use a temporary file and an atomic replacement:

set -euo pipefail

input="${SRCROOT}/Config/version.txt"
output="${DERIVED_FILE_DIR}/GeneratedVersion.swift"
tmp="${output}.tmp"

test -f "$input"
mkdir -p "$(dirname "$output")"

version="$(tr -d '
' < "$input")"
printf 'enum GeneratedVersion { static let value = "%s" }
' \
  "$version" > "$tmp"

if test -f "$output" && cmp -s "$tmp" "$output"; then
  rm "$tmp"
else
  mv "$tmp" "$output"
fi

The corresponding phase should declare $(SRCROOT)/Config/version.txt as an input and $(DERIVED_FILE_DIR)/GeneratedVersion.swift as an output. Placing generated files in the derived directory makes ownership easier to establish than modifying repository files directly, and it reduces the risk of concurrent tasks overwriting one another.

Validate the fix with minimal changes

Change only one dependency, one phase, or one output path at a time, then save a new log. Removing several references at once may make the build pass, but it becomes difficult to confirm the actual root cause and may leave the same issue unresolved in the archive configuration.

Validate the fix in this order:

  1. Run one clean build and confirm that the complete graph can be constructed from scratch.
  2. Without cleaning, run two consecutive builds and confirm that scripts correctly skip unchanged inputs.
  3. Check Debug and Release separately. For a release project, also run Archive once.
  4. Compare the two logs and confirm that no new duplicate outputs or unconditional script warnings appear.
  5. Document changes to Target dependencies, phase inputs and outputs, and the reason for each change in the code review description.

Deleting DerivedData can be used as a final isolation experiment, but it is not a fix. A genuinely stable Xcode project should produce the same dependency order across different levels of parallelism and during both clean and incremental builds. Only then will interactive development on a cloud Mac and unattended jobs avoid random failures caused by scheduling changes.

Frequently asked questions

Does deleting DerivedData fix an Xcode build cycle?

Usually not. It removes stale intermediate files, but a cycle caused by target dependencies or duplicate output paths will return during the next build.

Why do Run Script phases often cause build graph problems?

If a script omits declared inputs and outputs or writes to a path owned by another phase, Xcode cannot establish a reliable execution order.

How should the fix be validated?

Run one clean build followed by two builds without cleaning, then confirm that cycle errors, duplicate outputs, and unconditional script warnings are all gone.

Dedicated Apple Silicon physical nodes

Run your next Remote Mac task on a dedicated physical node

Choose from two available configurations, five nodes, and daily, weekly, monthly, or quarterly terms. Review the full configuration and USD amount before ordering.

Choose a configuration and order