4

I'm wondering how I can compile dependencies for my project while enabling specific settings for these dependencies, for example compiling the dependency as a static or dynamic library or with x64 or x86 settings or as another example when the project defines variables to determine how to build the project (like with Wayland or X.Org support).

My current setup looks like this:

Folder Structure

root_project
  |─── CMakeLists.txt
  |─── Project 1
  |      |─── .h and .cpp files
  |      └─── CMakeLists.txt
  |─── Dependency 1 (GLFW)
  |      |─── include directory
  |      |─── source directory
  |      |─── ...
  |      └─── CMakeLists.txt
  └─── Dependency 2 (GLEW)
         |─── build
         |      └─── cmake
         |            └─── CMakeLists.txt
         |─── source directory
         |─── include directory
         └─── ...

CMake files

My root cmake file:

cmake_minimum_required (VERSION 3.8)
project ("EbsiStaller")
add_subdirectory ("EbsiStaller")

# Adds the CMakeLists.txt file located in the specified directory
# as a build dependency.
add_subdirectory ("glfw")
include_directories("glfw/include")

add_subdirectory ("glew/build/cmake")
include_directories("glew/include")

My project cmake file:

cmake_minimum_required (VERSION 3.8)

add_executable (EbsiStaller 
    "....cpp" 
    "....h"
)

SET(CMAKE_CXX_STANDARD 17)
SET(CMAKE_CXX_STANDARD_REQUIRED  ON)

# Links the CMake build output against glfw.
target_link_libraries(EbsiStaller glfw ${GLFW_LIBRARIES} glew ${GLEW_LIBRARIES})

Additional Notes:

I'm using Visual Studio 2017 for this project under Windows while the project should be platform independent. As I don't have much experience with CMake I'm always open to any suggested changes to my CMake files.

When defining compile-specific settings for my dependencies, I don't want to edit their CMake files to do so.

Stephen Newell
  • 7,330
  • 1
  • 24
  • 28
ShadowDragon
  • 2,238
  • 5
  • 20
  • 32

2 Answers2

2

There's a lot of difficulties doing this in CMake, but I'm going to answer it to the best of my abilities.

Normally, any project you add via add_subdirectory will inherit all settings currently defined in the current scope. The simplest way (IMO) to change settings for a single dependency is to use ExternalProject_Add with the following macros:

Macros

include(ExternalProject)

#
#   Add external project.
#
#   \param name             Name of external project
#   \param path             Path to source directory
#   \param external         Name of the external target
#
macro(add_external_project name path)
    # Create external project
    set(${name}_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${path})
    set(${name}_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${path})
    ExternalProject_Add(${name}
        SOURCE_DIR "${${name}_SOURCE_DIR}"
        BINARY_DIR "${${name}_BINARY_DIR}"
        CMAKE_ARGS "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}"
                   "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"
                   "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
                   "-DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}"
                   # These are only useful if you're cross-compiling.
                   # They, however, will not hurt regardless.
                   "-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}"
                   "-DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR}"
                   "-DCMAKE_AR=${CMAKE_AR}"
                   "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}"
                   "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}"
                   "-DCMAKE_RC_COMPILER=${CMAKE_RC_COMPILER}"
                   "-DCMAKE_COMPILER_PREFIX=${CMAKE_COMPILER_PREFIX}"
                   "-DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH}"
       INSTALL_COMMAND ""
    )

endmacro(add_external_project)

