diff --git a/.pipelines/v2/templates/stages-build-native.yml b/.pipelines/v2/templates/stages-build-native.yml index b8f8f0e8..0428c391 100644 --- a/.pipelines/v2/templates/stages-build-native.yml +++ b/.pipelines/v2/templates/stages-build-native.yml @@ -108,6 +108,9 @@ stages: - output: pipelineArtifact artifactName: 'cpp-native-linux-x64' targetPath: '$(Build.ArtifactStagingDirectory)/native' + - output: pipelineArtifact + artifactName: 'cpp-native-symbols-linux-x64' + targetPath: '$(Build.ArtifactStagingDirectory)/symbols' steps: - template: steps-build-linux.yml parameters: @@ -139,6 +142,9 @@ stages: - output: pipelineArtifact artifactName: 'cpp-native-osx-arm64' targetPath: '$(Build.ArtifactStagingDirectory)/native' + - output: pipelineArtifact + artifactName: 'cpp-native-symbols-osx-arm64' + targetPath: '$(Build.ArtifactStagingDirectory)/symbols' steps: - template: steps-build-macos.yml parameters: diff --git a/.pipelines/v2/templates/steps-build-linux.yml b/.pipelines/v2/templates/steps-build-linux.yml index b16b13fd..a9ac31a7 100644 --- a/.pipelines/v2/templates/steps-build-linux.yml +++ b/.pipelines/v2/templates/steps-build-linux.yml @@ -96,4 +96,21 @@ steps: fi cp -P "$primary" "$dst/" echo " staged libfoundry_local.so" + displayName: 'Stage native artifacts' + +# Stage debug symbols into a separate artifact so they can be published +# independently without being bundled into the Python wheel (which consumes +# the native artifact wholesale and must not contain the .dbg file). +- bash: | + set -euo pipefail + src='$(Build.SourcesDirectory)/sdk_v2/cpp/build/Linux/${{ parameters.buildConfig }}/bin' + dst='$(Build.ArtifactStagingDirectory)/symbols' + mkdir -p "$dst" + + dbg="$src/libfoundry_local.so.dbg" + if [ -f "$dbg" ]; then + cp -P "$dbg" "$dst/" + echo " staged libfoundry_local.so.dbg" + fi + displayName: 'Stage debug symbol artifacts' diff --git a/.pipelines/v2/templates/steps-build-macos.yml b/.pipelines/v2/templates/steps-build-macos.yml index 278bdbff..dc2e57c4 100644 --- a/.pipelines/v2/templates/steps-build-macos.yml +++ b/.pipelines/v2/templates/steps-build-macos.yml @@ -101,4 +101,23 @@ steps: fi cp -P "$primary" "$dst/" echo " staged libfoundry_local.dylib" + displayName: 'Stage native artifacts' + +# Stage debug symbols into a separate artifact so they can be published +# independently without being bundled into the Python wheel (which consumes +# the native artifact wholesale and must not contain the .dSYM bundle; +# the bundle is a directory whose inner DWARF file shares the name +# libfoundry_local.dylib and would corrupt the wheel if copied flat). +- bash: | + set -euo pipefail + src='$(Build.SourcesDirectory)/sdk_v2/cpp/build/macOS/${{ parameters.buildConfig }}/bin' + dst='$(Build.ArtifactStagingDirectory)/symbols' + mkdir -p "$dst" + + dsym="$src/libfoundry_local.dylib.dSYM" + if [ -d "$dsym" ]; then + cp -r "$dsym" "$dst/" + echo " staged libfoundry_local.dylib.dSYM" + fi + displayName: 'Stage debug symbol artifacts' diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index f4a3c2dd..2c26d5a5 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -447,6 +447,11 @@ target_include_directories(foundry_local_cpp ) target_link_libraries(foundry_local_cpp INTERFACE foundry_local Microsoft.GSL::GSL) +# -------------------------------------------------------------------------- +# Split debug symbols (Linux / macOS, RelWithDebInfo only) +# -------------------------------------------------------------------------- +include(cmake/SplitDebugSymbols.cmake) + # -------------------------------------------------------------------------- # Tests # -------------------------------------------------------------------------- diff --git a/sdk_v2/cpp/cmake/SplitDebugSymbols.cmake b/sdk_v2/cpp/cmake/SplitDebugSymbols.cmake new file mode 100644 index 00000000..ea116a62 --- /dev/null +++ b/sdk_v2/cpp/cmake/SplitDebugSymbols.cmake @@ -0,0 +1,111 @@ +# Copyright (c) Microsoft. All rights reserved. +# +# Splits debug symbols from the foundry_local shared library for RelWithDebInfo +# builds on Linux and macOS. +# +# Linux: objcopy --only-keep-debug + strip --strip-debug + +# objcopy --add-gnu-debuglink wires the stripped .so back to its .dbg +# companion. GDB/LLDB find the .dbg automatically via the GNU +# debuglink section — no user configuration needed. +# +# macOS: dsymutil harvests DWARF from the .o debug maps into a .dSYM bundle, +# then strip -S removes the N_OSO stab entries from the .dylib. +# LLDB/Xcode find the .dSYM automatically when it sits in the same +# directory as the .dylib. +# +# Note: without this step the current macOS build has no persistent +# debug info — DWARF lives only in .o files that are gone after a +# clean or on any consumer machine. dsymutil is required. +# +# The split happens at build time so local developers get the same layout as CI +# without any manual steps; GDB/LLDB discover the symbols automatically. +# +# This is a no-op for: +# Debug — symbols are intentionally embedded, stripping defeats the purpose. +# Release — compiled without -g, nothing to extract. + +if(NOT (CMAKE_SYSTEM_NAME STREQUAL "Linux" OR APPLE)) + return() +endif() + +# Single-config generators (Ninja/Makefile) set CMAKE_BUILD_TYPE at configure +# time, which is how both the Linux and macOS CI agents invoke build.py. Only +# wire up the split for RelWithDebInfo. +if(NOT CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + return() +endif() + +# -------------------------------------------------------------------------- +# macOS: dsymutil → .dSYM bundle, then strip -S +# -------------------------------------------------------------------------- +if(APPLE) + find_program(DSYMUTIL dsymutil) + if(NOT DSYMUTIL) + message(WARNING + "dsymutil not found — dSYM generation disabled for RelWithDebInfo. " + "Install Xcode Command Line Tools to enable debug symbol extraction.") + return() + endif() + + # dsymutil must run BEFORE strip: it reads the N_OSO stab entries that + # point to .o files to harvest their DWARF. strip -S then removes those + # stab entries from the .dylib, leaving a clean production binary. + add_custom_command(TARGET foundry_local POST_BUILD + COMMAND "${DSYMUTIL}" + "$" + -o "$.dSYM" + COMMAND strip + -S + "$" + COMMENT "Generating dSYM bundle and stripping debug map from libfoundry_local.dylib" + VERBATIM + ) + message(STATUS "RelWithDebInfo: dSYM extraction + strip configured for libfoundry_local.dylib") + +# -------------------------------------------------------------------------- +# Linux: objcopy --only-keep-debug + strip --strip-debug + --add-gnu-debuglink +# -------------------------------------------------------------------------- +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Prefer the toolchain's objcopy/strip for cross-compilation correctness + # (CMAKE_OBJCOPY / CMAKE_STRIP are set by CMake when it detects the + # compiler). Fall back to find_program for environments that don't have + # these variables populated. + set(_objcopy "${CMAKE_OBJCOPY}") + if(NOT _objcopy) + find_program(_objcopy objcopy) + endif() + + set(_strip "${CMAKE_STRIP}") + if(NOT _strip) + find_program(_strip strip) + endif() + + if(NOT _objcopy OR NOT _strip) + message(WARNING + "objcopy/strip not found — debug symbol splitting disabled for RelWithDebInfo. " + "Install binutils to enable symbol extraction.") + return() + endif() + + add_custom_command(TARGET foundry_local POST_BUILD + # 1. Extract DWARF sections into a companion .dbg file. + COMMAND "${_objcopy}" + --only-keep-debug + "$" + "$.dbg" + # 2. Strip DWARF from the .so. --strip-debug keeps the exported symbol + # table (required for dynamic linking) but removes .debug_* sections. + COMMAND "${_strip}" + --strip-debug + "$" + # 3. Embed a GNU debuglink section so GDB/LLDB auto-discover the .dbg + # file. The debuglink stores the basename; GDB searches the same + # directory as the .so first, so colocating them is sufficient. + COMMAND "${_objcopy}" + "--add-gnu-debuglink=$.dbg" + "$" + COMMENT "Splitting debug symbols into libfoundry_local.so.dbg" + VERBATIM + ) + message(STATUS "RelWithDebInfo: debug symbol splitting configured for libfoundry_local.so") +endif()