diff --git a/CMakeLists.txt b/CMakeLists.txt index bf42af6a..b8d1ce8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,17 @@ cmake_minimum_required(VERSION 3.16) project(one VERSION 6.1.0 LANGUAGES C CXX) list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") +include(DebugSymbols) +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(ONE_DEBUG_SYMBOLS_DEFAULT OFF) +else() + set(ONE_DEBUG_SYMBOLS_DEFAULT ON) +endif() +option(ONE_DEBUG_SYMBOLS "Compile and extract debug symbols" ${ONE_DEBUG_SYMBOLS_DEFAULT}) +if(ONE_DEBUG_SYMBOLS) + sourcemeta_debug_symbols_enable() +endif() + # Options option(ONE_TESTS "Build the One unit tests" ON) option(ONE_SERVER "Build the One server" ON) @@ -86,10 +97,19 @@ if(ONE_INDEX) if(ONE_ENTERPRISE) add_subdirectory(enterprise/index) endif() + + if(ONE_DEBUG_SYMBOLS) + sourcemeta_debug_symbols_extract(sourcemeta_one_index + COMPONENT sourcemeta_one) + endif() endif() if(ONE_SERVER) add_subdirectory(src/server) + if(ONE_DEBUG_SYMBOLS) + sourcemeta_debug_symbols_extract(sourcemeta_one_server + COMPONENT sourcemeta_one) + endif() endif() sourcemeta_target_clang_format(SOURCES diff --git a/Dockerfile b/Dockerfile index 7459dbee..6fe639ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,8 @@ RUN cmake -S /source -B ./build -G Ninja \ -DONE_SERVER:BOOL=ON \ -DONE_TESTS:BOOL=ON \ -DONE_ENTERPRISE:BOOL=OFF \ - -DBUILD_SHARED_LIBS:BOOL=OFF + -DBUILD_SHARED_LIBS:BOOL=OFF \ + -DONE_DEBUG_SYMBOLS:BOOL=ON RUN cmake --build /build \ --config ${SOURCEMETA_ONE_BUILD_TYPE} \ @@ -62,8 +63,12 @@ LABEL org.opencontainers.image.authors="Sourcemeta " COPY --from=builder /usr/bin/sourcemeta-one-index \ /usr/bin/sourcemeta-one-index +COPY --from=builder /usr/bin/sourcemeta-one-index.debug \ + /usr/bin/sourcemeta-one-index.debug COPY --from=builder /usr/bin/sourcemeta-one-server \ /usr/bin/sourcemeta-one-server +COPY --from=builder /usr/bin/sourcemeta-one-server.debug \ + /usr/bin/sourcemeta-one-server.debug COPY --from=builder /usr/share/sourcemeta/one \ /usr/share/sourcemeta/one diff --git a/cmake/DebugSymbols.cmake b/cmake/DebugSymbols.cmake new file mode 100644 index 00000000..1a6696f4 --- /dev/null +++ b/cmake/DebugSymbols.cmake @@ -0,0 +1,74 @@ +macro(sourcemeta_debug_symbols_enable) + message(STATUS "Enabling debug symbols globally") + add_compile_options(-g) + # GCC's `-Wmaybe-uninitialized` produces false positives in + # template code (`std::tuple`, `std::optional`) when `-g` + # changes inlining decisions under `-O3 -flto` + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-Wno-error=maybe-uninitialized) + endif() +endmacro() + +function(sourcemeta_debug_symbols_extract TARGET_NAME) + cmake_parse_arguments(EXTRACT_DEBUG_SYMBOLS "" "COMPONENT" "" ${ARGN}) + if(NOT EXTRACT_DEBUG_SYMBOLS_COMPONENT) + message(FATAL_ERROR "The COMPONENT argument is required") + endif() + + set(BINARY_INPUT "$") + get_target_property(BINARY_OUTPUT_NAME "${TARGET_NAME}" OUTPUT_NAME) + if(NOT BINARY_OUTPUT_NAME) + set(BINARY_OUTPUT_NAME "${TARGET_NAME}") + endif() + get_target_property(BINARY_OUTPUT_DIR "${TARGET_NAME}" RUNTIME_OUTPUT_DIRECTORY) + if(NOT BINARY_OUTPUT_DIR) + get_target_property(BINARY_OUTPUT_DIR "${TARGET_NAME}" BINARY_DIR) + endif() + get_target_property(BINARY_OUTPUT_SUFFIX "${TARGET_NAME}" SUFFIX) + if(NOT BINARY_OUTPUT_SUFFIX) + set(BINARY_OUTPUT_SUFFIX "${CMAKE_EXECUTABLE_SUFFIX}") + endif() + set(BINARY_PATH "${BINARY_OUTPUT_DIR}/${BINARY_OUTPUT_NAME}${BINARY_OUTPUT_SUFFIX}") + + if(APPLE) + message(STATUS "Extracting debug symbols (.dSYM) for: ${TARGET_NAME}") + # With `-flto=full`, the post-LTO object normally lives in + # `/tmp/lto.o` and is removed immediately after the link, which + # makes `dsymutil` silently produce a near-empty bundle. Pin + # the LTO temp under the binary's build dir so it persists long + # enough for `dsymutil` to consume it + target_link_options("${TARGET_NAME}" PRIVATE + "LINKER:-object_path_lto,${BINARY_INPUT}.lto.o") + + find_program(DSYMUTIL_EXECUTABLE dsymutil REQUIRED) + add_custom_command(OUTPUT "${BINARY_PATH}.dSYM" + COMMAND "${DSYMUTIL_EXECUTABLE}" "${BINARY_INPUT}" + -o "${BINARY_PATH}.dSYM" + DEPENDS "${TARGET_NAME}" + VERBATIM) + add_custom_target("${TARGET_NAME}_debug_symbols" ALL + DEPENDS "${BINARY_PATH}.dSYM") + + install(DIRECTORY "${BINARY_PATH}.dSYM" + DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT "${EXTRACT_DEBUG_SYMBOLS_COMPONENT}") + elseif(UNIX) + message(STATUS "Extracting debug symbols (.debug) for: ${TARGET_NAME}") + add_custom_command(OUTPUT "${BINARY_PATH}.debug" + COMMAND "${CMAKE_OBJCOPY}" --only-keep-debug + "${BINARY_INPUT}" "${BINARY_PATH}.debug" + COMMAND "${CMAKE_OBJCOPY}" --strip-debug "${BINARY_INPUT}" + COMMAND "${CMAKE_OBJCOPY}" + "--add-gnu-debuglink=${BINARY_PATH}.debug" "${BINARY_INPUT}" + DEPENDS "${TARGET_NAME}" + VERBATIM) + add_custom_target("${TARGET_NAME}_debug_symbols" ALL + DEPENDS "${BINARY_PATH}.debug") + + install(FILES "${BINARY_PATH}.debug" + DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT "${EXTRACT_DEBUG_SYMBOLS_COMPONENT}") + else() + message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") + endif() +endfunction() diff --git a/enterprise/Dockerfile b/enterprise/Dockerfile index e6680f9d..6bb56a6f 100644 --- a/enterprise/Dockerfile +++ b/enterprise/Dockerfile @@ -47,7 +47,8 @@ RUN cmake -S /source -B ./build -G Ninja \ -DONE_SERVER:BOOL=ON \ -DONE_TESTS:BOOL=ON \ -DONE_ENTERPRISE:BOOL=ON \ - -DBUILD_SHARED_LIBS:BOOL=OFF + -DBUILD_SHARED_LIBS:BOOL=OFF \ + -DONE_DEBUG_SYMBOLS:BOOL=ON RUN cmake --build /build \ --config ${SOURCEMETA_ONE_BUILD_TYPE} \ @@ -94,8 +95,12 @@ LABEL org.opencontainers.image.authors="Sourcemeta " COPY --from=builder /usr/bin/sourcemeta-one-index \ /usr/bin/sourcemeta-one-index +COPY --from=builder /usr/bin/sourcemeta-one-index.debug \ + /usr/bin/sourcemeta-one-index.debug COPY --from=builder /usr/bin/sourcemeta-one-server \ /usr/bin/sourcemeta-one-server +COPY --from=builder /usr/bin/sourcemeta-one-server.debug \ + /usr/bin/sourcemeta-one-server.debug COPY --from=builder /usr/share/sourcemeta/one \ /usr/share/sourcemeta/one diff --git a/test/cli/CMakeLists.txt b/test/cli/CMakeLists.txt index a4f6b226..0135ad7d 100644 --- a/test/cli/CMakeLists.txt +++ b/test/cli/CMakeLists.txt @@ -40,6 +40,7 @@ if(ONE_INDEX) sourcemeta_one_test_cli(common index fail-invalid-schema-uri-percentage) sourcemeta_one_test_cli(common index fail-invalid-schema-yaml) sourcemeta_one_test_cli(common index fail-maximum-entries-non-numeric-value) + sourcemeta_one_test_cli(common index debug-symbols) sourcemeta_one_test_cli(common index help) sourcemeta_one_test_cli(common index help-skip-banner) sourcemeta_one_test_cli(common index no-arguments) @@ -149,3 +150,7 @@ if(ONE_INDEX) sourcemeta_one_test_cli(community index fail-lint-rule-enterprise-only) endif() endif() + +if(ONE_SERVER) + sourcemeta_one_test_cli(common server debug-symbols) +endif() diff --git a/test/cli/index/common/debug-symbols.sh b/test/cli/index/common/debug-symbols.sh new file mode 100755 index 00000000..447de484 --- /dev/null +++ b/test/cli/index/common/debug-symbols.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +BINARY="$1" + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +if [ "$(uname -s)" = "Darwin" ]; then + # `_main` is the Mach-O mangled name of `main`. We resolve its + # address, then ask `atos` to symbolicate. `atos` follows the + # binary's debug map (which can point at a `.dSYM` bundle via + # UUID through Spotlight, at the original `.o` files, or both) + # so the test does not depend on where the symbols ended up. + # When DWARF is reachable the output ends with `(file:line)`; + # without DWARF it ends with `+ ` + ADDRESS="$(nm "$BINARY" | awk '$3 == "_main" {print "0x"$1; exit}')" + test -n "$ADDRESS" || { echo "no main symbol in $BINARY" >&2; exit 1; } + atos -o "$BINARY" "$ADDRESS" > "$TMP/output.txt" + grep -qE '\(.+:[0-9]+\)' "$TMP/output.txt" || { + echo "no source location resolved:" >&2 + cat "$TMP/output.txt" >&2 + exit 1 + } +else + # `nm --line-numbers` annotates each symbol with `:` + # when source mapping is available. It uses BFD, which follows + # the binary's `.gnu_debuglink` section to locate a `.debug` + # sidecar in any standard location, so the test does not depend + # on where the sidecar ended up. We do not pin to a single symbol + # because LTO routinely splits or stubs out specific symbols + # (notably `main`), losing their line mapping while leaving the + # rest of the binary fully annotated. As long as some symbol + # resolves, the sidecar is reachable and DWARF is intact + nm --line-numbers "$BINARY" > "$TMP/output.txt" + grep -qE ':[1-9][0-9]*$' "$TMP/output.txt" || { + echo "no symbol resolved to a source location" >&2 + head -5 "$TMP/output.txt" >&2 + exit 1 + } +fi diff --git a/test/cli/server/common/debug-symbols.sh b/test/cli/server/common/debug-symbols.sh new file mode 100755 index 00000000..447de484 --- /dev/null +++ b/test/cli/server/common/debug-symbols.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +BINARY="$1" + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +if [ "$(uname -s)" = "Darwin" ]; then + # `_main` is the Mach-O mangled name of `main`. We resolve its + # address, then ask `atos` to symbolicate. `atos` follows the + # binary's debug map (which can point at a `.dSYM` bundle via + # UUID through Spotlight, at the original `.o` files, or both) + # so the test does not depend on where the symbols ended up. + # When DWARF is reachable the output ends with `(file:line)`; + # without DWARF it ends with `+ ` + ADDRESS="$(nm "$BINARY" | awk '$3 == "_main" {print "0x"$1; exit}')" + test -n "$ADDRESS" || { echo "no main symbol in $BINARY" >&2; exit 1; } + atos -o "$BINARY" "$ADDRESS" > "$TMP/output.txt" + grep -qE '\(.+:[0-9]+\)' "$TMP/output.txt" || { + echo "no source location resolved:" >&2 + cat "$TMP/output.txt" >&2 + exit 1 + } +else + # `nm --line-numbers` annotates each symbol with `:` + # when source mapping is available. It uses BFD, which follows + # the binary's `.gnu_debuglink` section to locate a `.debug` + # sidecar in any standard location, so the test does not depend + # on where the sidecar ended up. We do not pin to a single symbol + # because LTO routinely splits or stubs out specific symbols + # (notably `main`), losing their line mapping while leaving the + # rest of the binary fully annotated. As long as some symbol + # resolves, the sidecar is reachable and DWARF is intact + nm --line-numbers "$BINARY" > "$TMP/output.txt" + grep -qE ':[1-9][0-9]*$' "$TMP/output.txt" || { + echo "no symbol resolved to a source location" >&2 + head -5 "$TMP/output.txt" >&2 + exit 1 + } +fi