#
#   Add external target to external project.
#
#   \param name             Name of external project
#   \param includedir       Path to include directory
#   \param libdir           Path to library directory
#   \param build_type       Build type {STATIC, SHARED}
#   \param external         Name of the external target
#
macro(add_external_target name includedir libdir build_type external)
    # Configurations
    set(${name}_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${libdir})

    # Create external library
    add_library(${name} ${build_type} IMPORTED)
    set(${name}_LIBRARY "${${name}_BINARY_DIR}/${CMAKE_CFG_INTDIR}/${CMAKE_${build_type}_LIBRARY_PREFIX}${name}${CMAKE_${build_type}_LIBRARY_SUFFIX}")

    # Find paths and set dependencies
    add_dependencies(${name} ${external})
    set(${name}_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/${includedir}")

    # Set interface properties
    set_target_properties(${name} PROPERTIES IMPORTED_LOCATION ${${name}_LIBRARY})
    set_target_properties(${name} PROPERTIES INCLUDE_DIRECTORIES ${${name}_INCLUDE_DIR})
endmacro(add_external_target)

Macro Explanation

The macros basically configure a new instance of CMake with very similar CMake variable definitions.

The first macro, ExternalProject_Add, notifies CMake about an external project it needs to build once with those custom CMake arguments, source directory, and output binary directory. In particular, options like "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}" tell CMake to use the same build type (Debug, Release, etc.) as the current build type, while "-DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}" instructs CMake to use the same preference when building shared libraries (by default, if BUILD_SHARED_LIBS is set to OFF, the project should build static dependencies).

The second macro then creates an imported target CMake may link against with properties similar to a native CMake library.

Using these macros

To use these macros by default, you may do:

add_external_project(googletest_external googletest)
add_external_target(gtest googletest/googletest/include googletest/googlemock/gtest STATIC googletest_external)
add_external_target(gtest_main googletest/googletest/include googletest/googlemock/gtest STATIC googletest_external)

In this example, I configure the external project googletest, and then create the targets gtest and gtest_main which should be static libraries (due to how Googletest forces static linkage), which may be linked against like any normal CMake library.

Hijacking these macros for custom builds

Now that you have a cursory understanding of what these macros do, modifying them to allow custom configurations of each dependency is very easy. Say, for example, I would like a static release build of glew, regardless of my actual project settings. Let's also say hypothetically I want GLEW_OSMESA to be set to ON.

#
#   Add external project.
#
macro(add_release_osmesa_glew)
    # Create external project
    set(${name}_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/glew)
    set(${name}_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/glew)
    ExternalProject_Add(glew_external
        SOURCE_DIR "${${name}_SOURCE_DIR}"
        BINARY_DIR "${${name}_BINARY_DIR}"
        CMAKE_ARGS "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}"
                   "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"
                   "-DCMAKE_BUILD_TYPE=Release"
                   "-DBUILD_SHARED_LIBS=${BUILD_SHARED_LIBS}"
                   # These are only useful if you're cross-compiling.
                   # They, however, will not hurt regardless.
                   "-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}"
                   "-DCMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR}"
                   "-DCMAKE_AR=${CMAKE_AR}"
                   "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}"
                   "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}"
                   "-DCMAKE_RC_COMPILER=${CMAKE_RC_COMPILER}"
                   "-DCMAKE_COMPILER_PREFIX=${CMAKE_COMPILER_PREFIX}"
                   "-DCMAKE_FIND_ROOT_PATH=${CMAKE_FIND_ROOT_PATH}"
                   "-DGLEW_OSMESA=ON"
       INSTALL_COMMAND ""
    )

Then, to use the glew built with these configuration options, I can do the following:

add_release_osmesa_glew()
add_external_target(
    glew
    glew/include 
    glew 
    SHARED 
    glew_external
)

add_external_target(
    glew_s 
    glew/include 
    glew 
    STATIC 
    glew_external
)

And finally, I may link against it with the following options:

target_link_libraries(my_target
    glew_s
    ...
)

Pros

  • Requires no changes to the project's CMakeLists.
  • Supports all possible configurations the dependent project supports.
  • Builds the dependent library only once, and can use inherited settings or custom settings as need be.
  • Should be target independent (meaning it should work with Visual C++ projects, Makefiles, etc.) out-of-the-box.

Cons

  • A large amount of boilerplate
  • Configuration dependent on the CMakeLists in the dependent project
Alex Huszagh
  • 13,272
  • 3
  • 39
  • 67
  • Could you please outline what's the difference between your answer and @Stephen Newell answer as his answer seems to be a simpler solution. – ShadowDragon Mar 26 '18 at 19:06
  • @ShadowDragon The two are very different. The latter let's you change compile definitions, linkage flags, and other settings, but everything is 1). Compiler dependent, 2). Easily toggle settings like static/shared builds, or release/debug builds, 3). Culminates in duplicating a lot of the logic that is already in the CMakeLists of the project you wish to build. My solution essentially uses a fair amount of boiler-plate up-front so you can use the predefined configurations as if you were running `cmake /path/to/source -D...`, only internally. – Alex Huszagh Mar 26 '18 at 21:14
  • 1
    @ShadowDragon Say, for example, you need to change between 32-bit and 64-it dependencies on Linux only, his solution is almost certainly a lot cleaner. If you need a lot of custom logic set from defining `OSMESA` in `glfw`, but not `glew`, my solution is likely to be cleaner (an edge-case, but relevant). The latter reason I prefer this, which is mostly tangential, is to avoid building dependencies multiple times. Most projects I used to manage had onerous compile times due to numerous dependencies which would trigger re-builds every change I made, this causes a single build. – Alex Huszagh Mar 26 '18 at 21:18
1

The right way to do this is operating on targets directly. For example (guessing with target names, so forgive me):

add_subdirectory ("glfw")
set_target_properties(glfw PROPERTIES
    COMPILE_FLAGS "-m32 -O2" # Adjust as needed
)
target_link_libraries(glew INTERFACE
    ${GLFW_LIBRARIES}
)

add_subdirectory ("glew/build/cmake")
target_include_directories(glew PUBLIC
    "glfw/include"
)
target_link_libraries(glew INTERFACE
    ${GLEW_LIBRARIES}
)

This lets you tweak things on a per-target basis instead of globally (this is the basis of modern CMake usage). You can tweak pretty much anything you like about a target using these functions and their friends, including adjusting compiler flags and even adding new files.

The method you're using works, but you're affecting every target that's declared afterwards, including in sub-directories that are added later.

Your main project's CMakeLists.txt could look something like this:

 add_executable (EbsiStaller 
    "....cpp" 
    "....h"
)
target_compile_features(EbsiStaller PUBLIC
    cxx_std_17 # might actually be a cmake 3.9 thing, but you get the idea
)

# Links the CMake build output against glfw.
target_link_libraries(EbsiStaller
    glfw
    glew
)

There's way too much to cover here, but it all comes down to modernizing your CMake. The online docs are fantastic.

Stephen Newell
  • 7,330
  • 1
  • 24
  • 28
  • I'm wondering why are you using `target_include_directories` for glew and then defining the glfw include directionary within this statement and also why you only use `target_include_directories` for glew and not for glfw? Well, the online docs would even be more fantastic with a tutorial newer than cmake 2.6. – ShadowDragon Mar 26 '18 at 19:18
  • @ShadowDragon - Unless I made a mistake, this should match the example you provided. `include_directories` applies to future targets, so that command only affects the glew subdirectory in your example. – Stephen Newell Mar 26 '18 at 19:23
  • Using the code you provided would result into this error within the root cmake file: `CMake Error at CMakeLists.txt:23 (target_link_libraries): Cannot specify link libraries for target "glew" which is not built by this project.` I also meant why you use `target_include_directories` for glew with the glfw include libraries but not for glew with the glew include folder. I would automatically think it has to be this way: `target_include_directories` for `glfw` with `glfw/include` and `target_include_directories`for `glew` with `glew/include`. – ShadowDragon Mar 26 '18 at 19:36
  • 1
    I'm assuming glew automatically handles its own includes, but consumes glfw headers from an external source (this is what your example implied, at least based on cmake projects I've dealt with in the past). If that's not the case, I'll need more information about how the subfolders relate to each other before I can provide help. – Stephen Newell Mar 26 '18 at 20:40
  • I don't know wether glew handles it includes itself or/and consumes glfw headers. I only know that I'm using GLFW for window creation and glew as one library out of many possible choices to provide me the newer OpenGL functions (anything newer than OpenGL 1.x which ships with Windows). The project itself should compile both glew and glfw and afterwards link it to my own project which requires those libraries. Wether glew and/or glfw should be a dll or a static library: I haven't thought about this yet. – ShadowDragon Mar 26 '18 at 20:47
  • This is partially why I like my own opaque solution, although it's not ideal for CMake. For simple tasks, this is by far the best solution. For opaque tasks, typically messing with CMake's toolchains and build commands tends to work for the best (from experience) :/ I wish it were all like this, and you could mark targets to be built only once. +1 – Alex Huszagh Mar 26 '18 at 21:30