Debugging Cacheable Swift Modules
December 21, 2022 ยท View on GitHub
This is a guide to enable debugging of cacheable Swift modules in local debug builds.
At the time of writing, lldb depends on debugging options embedded in .swiftmodule files. These options include paths that are only valid on the build host. For local builds, this all just works, but for remote builds, it doesn't.
The solution is two parts:
- Use
-no-serialize-debugging-optionsglobally, to prevent embedded paths - Use
-serialize-debugging-optionslocally in one empty module
Globally disabling debugging options makes those .swiftmodules usable on any machine. Locally enabling debugging options for one module provides lldb with enough info to make debugging work.
An lldb bug has been filed here: https://bugs.swift.org/browse/SR-11485
Note
If you don't care about cache misses, instead of following this guide you can instead disable the
swift.cacheable_swiftmodulesfeature:--features=-swift.cacheable_swiftmodules. This is not recommended though if you use a remote cache.
Disable Debugging Options Globally
To globally disable debugging options, use the swift.cacheable_swiftmodules feature, which is enabled by default, in rules_swift. What this does is ensure all modules are built explicitly with -no-serialize-debugging-options. It has to be explicit because swiftc enables -serialize-debugging-options in some cases.
Add Debug Build Config
In a BUILD file - in this example, the root BUILD file, define the following config_setting. This will allow targets to conditionally depend on the locally built debugging module.
config_setting(
name = "debug",
values = {
"compilation_mode": "dbg",
},
)
Define Local Debug Target
In the BUILD file of your choice, define a swift_library with:
- A single empty source file
- Enables
-serialize-debugging-options - Built locally, not remote - using the
no-remotetag
Here is one way to define the BUILD file, using a genrule to create the empty swift file.
genrule(
name = "empty",
outs = ["empty.swift"],
cmd = "touch $(OUTS)",
)
swift_library(
name = "_LocalDebugOptions",
srcs = [":empty"],
copts = [
"-Xfrontend",
"-serialize-debugging-options",
],
module_name = "_LocalDebugOptions",
tags = ["no-remote"],
visibility = ["//visibility:public"],
)
Update Top Level Test Targets
Finally, for each top-level test target (ios_unit_test, ios_ui_test*, etc), conditionally add the local debugging module to the deps. This is done via the debug config. In the past this also had to be done for ios_application targets but that has since been fixed in lldb.
debug_deps = select({
"//:debug": ["//some/path:_LocalDebugOptions"],
"//conditions:default": [],
})
ios_unit_test(
name = "...",
deps = debug_deps + [
# ...
],
# ...
)
Note about ios_unit_test
When using a test host, the debugging module must be added to the test host target only, not the unit test target. However, for tests without a test host, the debugging module must be added to the unit test target.
LLDB Settings
Additional settings may be required, depending on your build setup. For example, an Xcode Run Script may look like:
echo "settings set target.sdk-path $SDKROOT"
echo "settings set target.swift-framework-search-paths $FRAMEWORK_SEARCH_PATHS"
Other settings you can try customizing are:
target.clang-module-search-pathstarget.debug-file-search-pathstarget.sdk-pathtarget.swift-extra-clang-flagstarget.swift-framework-search-pathstarget.swift-module-search-pathstarget.use-all-compiler-flagssymbols.clang-modules-cache-path
These settings would be written to some project specific lldbinit file which you can include directly in Xcode's scheme.