Search code examples
c++cmakeeigen

How to prevent cmake to not add all targets from external dependencies using modern cmake constructs


I am trying to add external dependencies to my new cmake project in the cleanest and modern cmake way possible. I want to download these dependencies during configuration time. My problem is that all the targets from external dependencies are always imported. This spams my configurations in e.g. Clion which i want to be much cleaner by default but as you can see i have several useless targets from gtest, googlebenchmark, eigen and spdlog in the figure below. The project should compile on Windows and Linux.

enter image description here

So my question is how to prevent this?

I watched several modern cmake videos, e.g. https://www.youtube.com/watch?v=eC9-iRN2b04 and the sources from https://cliutils.gitlab.io/modern-cmake/.

My project folder structure looks as follows

.
├── CMakeLists.txt
├── Ikarus
|   └── CMakeLists.txt
|   └── addexternalLibs.cmake
│   └── src
│       └── Ikarus
│           └── [...]
├── problems
|   └── CMakeLists.txt
|   └── [.cpp files]
├── tests
|   └── CMakeLists.txt
|   └── [.cpp files]
├── benchmarks
|   └── CMakeLists.txt
|   └── [.cpp files]

I have a top level CMakeLists.txt as follows

cmake_minimum_required(VERSION 3.16)
project(Ikarus)

add_subdirectory(Ikarus)
add_subdirectory(problems)
add_subdirectory(tests)
add_subdirectory(benchmarks)

In the folder Ikarus i want to build my Ikarus_lib which should be used by the targets defined in problems, tests and benchmarks.

The file Ikarus/CMakeLists.txt looks as follows

file(GLOB_RECURSE IKARUS_CPP_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} CONFIGURE_DEPENDS  *.cpp )


add_library(${PROJECT_NAME}_lib STATIC)
target_sources(${PROJECT_NAME}_lib PRIVATE ${IKARUS_CPP_SOURCES})

include(addexternalLibs.cmake)

target_include_directories(${PROJECT_NAME}_lib PUBLIC src)

target_link_libraries(${PROJECT_NAME}_lib PUBLIC Eigen3::Eigen
                                          PUBLIC spdlog::spdlog)

Now i want to talk about the contents of the file Ikarus/addexternalLibs.cmake where the external depencies are imported. The difficulties similar arise in benchmark and tests where i import googletest and googlebenchmark but i think i will restrict myself to the Ikarus_lib case since the difficulties are the same.

Since i want to download my dependencies during configuration time i found the nice solution using https://cmake.org/cmake/help/latest/module/FetchContent.html

The result is Ikarus/addexternalLibs.cmake:

include(FetchContent)

message("Configure Eigen: ")

FetchContent_Declare(
        eigen
        GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
        GIT_TAG 3.4
        GIT_SHALLOW TRUE
        GIT_PROGRESS TRUE
)
option(EIGEN_BUILD_DOC OFF)
option(BUILD_TESTING OFF)
option(EIGEN_LEAVE_TEST_IN_ALL_TARGET OFF)
option(EIGEN_BUILD_PKGCONFIG OFF)
FetchContent_MakeAvailable(eigen)

message("====================================")
message("Configure spdlog: ")

FetchContent_Declare(
        spdlog
        GIT_REPOSITORY https://github.com/gabime/spdlog.git
        GIT_TAG v1.8.5
        GIT_SHALLOW TRUE
        GIT_PROGRESS TRUE
)
option(SPDLOG_BUILD_SHARED OFF)
option(SPDLOG_BUILD_ALL OFF)
FetchContent_MakeAvailable(spdlog)

This solution works and i managed to deactivate some targets using the options EIGEN_BUILD_DOC OFF.... But still i have lots of different useless targets as seen in the image above which cannot be deactivated using options of the package.

I tried several other things, e.g. using EXCLUDE_FROM_ALL https://cmake.org/cmake/help/latest/prop_tgt/EXCLUDE_FROM_ALL.html as follows

FetchContent_Declare(
        eigen
        GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
        GIT_TAG 3.4
        GIT_SHALLOW TRUE
        GIT_PROGRESS TRUE
)
option(EIGEN_BUILD_DOC OFF)
option(BUILD_TESTING OFF)
option(EIGEN_LEAVE_TEST_IN_ALL_TARGET OFF)
option(EIGEN_BUILD_PKGCONFIG OFF)

