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