Pre-building your application's dependencies using Carthage and checking them in to source control has numerous advantages over not doing so. Local build times for developers as well as CI build times are vastly improved, we guarantee the consistency of built dependencies across build machines, and we don't have to dirty up our projects with a hideously over-engineered dependency manager like CocoaPods. However, doing so can lead to LLDB problems for projects developed on more than one machine i.e by development teams larger than one.
When attempting to debug a project with pre-built dependencies, you are likely to come across the following error:
(lldb) po view
error: virtual filesystem overlay file
'/Users/auserwhoisnotyou/Library/Caches/org.carthage.CarthageKit/DerivedData/12.4_12D4e/hopoate/1.7.0/Build/Intermediates.noindex/Hopoate.build/Release-iphonesimulator/Hopoate iOS.build/all-product-headers.yaml' not found
error: couldn't IRGen expression. Please check the above error messages for possible root causes.
The reason for this error is actually quite simple. LLDB is attempting to use the location on the machine that built the dependencies to lookup debug symbols for our dependency, as absolute paths are stored in the binary Swift module. Solve this, and the problem goes away, right?
Not quite. Even if we fix this, LLDB is still likely to lookup debug information for pre-built dependencies in the wrong location. LLDB's debug-symbol lookup ordering can be broadly described as:
- next to the executable/dylib
- through any custom .dSYM location mechanism
- through Spotlight, using the UUID of the executable/dylib. Note that Spotlight doesn't index /tmp
The dSYMs for our dependencies are not located next to the dylibs themselves as they are not copied in to the application bundle. As we have no custom lookup mechanism specified, LLDB resorts to performing a Spotlight search using the dependency UUIDs. This is where it will often fail, preventing the debug session from operating using symbols.
In summary, we have two problems to solve. We must prevent search paths from being hardcoded in to our built frameworks, and we must provide a way for LLDB to find the dSYMs for our built frameworks during debugging.
To solve this debug nightmare, we require a three step solution. The first step ensures portable debug information is generated when building our XCFramework dependencies. Step 2 provides a way for LLDB to locate the correct dSYMs, and the final step automates the whole process to ensure a consistent debugging experience across development machines. Let's get started!
This one is actually fairly simple. Start by creating an XCConfig file that will be used to provide build settings to Carthage. It should look like this:
SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO
OTHER_SWIFT_FLAGS=$(inherited) -Xfrontend -no-serialize-debugging-options
The -no-serialize-debugging-options
option prevents information about Swift module search paths being included in the generated .swiftModule
. We also set SWIFT_SERIALIZE_DEBUGGING_OPTIONS
to NO
to prevent Xcode from passing -serialize-debugging-options
. Save this as carthage.xcconfig
in your repository root directory.
Next, we need to make Carthage use this configuration. We create a shell script named carthage.sh
to achieve this:
#!/bin/bash
export XCODE_XCCONFIG_FILE=$PWD/carthage.xcconfig
carthage_command="carthage $@ --platform iOS --use-xcframeworks --no-use-binaries"
$carthage_command
This creates a carthage command using the arguments passed to the script, and exports our config file to XCODE_XCCONFIG_FILE
so that Carthage can find it. This allows us to change our Carthage update step from carthage update --platform iOS --use-xcframeworks --no-use-binaries
to ./carthage.sh update
. A tidier command and portable debug information. Nice!
Note that, as mentioned earlier, LLDB can be configured to look in a custom location for dSYMs before resorting to using Spotlight. The LLDB Documentation for the macOS DebugSymbols.framework includes a section on File Mapped UUID Directories, and this is the technique that we will use. Put simply, each dependency executable has a UUID associated with it that you can find quite easily using dwarfdump:
$ xcrun dwarfdump --uuid ./Carthage/Build/Hopoate.xcframework/ios-arm64/Hopoate.framework/Hopoate
UUID: AFD8D983-33F0-3110-B71B-07BF1550C026 (arm64) ./Carthage/Build/Hopoate.xcframework/ios-arm64/Hopoate.framework/Hopoate
We can use the UUID from dwarfdump
to create a series of directories that result in the final 12-byte chunk of the UUID being used as a file that sym-links back to the location of the corresponding executable's dSYM. Remember that as our built dependencies are checked in to source control, these paths can be created relative to the repository root directory, making them portable across machines. For our example above, the directory structure would be AFD8/D983/33F0/3110/B71B/07BF1550C026
, with 07BF1550C026
being the file linked to the appropriate dSYM.
Let's see how we can automate the process to generate these file mapped directories. Create a script named create-uuid-dirs.sh
and populate it with the following:
#!/bin/bash
# Find all executables embedded in our built XCFrameworks
executable_paths=`ls -l Carthage/Build/*xcframework/*/*.framework/* | grep -Ev '\.h$' | grep "\-rwx" | awk '{print $NF}'`
for executable_path in $executable_paths
do
# $executable_path will resemble something like Carthage/Build/Hopoate.xcframework/ios-arm64/Hopoate.framework/Hopoate
# Get the executable name from the last path component
executable_name=$(echo $executable_path | cut -d'/' -f 6)
# Get the path to the framework architecture directory inside the parent xcframework directory, relative to the Carthage root directory
architecture_directory_path=$(echo $executable_path | cut -d'/' -f 2,3,4)
# Create the path to the architecture's dSYM file that was created when the XCFramework was built
dSYM_path=$architecture_directory_path/dSYMS/$executable_name.framework.dSYM/Contents/Resources/DWARF/$executable_name
# Extract the UUID from the executable.
dwarf_dump=`xcrun dwarfdump --uuid $executable_path`
UUID=${dwarf_dump:6:36}
# Create the UUID directory structure required by LLDB's File Mapped UUID Directories
directory_path=Carthage/dSYMs/UUIDs/${UUID:0:4}/${UUID:4:4}/${UUID:9:4}/${UUID:14:4}/${UUID:19:4}
# Create the directories
mkdir -p $directory_path
# The final leaf node to be sym-linked to the dSYM file
final_node=${UUID:24:12}
# Sym-link back to the executable's dSYM
ln -s ../../../../../../../$dSYM_path $directory_path/$final_node
done
There's rather a lot here, so let's walk through it piece by piece.
In order to find all of the individual architecture frameworks that exist within our built XCFrameworks, we use the ls
command: ls -l Carthage/Build/*xcframework/*/*.framework/*
. This finds everything inside the individual frameworks, not just executables, so we pass it to grep "\-rwx"
, narrowing our results to just the executables. In some cases, a framework vendor may have left a public header as executable, so to avoid those we grep -Ev '\.h$'
. All we want is the paths to the executables from ls
's output, so we use awk '{print $NF}'
to grab the last part of each line of the output, which are the paths to the executables.
Next, we loop over the paths that we just extracted, and perform a 7-step process:
exectuable_name
using cut
to take the last component of the executable path, which is the name of the built framework.cut
on the executable path. Store it in architecture_directory_path
.exectuable_name
and architecture_directory_path
created earlier, and store it in dSYM_path
.dwarfdump
to extract the UUID of the executable.Carthage/dSYMs/UUIDs/
in the structure required by LLDB.final_node
.final_node
file to the dSYM-path
we created earlier.Note that we have created a new directory structure in Carthage/dSYMs/UUIDs/
. These should be checked in to source control alongside your built dependencies.
Now that we have our file mapped directory generation script, we need to add a call to it in the carthage.sh
script we made earlier:
#!/bin/bash
set -euo pipefail
export XCODE_XCCONFIG_FILE=$PWD/carthage.xcconfig
carthage_command="carthage $@ --platform iOS --use-xcframeworks --no-use-binaries"
$carthage_command
rm -rf Carthage/dSYMs
bash ./create-uuid-dirs.sh
This ensures that whenever a Carthage command is run the existing file mapped directories are removed and new ones are created.
Next, we need to tell LLDB where to look to find our UUID directories. We can do this with a simple Run Script build phase in our Xcode project:
repository_root=$PROJECT_DIR
carthage_uuid_directory_path=$repository_root/Carthage/dSYMs/UUIDs
defaults write com.apple.DebugSymbols DBGFileMappedPaths $carthage_uuid_directory_path
This sets the default value for DBGFileMappedPaths
to the root of our generated file mapped directory structure. Adding this as a build phase guarantees that your debug session will look in the correct location for your dependency debug information. You can also verify that this step has set the location correctly by running defaults read com.apple.DebugSymbols DBGFileMappedPaths
in your terminal.
Anyone who has worked on a team using Carthage for dependency management will know the pain of the "couldn't IRGen expression" error. Hopefully, with the tips and tricks in this article, you can resolve the issue once and for all. I would like to thank the good folks at Strava, whose article on fixing this problem for non-XCFramework dependencies pointed me in the right direction regarding the use of File Mapped UUID Directories to provide LLDB with the correct debugging information.