FetchContent_GetProperties(eigen)
if(NOT eigen_POPULATED)
   FetchContent_Populate(Eigen)
    add_subdirectory(${eigen_SOURCE_DIR} ${eigen_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()

which also works but does also adds all that targets which i don't want. Therefore, EXCLUDE_FROM_ALL does not work the way i think it works.

Another way of using EXCLUDE_FROM_ALL could be

include(FetchContent)

message("Configure Eigen: ")

FetchContent_Declare(
        eigen
        GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
        GIT_TAG 3.4
        GIT_SHALLOW TRUE
        GIT_PROGRESS TRUE
)
option(EIGEN_BUILD_DOC OFF)
option(BUILD_TESTING OFF)
option(EIGEN_LEAVE_TEST_IN_ALL_TARGET OFF)
option(EIGEN_BUILD_PKGCONFIG OFF)
FetchContent_MakeAvailable(eigen)
set_target_properties(eigen PROPERTIES EXCLUDE_FROM_ALL TRUE)

which also does not change the behaviour.

I also tried CPMAddPackage from https://github.com/cpm-cmake/CPM.cmake

CPMAddPackage(
        GITLAB_REPOSITORY libeigen/eigen
        GIT_TAG 3.4
        OPTIONS "EIGEN_BUILD_DOC NO"
                "BUILD_TESTING NO"
                "EIGEN_LEAVE_TEST_IN_ALL_TARGET YES"
                "EIGEN_BUILD_PKGCONFIG NO"
)

which also works but also adds all that targets.

So now i don't know what else to try and i think i will just keep the targets and delete them from my Clion configuration interface and hope they don't reappear. But still i want to understand what am i missing. I think this is a common problem and the solution should be very easy?

Furthermore, i know that i can just download e.g. eigen and use include_directories(...) to automatically add the headers without the targets. But on one hand this is not modern cmake as advertised in https://www.youtube.com/watch?v=eC9-iRN2b04 where using include_directories is called an anti-pattern and one should "think in modules". On the other hand this does not help for packages which are not header-only.

So how can this problem be solved elegantly? Additionally, i would appreciate every other comment to my (bad) usage of Cmake. Thanks!


Solution

  • I used to have similar issues.

    Change this directory structure

    ├── Ikarus
    |   ├── CMakeLists.txt
    |   ├── addexternalLibs.cmake
    │   └── src
    │       └── Ikarus
    │           └── [...]
    

    by adding an extern folder

    ├── Ikarus
    |   ├── CMakeLists.txt
    |   ├── addexternalLibs.cmake
    |   ├── extern/
    │   └── src
    │       └── Ikarus
    │           └── [...]
    

    You can now right click the extern folder in CLion and "Mark Directory as" -> "Excluded". I assume your build folder(s) is(are) already excluded too.

    Now in Ikarus/addexternalLibs.cmake you change the first to lines to

    include(FetchContent)
    set(FETCHCONTENT_BASE_DIR ${PROJECT_SOURCE_DIR}/extern)
    

    Then within each FetchContent_Declare(packagename... you add the option BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/extern/packagename. So for spdlog, for example:

    FetchContent_Declare(
            spdlog
            GIT_REPOSITORY https://github.com/gabime/spdlog.git
            GIT_TAG v1.8.5
            GIT_SHALLOW TRUE
            GIT_PROGRESS TRUE
            BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/extern/spdlog
    )
    

    This works easily with STATIC and INTERFACE libraries. For SHARED libraries in Windows, you might need to change the BINARY_DIR to a project-wide binary directory.

    Bonus:

    Even with the above, I was still having some issues with Eigen so I did something different: only download the Eigen subfolder using the option SOURCE_SUBDIR Eigen, i.e.:

    FetchContent_Declare(
        Eigensource
        GIT_REPOSITORY https://gitlab.com/libeigen/eigen
        GIT_TAG 3.4.0
        GIT_SHALLOW ON
        SOURCE_SUBDIR Eigen
        BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/extern/Eigen
        GIT_PROGRESS ON
        FIND_PACKAGE_ARGS
    )
    

    And then manually create the library interface with CMake:

    add_library(Eigen INTERFACE)
    add_library(Eigen::Eigen ALIAS Eigen)
    target_include_directories(Eigen INTERFACE ${eigensource_SOURCE_DIR})
    target_compile_definitions(Eigen INTERFACE EIGEN_FAST_MATH EIGEN_DEFAULT_TO_ROW_MAJOR
        $<$<CONFIG:Release>:EIGEN_NO_DEBUG>)