c++cmakecode-coveragegcovlcov

Alternative to calling target_link_libraries in subdirectory


Case:

I've declared a function setup_target_for_coverage in a separate .cmake-script, added to CMAKE_MODULE_PATH, which prepares a target (mylib) for code coverage analysis. setup_target_for_coverage is called from a subdirectory to mylib (mylib_tests).

The requirement is that everything that has to do with testing and code coverage, for this target only, happens in mylib/tests and setup_target_for_coverage, and from mylib/CMakeLists.txt we only add the test-subdirectory and nothing more. In the end, neither test-specifics or the existence of code coverage is known to mylib, it just "blindly" calls add_subdirectory( tests ).

Example:

It looks like this:

mylib:

# ./mylib/CMakeLists.txt:

project( mylib )
add_library( mylib main.cpp )
# This is all mylib should be aware of, everything else is hidden
add_subdirectory( tests )

mylib_tests:

# ./mylib/tests/CMakeLists.txt:

project( mylib_tests )

# ...

include(code-coverage)
setup_target_for_coverage(
    TARGET mylib
    TEST_RUNNER mylib_tests
)

code-coverage:

# ./cmake/code-coverage.txt:

function(setup_target_for_coverage)
    # Parse arguments...

    # Setting compile flags: Works as expected
    target_compile_options( ${args_TARGET}
        PRIVATE
            -g -O0 --coverage
    )

    # Setting linker flags: THIS FAILS <---------------
    target_link_libraries( ${args_TARGET}
        PRIVATE
            --coverage
    )

    add_custom_target( ${args_TARGET}_coverage}
        # Setup lcov, run "TEST_RUNNER", generate html...
        COMMAND ...
    )

endfunction()

In short, it sets up compiler & linker-flags (--coverage) and adds a custom target which runs gcov/lcov and genhtml.

Issue:

The issue is with target_link_libraries, which (as documented) is required to occur within the same "directory" where the target was created:

The named <target> must have been created in the current directory by a command such as add_executable() or add_library() [...]

I thus get the following error:

Attempt to add link library "--coverage" to target "mylib" which is not built in this directory.

I've tried to get around this by using set_property directly

set_property(
    TARGET ${TARGET} 
    APPEND_STRING PROPERTY LINK_FLAGS "--coverage"
)

to no avail. It fails with the following error, even if in same CMakeLists.txt:

"undefined reference to `__gcov_merge_add'"

It works if target_link_libraries is moved to ./mylib/CMakeLists.txt, but this is as mentioned not fulfilling my requirements.

Any ideas?


Solution

  • Turning comment into an answer. As you've highlighted, you can only call target_link_libraries() on a target defined in the same directory scope (Edit: this restriction no longer exists since CMake 3.13). When you call add_subdirectory(), you enter a new directory scope and therefore hit the problem you've asked about.

    The include() command, on the other hand, does not create a new directory scope. This has two immediately relevant effects:

    A typical pattern might be something like this:

    mylib/CMakeLists.txt:

    add_library( mylib main.cpp )
    include( tests/CMakeLists.txt )
    

    mylib/tests/CMakeLists.txt:

    add_executable( someTest "${CMAKE_CURRENT_LIST_DIR}/someTest.cpp" )
    # ...
    

    Perhaps at least tangentially related, you might also find the ideas in this article to be of interest. It shows how to manage sources and targets across directories, touching on the add_subdirectory() versus include() discussion along the way.