Simplify installation of (modern) CMakeConfig files with targets
I recently suggested to an OSS project to use CMake and install CMakeConfig files to support downstream projects using it with find_package
. This is usually met with hesitation as CMake alone is quite hard and the documentation often lacking good examples for common use cases.
I started to create a CMake file and do the installation "correctly" by harvesting online sources but quickly failed due to misleading, outdated and partially wrong information and even after I got all pieces it still feels wrong due to the amount of boilerplate and repetition of the same information.
When I opened this issue I remembered the CMake wiki from the homepage and tried to find help there. I expected this (supposedly extremely common) information under "look here first" where I found "Structure of a CMake Build System" but there it wasn't included. But there was a link to https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#creating-packages which is kinda helpful but hard to find. Similar information can be found on How to package your project for use by others but that page is confusing and when you finally understand you need a *Config.cmake
you get an empty page at https://gitlab.kitware.com/cmake/community/wikis/doc/doc/tutorials/How-to-create-a-ProjectConfig.cmake-file
So on the documentation part I would like a Copy&Paste ready, take-me-by-the-hand version on how I'm supposed to install common projects. There should be ready-to-use examples for:
- Binary only
- Shared library
- Static library
- header only library
- multi-configuration installs (Debug, Release) (even foonathan, who is usually great, got this wrong)
- multi-version installs (optional)
Once this is done, those codes should be simplified by providing something with newer CMake and as single-file downloads for older CMake.
My example for a header-only library called turtle
is below. It assumes a standard project layout of top-level folders src, include, tests, examples
, in this case only include and tests.
CMakeLists.txt
cmake_minimum_required(VERSION 3.1)
project(turtle VERSION 1.3.2)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(IS_ROOT_PROJECT ON)
else()
set(IS_ROOT_PROJECT OFF)
endif()
option(TURTLE_TESTS "Enable to generate test targets" ${IS_ROOT_PROJECT})
option(TURTLE_INSTALL "Enable to add install target" ${IS_ROOT_PROJECT})
add_library(turtle INTERFACE)
add_library(turtle::turtle ALIAS turtle)
target_include_directories(turtle INTERFACE $<BUILD_INTERFACE:include> $<INSTALL_INTERFACE:include>)
if(TURTLE_TESTS)
enable_testing()
add_subdirectory(test)
endif()
if(TURTLE_INSTALL)
install(TARGETS turtle EXPORT TurtleTargets)
install(EXPORT TurtleTargets
NAMESPACE turtle::
DESTINATION lib/cmake/turtle
)
install(DIRECTORY include/ DESTINATION include)
include(CMakePackageConfigHelpers)
configure_package_config_file(TurtleConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/TurtleConfig.cmake
INSTALL_DESTINATION lib/cmake/turtle
)
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/TurtleConfigVersion.cmake
COMPATIBILITY SameMajorVersion
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/TurtleConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/TurtleConfigVersion.cmake
DESTINATION lib/cmake/turtle
)
endif()
TurtleConfig.cmake
include(${CMAKE_CURRENT_LIST_DIR}/TurtleTargets.cmake)
This supports both common use cases:
- Usage after installation via
find_package(turtle)
- Usage after adding with
add_subdirectory(...)
(e.g. with git submodules)
What I dislike is the amount of boilerplate and repetition:
-
install(TARGETS turtle EXPORT TurtleTargets)
: Kinda useless here, but ok -
install(EXPORT...
: TheNAMESPACE
is duplicated with the alias-library,DESTINATION
is strange (foundlib/turtle/cmake
,lib/cmake/turtle
, wondering iflib/cmake
isn't enough) -
configure_package_config_file
: TheINSTALL_DESTINATION
again. Reading the code I (again) don't understand why -
install(FILES
: Again reference to file from the 2 commands above, and again theDESTINATION
(for a total of 3 times)
I could think of the following command:
install(PACKAGE
TARGETS target1 [target2 ...] # Or reference to EXPORT target, but that concept is hard to grasp
NAMESPACE <...> # Also creates alias targets in current scope as with calling add_library
CONFIG <TargetsConfig.cmake.in> # If not given the plain one from above including *Targets.cmake is created
PATH_VARS <...> # Optional
VERSION <...> # Defaults to PROJECT_VERSION, I even would not use this
COMPATIBILITY <...>
ARCH_INDEPENDENT
DESTINATION <...> # May contain generator expressions for multi-config builds?, maybe even provide a default?
)
This would reduce the above code to:
if(TURTLE_INSTALL)
install(PACKAGE
TARGETS turtle
NAMESPACE turtle::
COMPATIBILITY SameMajorVersion
DESTINATION lib/cmake/turtle
)
endif()