2

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?

helmesjo
  • 625
  • 5
  • 17
  • 2
    Can you please give a [mcve]? From what you are showing, it's not clear to me what you are trying to do. Can you give the code of "the setup-function" or at least the `target_link_libraries()` call in question? – Florian Jun 28 '17 at 19:52
  • @Florian Definitely! I wrote this with tired eyes last night, and realize it's rather vague... I'll edit it asap. – helmesjo Jun 29 '17 at 03:54
  • Weird. In CMake terminology "current directory" is introduced with `add_subdirectory()` only, it doesn't distinguish case when function is defined in included file. And such use case (calling `target_link_libraries` from the function, defined not in the current directory) works for me with CMake 3.4. – Tsyvarev Jun 29 '17 at 07:49
  • @Tsyvarev Oh, my bad! My example lies a little in that `setup_target_for_coverage` is actually run within the `tests` subdirectory..! Reason for this is that I don't know the target-name for the test-target until here (mylib is agnostic to what the test does). I'll fix the example. – helmesjo Jun 29 '17 at 08:53
  • @Tsyvarev Though. solving the "test-target name", if it works by having `setup_target_for_coverage` in `mylib/CMakeLists.txt`, that is a much easier hurdle to overcome... – helmesjo Jun 29 '17 at 08:54
  • 1
    Could your requirements be relaxed a little to allow using `include()` instead of `add_subdirectory()`? If so, then this would allow `target_link_libraries()` to be used. The main disadvantage is that inside your `tests` directory, you would have to remember that CMake would be treating the current source directory as `mylib`, not `mylib/tests` (and similar for the corresponding binary directory). – Craig Scott Jun 29 '17 at 11:23
  • @CraigScott That would be a viable sacrifice, thanks for the tip! I'll give it a try! – helmesjo Jun 29 '17 at 11:55

2 Answers2

4

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:

  • In the included directory's file, you can still call target_link_libraries() for a target in the including directory's scope.
  • The value of CMAKE_CURRENT_SOURCE_DIR doesn't change, so you will likely be wanting to use CMAKE_CURRENT_LIST_DIR instead within the included file. Note that there is no equivalent for CMAKE_CURRENT_BINARY_DIR which also doesn't change.

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.

Craig Scott
  • 9,238
  • 5
  • 56
  • 85
  • Thanks! This is indeed a valid alternative, with only minor modifications. One thing that bothers me a little with this approach though is that it pushes the changes upwards, all the way to `mylib`. Personally, I'd rather solve it by a "hack" in `code-coverage.cmake` (something like `set_property`, if it would just work as advertised). This way, the added complexity would be hidden from higher-level entities. If I can't find a solution like that any time soon, I'll mark this as the answer! – helmesjo Jun 29 '17 at 18:52
  • I went with this solution! It was the (IMO) most clean "work-around" which didn't complicate things too much. See implementation [here](https://github.com/helmesjo/hello-ci/blob/7bde2cd03dcc5b935f3ea01e8ed226aa21e6ad7c/src/mylib/CMakeLists.txt#L74). – helmesjo Jul 06 '17 at 13:00
0

Replace function by macro:

macro(setup_target_for_coverage)
  [...]
endmacro()

CMake macros are similar to C macros: you can think of it as a text replacement, so that it behaves as if the code was located in the original file.

oLen
  • 5,177
  • 1
  • 32
  • 48