LTO breaks debug information on Apple/Ninja
When using the Ninja generator with Apple's ld
linker, enabling LTO misses flags that are necessary to preserve debugging information. This leads to a bad user experience when attempting to use anything that relies on that debugging information, like Address Sanitizer, Instruments, or gdb/lldb.
Here is a MRE:
cmake_minimum_required(VERSION 3.27)
project(example)
include(CheckIPOSupported)
check_ipo_supported()
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
add_executable(main main.c)
Where main.c
simply has int main() { return 0; }
I then run CMake, build the resulting project, and try to run dsymutil
on the binary.
$ cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Debug
-- The C compiler identification is AppleClang 14.0.3.14030022
-- The CXX compiler identification is AppleClang 14.0.3.14030022
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (11.2s)
-- Generating done (0.0s)
-- Build files have been written to: /Users/areinkin/repro/build
$ cmake --build build/ --verbose
Change Dir: '/Users/areinkin/repro/build'
Run Build Command(s): /opt/homebrew/bin/ninja -v
[1/2] /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -g -flto=thin -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk -MD -MT CMakeFiles/main.dir/main.c.o -MF CMakeFiles/main.dir/main.c.o.d -o CMakeFiles/main.dir/main.c.o -c /Users/areinkin/repro/main.c
[2/2] : && /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -g -flto=thin -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.3.sdk -Wl,-search_paths_first -Wl,-headerpad_max_install_names CMakeFiles/main.dir/main.c.o -o main && :
$ dsymutil build/main
warning: (arm64) /tmp/lto.o unable to open object file: No such file or directory
warning: no debug symbols in executable (-arch arm64)
Naturally, if I leave CMAKE_INTERPROCEDURAL_OPTIMIZATION
off, then -flto=thin
does not appear and the debug symbols are preserved as intended.
The reason this happens is because, as the man
page for ld
explains it,
-object_path_lto filename
When performing Link Time Optimization (LTO) and a temporary mach-o object file is needed, if this option is used, the temporary file will be stored at the specified path and remain after the link is complete. Without the option, the linker picks a path and deletes the object file before the linker tool completes, thus tools such as the debugger or dsymutil will not be able to access the DWARF debug info in the temporary object file.
So clearly the best solution is to set -object_path_lto
to something. I believe CMake itself should set this flag for a few reasons:
- It is extremely useful to have debugging information in the default
Debug
config. - It is quite obscure and there is not much information about this on the internet.
- The Xcode generator adds it automatically, so this is inconsistent behavior.
Here's a workaround for today's projects (with help from @craig.scott)
if (APPLE)
add_link_options("LINKER:-object_path_lto,$<TARGET_PROPERTY:NAME>_lto.o")
add_link_options("LINKER:-cache_path_lto,${CMAKE_BINARY_DIR}/LTOCache")
endif ()
I'm still not 100% sure I'm avoiding name clashes here. Xcode uses the name build/main.build/Debug/Objects-normal/arm64/main_lto.o
for my main
target.