Diagnosing “Dependency Cycle Between Targets” in Xcode 9’s New Build System

Xcode 9 includes a new build system that can substantially improve build times. The new build system is more strict about build issues, but some of its diagnostic output can be difficult to reason about. One difficult to debug error is “Dependency cycle between targets,” which may appear during incremental builds where you may not have an explicit circular build dependency between targets. Let’s talk about how these types of cycles are introduced and how to troubleshoot and fix them.

There is more than one way to cause this sort of target dependency cycle without adding an explicit target dependency. The behavior of the legacy build system is to continue to (re)build instead of failing. This can increase incremental build times for your project, but the build ultimately succeeds and you may not know there’s a problem. The new build system fails instead, which helps you better maintain your project and means better incremental build times once you’ve fixed the dependency issues.

During a full (clean) build the compiler will emit a list of dependencies for each compilation unit in your project. On subsequent (incremental builds), this data is used by the build system to help determine what parts of your project need to be rebuilt depending on what has changed since the last build. This list may include implicit dependencies (not to be confused with the per-scheme setting!) picked up by the compiler.

First, set this flag to get a bit more diagnostic info in the build log:

defaults write com.apple.dt.XCBuild EnableDebugActivityLogs YES

Reproduce the dependency cycle error and expand the related error message in the build log. Usually there’s a file name in the log. Open that file and make sure its imports are correct. For instance, if a library B is consumed by library A and library B also #imports a file in library A then there’s a circular dependency. Remove the offending import and build again.

There’s at least one more way to introduce implicit dependency cycles. For instance, modulemaps within header search paths will be added as dependencies for a given file automatically. In fact, a file may introduce a dependency cycle with its own module if the (e.g. static library manually specifying a modulemap) target containing the file has a modulemap in its Headers build phase and that phase is placed after the Compile Sources build phase. This can be resolved by dragging the Headers phase above the Compile Sources phase in the Build Phases subtab of your target’s settings in Xcode.

If this doesn’t work then it’s time to dig a bit deeper. You can see a list of dependencies (found by the compiler) for a given file by inspecting the contents of <filename>.d in ~/Library/Developer/Xcode/DerivedData/<project-name>-<some-identifier>/Build/Intermediates.noindex/<name>.build/Debug-<target-device>/<name>.build/Objects-normal/<arch>/ after performing a build.

Look for modulemaps that don’t belong in the dependency list. For instance, if the file is in library A and library A’s target doesn’t depend on libraries B and C, but B and C’s modulemaps appear in the dependency list then library A’s header search paths are likely misconfigured. Clean up library A’s the header search paths and try again.

This is probably not an exhaustive list of ways to cause and fix this error. You can let me know if you know more about this issue on Twitter.