iosdev

Debugging an Xcodebuild hang, when the Swift compiler gets stuck

xcodebuild is vast well of magic I barely understand. My dev nightmare is when it hangs forever without errors or warnings.

Building a complex iOS project with the Release scheme would hang indefinitely - no errors, no messages, just stuck. The Debug config worked fine but any Release configuration scheme would never complete. The build would appear to be running, but never finish.

The Investigation

Initial suspicion: circular dependencies between few local Swift packages that were shared among many other local packages (app has over 20 local packages and about 20+ more remote ones).

Step 1: Initial Database Lock Error

Running xcodebuild with verbose output:

xcodebuild build -project MeridianBet.xcodeproj -scheme XX \
  -destination 'generic/platform=iOS' -verbose 2>&1 | tee build.log

revealed the first issue — build database was locked:

error: unable to attach DB: database is locked
Possibly there are two concurrent builds running in the same filesystem location.

This was very unnecessary self-sabotage step as I had a build running in Xcode which I completely forgot and started another build in command line. Unrelated to the problem but thought to still include it as it’s super easy to run into this.

Thus here just kill all running builds and do Clean derived data.

rm -rf ~/Library/Developer/Xcode/DerivedData/MeridianBet-*

Step 2: The Real Hang

After cleaning, the build started but got stuck during package compilation. The process was running but making no progress.

Check process status:

# Check xcodebuild CPU usage
top -l 1 -pid <xcodebuild_pid> -stats pid,cpu,time,command

Result: xcodebuild at 0.0% CPU - not doing work, but not crashed either.

Find the hung compiler:

pgrep -fl swift

Result: Found swift-frontend process compiling XXData package, also at 0.0% CPU, running for 8+ minutes.

Step 3: Sampling the Hung Process

Used macOS sample command to see what the compiler was doing:

sample <swift-frontend-pid> 1 -file /tmp/swift_sample.txt
grep -A 30 "Call graph:" /tmp/swift_sample.txt

And there it was — the call stack showed an infinite recursion:

swift::GenericSpecializer::run()
  -> swift::trySpecializeApplyOfGeneric()
    -> swift::ReabstractionInfo::prepareAndCheck()
      -> swift::TypeMatcher::visitNominalType()
        -> Traversal::doIt(swift::Type)
          -> Traversal::visitBoundGenericType()
            -> Traversal::doIt(swift::Type) [RECURSING INFINITELY]

The Root Cause

The Swift compiler’s generic specializer was stuck in an infinite loop when trying to optimize complex generic types in the XXData package with -O -whole-module-optimization enabled.

This was not any of the stuff I suspected:

It was:

The funny thing is — I did check each local package on their own. Each one built just fine. But I was using simple auto-generated schema you get when you open Package.swift in Xcode and that did not reveal the issue.

The Solution

Disable whole-module optimization for the problematic package by modifying XXData/Package.swift:

targets: [
    .target(
        name: "XXData",
        dependencies: [...],
        swiftSettings: [
            // Disable whole-module optimization to prevent compiler hang
            // during generic specialization in Release builds
            .unsafeFlags(["-Onone"], .when(configuration: .release))
        ]
    ),
]

DEBUG builds don’t enable whole-module optimization by default, so they never triggered the compiler bug. Release builds (-O -whole-module-optimization) attempted aggressive generic specialization, causing the infinite loop.

Here’s compact summary of key steps as some of these shell tools are quite new for me.

# 1. Clean derived data when build seems stuck
rm -rf ~/Library/Developer/Xcode/DerivedData/MeridianBet-*

# 2. Run build with verbose output in background
xcodebuild build -project Project.xcodeproj -scheme Scheme \
  -destination 'generic/platform=iOS' -verbose 2>&1 | tee build.log &

# 3. Monitor xcodebuild process
top -l 1 -pid <pid> -stats pid,cpu,time,command

# 4. Find hung Swift compiler
pgrep -fl swift

# 5. Sample the hung process to see call stack
sample <pid> 1 -file /tmp/sample.txt
grep -A 30 "Call graph:" /tmp/sample.txt

Lessons Learned

  1. A hang at 0% CPU is different from a hang at 100% CPU - the former suggests waiting on something (or infinite recursion with tail call optimization), the latter suggests an infinite computation loop.
  2. Verbose build output catches database/file lock issues that silent builds hide.
  3. The sample command is invaluable for diagnosing hung processes on macOS.
  4. Compiler bugs can manifest as build hangs - not all hangs are your code’s fault.
  5. Per-package compiler flags are useful for working around compiler issues without affecting the entire project

I have to commend Claude Code (Sonnet 4.5) as invaluable helper for all of this analysis and investigation. Of all the various tasks one can delegate to LLM tools, those are where it truly excels.