From 6a702ed88add250c669b1cd10750126c5bc76a20 Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Thu, 25 Apr 2024 13:12:46 -0400 Subject: [PATCH] xo-cmake: xo-cmake-config helper script + streamline coverage gen --- CMakeLists.txt | 39 +++++++++-- bin/xo-cmake-config.in | 74 +++++++++++++++++++++ bin/xo-cmake-lcov-harness.in | 121 +++++++++++++++++++++++++++++++++++ cmake/xo_macros/xo_cxx.cmake | 60 ++++++++++++++++- share/xo-macros/gen-ccov.in | 30 +++++++++ 5 files changed, 319 insertions(+), 5 deletions(-) create mode 100755 bin/xo-cmake-config.in create mode 100755 bin/xo-cmake-lcov-harness.in create mode 100644 share/xo-macros/gen-ccov.in diff --git a/CMakeLists.txt b/CMakeLists.txt index e7e30b56..516c8c84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,13 +3,26 @@ cmake_minimum_required(VERSION 3.10) project(xo_macros VERSION 1.0) # if any are useful for this project.. -#include (cmake/foo.cmake) - -# ---------------------------------------------------------------- -# cmake export +include (GNUInstallDirs) set(XO_PROJECT_NAME xo_macros) +# LCOV_EXECUTABLE,GENHTML_EXECUTABLE: needed by xo-cmake-lcov-harness.in +find_program(LCOV_EXECUTABLE NAMES lcov) +find_program(GENHTML_EXECUTABLE NAMES genhtml) + +configure_file( + ${PROJECT_SOURCE_DIR}/bin/xo-cmake-lcov-harness.in + ${PROJECT_BINARY_DIR}/xo-cmake-lcov-harness + @ONLY + ) + +configure_file( + ${PROJECT_SOURCE_DIR}/bin/xo-cmake-config.in + ${PROJECT_BINARY_DIR}/xo-cmake-config + @ONLY + ) + install( FILES "cmake/xo_macros/xo-project-macros.cmake" @@ -18,3 +31,21 @@ install( PERMISSIONS OWNER_READ GROUP_READ WORLD_READ DESTINATION share/cmake/xo_macros ) + +install( + FILES + "${PROJECT_BINARY_DIR}/xo-cmake-lcov-harness" + "${PROJECT_BINARY_DIR}/xo-cmake-config" + PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +# The cmake template gen-ccov.in should be expanded in downstream project; +# to pickup downstream project's PROJECT_SOURCE_DIR / PROJECT_BINARY_DIR +# +install( + FILES + "share/xo-macros/gen-ccov.in" + PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE + DESTINATION ${CMAKE_INSTALL_DATADIR}/xo-macros +) diff --git a/bin/xo-cmake-config.in b/bin/xo-cmake-config.in new file mode 100755 index 00000000..aa8a4d7b --- /dev/null +++ b/bin/xo-cmake-config.in @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +usage() { + echo "$0 [-u|--usage|-h|--help|--lcov-exe|--genhtml-exe|--lcov-harness-exe|--gen-ccov-template|--cmake-module-path]" 1>&2 +} + +help() { + usage + + cat < 0 ]]; do + case "$1" in + -u | --usage) + cmd='usage' + ;; + -h | --help) + cmd='help' + ;; + --lcov-exe) + cmd='lcov_exe' + ;; + --genhtml-exe) + cmd='genhtml_exe' + ;; + --lcov-harness-exe) + cmd='lcov_harness_exe' + ;; + --gen-ccov-template) + cmd='gen_ccov_template' + ;; + --cmake-module-path) + cmd='cmake_module_path' + ;; + *) + usage + exit 1 + ;; + esac + + shift +done + +if [[ $cmd == 'usage' ]]; then + echo -n "usage: " + usage +elif [[ $cmd == 'help' ]]; then + echo -n "help: " + help +elif [[ $cmd == 'lcov_exe' ]]; then + echo -n @LCOV_EXECUTABLE@ +elif [[ $cmd == 'genhtml_exe' ]]; then + echo -n @GENHTML_EXECUTABLE@ +elif [[ $cmd == 'lcov_harness_exe' ]]; then + echo -n @CMAKE_INSTALL_FULL_BINDIR@/xo-cmake-lcov-harness +elif [[ $cmd == 'gen_ccov_template' ]]; then + echo -n @CMAKE_INSTALL_FULL_DATADIR@/xo-macros/gen-ccov.in +elif [[ $cmd == 'cmake_module_path' ]]; then + echo -n @CMAKE_MODULE_PATH@ +fi diff --git a/bin/xo-cmake-lcov-harness.in b/bin/xo-cmake-lcov-harness.in new file mode 100755 index 00000000..414b950a --- /dev/null +++ b/bin/xo-cmake-lcov-harness.in @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +srcdir=$1 +builddir=$2 +outputstem=$3 # optional +lcov=$4 # optional +genhtml=$5 # optional + +if [[ -z "${srcdir}" ]]; then + echo "xo-cmake-lcov-harness: expected non-empty srcdir" + exit 1 +fi + +if [[ -z "${builddir}" ]]; then + echo "xo-cmake-lcov-harness: expected non-empty builddir" + exit 1 +fi + +if [[ -z ${outputstem} ]]; then + outputstem=$builddir/ccov/out +fi + +if [[ -z ${lcov} ]]; then + lcov=@LCOV_EXECUTABLE@ + if [[ $lcov == "LCOV_EXECUTABLE-NOTFOUND" ]]; then + echo "xo-cmake-lcov-harness: lcov executable not found during xo-cmake build/install" + exit 1 + fi +fi + +if [[ -z ${genhtml} ]]; then + genhtml=@GENHTML_EXECUTABLE@ + if [[ $genhtml == "GENHTML_EXECUTABLE-NOTFOUND" ]]; then + echo "xo-cmake-lcov-harness: genhtml executable not found during xo-cmake build/install" + exit 1 + fi +fi + +mkdir -p $builddir/ccov + +# directory stems for location of {.gcda, gcno} coverage information, +# +# if we have source tree: +# +# ${srcdir} +# +- foo +# | \- foo.cpp +# \- bar +# \- quux +# +- quux.cpp +# \- quux_main.cpp +# +# then we expect build tree: +# +# ${builddir} +# +- foo +# | \- CMakeFiles +# | \- foo_target.dir +# | +- foo.cpp.gcda +# | \- foo.cpp.gcno +# +- bar +# \- quux +# \- CMakeFiles +# \- target4quux.dir +# +- quux.cpp.gcda +# +- quux.cpp.gcno +# +- quux_main.cpp.gcda +# \- quux_main.cpp.gcno +# +# in which case will have cmd_body: +# +# ${primarydirs} +# ./foo/CMakeFiles/foo_target.dir +# ./bar/quux/CMakeFiles/target4quux.dir +# +# here foo_target, quux_target are whatever build is using for corresponding cmake target names. +# +# We want to invoke lcov like: +# +# lcov --capture \ +# --output ${builddir}/ccov \ +# --exclude /utest/ \ +# --base-directory ${srcdir}/foo --directory ${builddir}/foo/CMakeFiles/foo_target.dir \ +# --base-directory ${srcdir}/bar/quux --directory ${builddir}/bar/quux/CMakeFiles/target4quux.dir +# +primarydirs=$(cd ${builddir} && find -name '*.gcno' \ + | xargs --replace=xx dirname xx \ + | uniq \ + | sed -e 's:^\./::') + +#echo "primarydirs=${primarydirs}" + +cmd="${lcov} --output ${outputstem}.info --capture --ignore-errors source" + +for bdir in ${primarydirs}; do + sdir=$(dirname $(dirname ${bdir})) + + cmd="${cmd} --base-directory ${srcdir}/${sdir} --directory ${builddir}/${bdir}" +done + +#echo cmd=${cmd} + +set -x + +# capture +${cmd} + +# keep only files with paths under source tree +# (don't want coverage for external libraries such as libstdc++ etc) +${lcov} --extract ${outputstem}.info "${srcdir}/*" --output ${outputstem}2.info + +# remove unit test dirs +# (we're interested in coverage of our installed code, not of the unit tests that exercise it) +${lcov} --remove ${outputstem}2.info '*/utest/*' --output ${outputstem}3.info + +# generate .html tree +mkdir -p ${builddir}/ccov/html +${genhtml} --ignore-errors source --show-details --prefix ${srcdir} --output-directory ${builddir}/ccov/html ${outputstem}3.info + +# also send report to stdout +${lcov} --list ${outputstem}3.info diff --git a/cmake/xo_macros/xo_cxx.cmake b/cmake/xo_macros/xo_cxx.cmake index 3fcf3e2f..4d5bb431 100644 --- a/cmake/xo_macros/xo_cxx.cmake +++ b/cmake/xo_macros/xo_cxx.cmake @@ -1,5 +1,19 @@ +macro(xo_cxx_config_message) + message(STATUS "GUESSED_CMAKE_CMD=cmake -DXO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE} -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_DOCDIR=${CMAKE_INSTALL_DOCDIR} -B ${CMAKE_BINARY_DIR}") + message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}") + message(STATUS "CMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}") +endmacro() + +macro(xo_cxx_bootstrap_message) + if (NOT XO_SUBMODULE_BUILD) + xo_cxx_config_message() + endif() +endmacro() + +# deprecated -- prefer xo_cxx_toplevel_options2() macro(xo_cxx_toplevel_options) + message(WARNING "deprecated: prefer xo_cxx_toplevel_options2") enable_language(CXX) xo_toplevel_compile_options() xo_toplevel_testing_options() @@ -11,6 +25,50 @@ macro(xo_toplevel_testing_options) add_code_coverage_all_targets(EXCLUDE /nix/store* utest/*) endmacro() +macro(xo_cxx_toplevel_options2) + enable_language(CXX) + xo_toplevel_compile_options() + enable_testing() +endmacro() + +# coverage build: +# 0. +# (cmake -DCMAKE_BUILD_TYPE=coverage ..) +# 1. invoke instrumented executables for which you want coverage: +# (cmake --build path/to/build -- test) +# 2. post-process low-level coverage data +# (path/to/build/gen-ccov) +# 3. point browser to generated html data +# file:///path/to/build/ccov/html/index.html +# +macro(xo_toplevel_coverage_config2) + #find_program(LCOV_EXECUTABLE NAMES lcov) + #find_program(GENHTML_EXECUTABLE NAMES genhtml) + # see bin/xo-cmake-lcov-harness in this repo + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --lcov-harness-exe OUTPUT_VARIABLE XO_CMAKE_LCOV_HARNESS_EXECUTABLE) + execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --gen-ccov-template OUTPUT_VARIABLE XO_CMAKE_GEN_CCOV_TEMPLATE) + + if (NOT DEFINED PROJECT_CXX_FLAGS_COVERAGE) + # note: for clang would use -fprofile-instr-generate -fcoverage-mapping here instead and also at link time + set(PROJECT_CXX_FLAGS_COVERAGE -ggdb -Og -fprofile-arcs -ftest-coverage + CACHE STRING "coverage c++ compiler flags") + endif() + message("-- PROJECT_CXX_FLAGS_COVERAGE: coverage c++ flags are [${PROJECT_CXX_FLAGS_COVERAGE}]") + + add_compile_options("$<$:${PROJECT_CXX_FLAGS_COVERAGE}>") + # when -DCMAKE_BUILD_TYPE=coverage, must also link executables with gcov + link_libraries("$<$:gcov>") + + if("${XO_CMAKE_GEN_CCOV_TEMPLATE}" STREQUAL "") + message(ERROR "xo_toplevel_testing_config2: XO_CMAKE_GEN_CCOV_TEMPLATE not set") + message(ERROR "see xo_toplevel_testing_options2()") + else() + message(DEBUG "XO_CMAKE_GEN_CCOV_TEMPLATE=${XO_CMAKE_GEN_CCOV_TEMPLATE}") + configure_file(${XO_CMAKE_GEN_CCOV_TEMPLATE} ${PROJECT_BINARY_DIR}/gen-ccov @ONLY) + file(CHMOD ${PROJECT_BINARY_DIR}/gen-ccov PERMISSIONS OWNER_READ OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) + endif() +endmacro() + macro(xo_toplevel_compile_options) define_property( TARGET @@ -557,7 +615,7 @@ macro(xo_dependency_helper target visibility dep) xo_dependency_helper1(${target} ${visibility} repo/${_nxo_dep}/include) endif() else() - message("-- [${target}] find_package(${dep}) (xo_dependency_helper)") + message(STATUS "[${target}] find_package(${dep}) (xo_dependency_helper)") find_package(${dep} CONFIG REQUIRED) endif() diff --git a/share/xo-macros/gen-ccov.in b/share/xo-macros/gen-ccov.in new file mode 100644 index 00000000..c81c3f53 --- /dev/null +++ b/share/xo-macros/gen-ccov.in @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +srcdir=@PROJECT_SOURCE_DIR@ +builddir=@PROJECT_BINARY_DIR@ + +#lcov=@LCOV_EXECUTABLE@ +#genhtml=@GENHTML_EXECUTABLE@ +# +#if [[ $lcov == "LCOV_EXECUTABLE-NOTFOUND" ]]; then +# echo "gen-ccov: lcov executable not found" +# exit 1 +#fi +# +#if [[ $genhtml == "GENHTML_EXECUTABLE-NOTFOUND" ]]; then +# echo "gen-ccov: genhtml executable not found" +# exit 1 +#fi + +lcovharness=@XO_CMAKE_LCOV_HARNESS_EXECUTABLE@ + +if [[ -z $lcovharness ]]; then + echo "gen-ccov: lcov-harness executable (XO_CMAKE_LCOV_HARNESS_EXECUTABLE) not configured" + echo "gen-ccov: expect value of path/to/xo-cmake-config --lcov-harness-exe" + echo "gen-ccov: stored in XO_CMAKE_LCOV_HARNESS_EXECUTABLE by xo_toplevel_testing_options2()" + exit 1 +fi + +# TODO: allow providing LCOV_EXECUTABLE GENHTML_EXECUTABLE here + +$lcovharness $srcdir $builddir