Installing and packaging
Making a library consumable
We have built a library,
cmake_course_lib, and used it within our own project.
The final step in modern CMake is to install it so that a completely separate
project can find and link it with find_package, exactly as we did for Eigen and
Boost.Namespaced targets
First, give the library a namespaced alias:
add_library(CMakeCourse::cmake_course_lib ALIAS cmake_course_lib)
Consumers link against
CMakeCourse::cmake_course_lib. The :: form is more than
cosmetic: CMake treats a name containing :: as a target, so if you mistype it
you get an error at configure time rather than a confusing linker error later. It
also means consumers use the same name whether they get the library from
add_subdirectory or from an installed package.Include directories that survive installation
Until now our library's include directory pointed at its source tree:
target_include_directories(cmake_course_lib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
That path is correct while building in place, but it is meaningless once the
library is installed somewhere else. Generator expressions let us give the right
answer for each case:
target_include_directories(cmake_course_lib PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}> $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
$<BUILD_INTERFACE:...> applies when building the project directly;
$<INSTALL_INTERFACE:...> applies to consumers of the installed package.Declaring the public headers
To install the headers, tell CMake which ones are public using a file set
(CMake 3.23 and later):
target_sources(cmake_course_lib PUBLIC FILE_SET HEADERS BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} FILES functionality.hpp functionality_eigen.hpp)
Installing
install() describes what ends up in the install tree. GNUInstallDirs gives us
the conventional destinations (lib, include, bin, ...) for the platform:include(GNUInstallDirs) install(TARGETS cmake_course_lib EXPORT CMakeCourseTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} FILE_SET HEADERS DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
The
EXPORT keyword collects the target into an export set we can turn into a
CMake package.The first time you do this,
install(EXPORT ...) is likely to fail with an error
about a target that "is not in any export set", naming project_warnings.Even though we linked it
PRIVATE, a static library cannot carry its
dependencies inside itself, so CMake records them in the exported target's link
interface for consumers to link too. project_warnings is a local, un-exported
target, so the export cannot be written.The warnings are only needed while building this project, so scope them to the
build:
target_link_libraries(cmake_course_lib PRIVATE $<BUILD_INTERFACE:project_warnings>)
Real dependencies such as Eigen are different: consumers genuinely do need them,
which is why the package config re-finds Eigen below.
Generating a package so find_package works
Consumers use
find_package(CMakeCourse), which looks for a
CMakeCourseConfig.cmake file. We generate one, plus a matching targets file and
a version file.include(CMakePackageConfigHelpers) install(EXPORT CMakeCourseTargets FILE CMakeCourseTargets.cmake NAMESPACE CMakeCourse:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/CMakeCourse) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/CMakeCourseConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/CMakeCourseConfig.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/CMakeCourse) write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/CMakeCourseConfigVersion.cmake VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/CMakeCourseConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/CMakeCourseConfigVersion.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/CMakeCourse)
The
CMakeCourseConfig.cmake.in template is short: it pulls in the exported
targets, and re-finds the dependencies consumers need so they do not have to.@PACKAGE_INIT@ include(CMakeFindDependencyMacro) find_dependency(Eigen3 CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/CMakeCourseTargets.cmake")
find_dependency is a thin wrapper around find_package that forwards the
REQUIRED and QUIET arguments from the outer find_package(CMakeCourse ...)
call, and reports a missing dependency as a failure to find our package.Why re-find Eigen when we linked it PRIVATE?
Because
cmake_course_lib is static. A shared library resolves its private
dependencies into itself at link time, and consumers never hear about them. A
static library is only an archive of object files, so whoever links it must
supply everything it needs.CMake therefore keeps a static library's
PRIVATE dependencies in the exported
link interface, wrapped in $<LINK_ONLY:...>, which means "link this, but take
none of its usage requirements". You can see it in the generated
CMakeCourseTargets.cmake:set_target_properties(CMakeCourse::cmake_course_lib PROPERTIES INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:Eigen3::Eigen>")
That line names a target, and CMake requires every target in an imported
target's link interface to exist. Without
find_dependency(Eigen3 CONFIG),
nothing defines Eigen3::Eigen in the consumer's project, and configuring it
fails with an error about linking to a target that was not found.This is the same rule that made
install(EXPORT ...) reject project_warnings
above. The difference is that Eigen comes from a package the consumer can find
again, whereas project_warnings is a local target belonging to no package,
which is why one is re-found and the other is confined to the build interface.The export machinery would barely change.
install(TARGETS ...) already lists
ARCHIVE, LIBRARY and RUNTIME destinations precisely so it handles either
kind of library, and the install(EXPORT ...) and package-config steps are the
same.A couple of things above would simply stop mattering: a shared library keeps its
private dependencies to itself, so
Eigen3::Eigen would not reach the exported
interface and project_warnings would not upset install(EXPORT ...). Both
lines are still worth keeping, in case the library type ever changes.The extra work with a shared library is in building it rather than exporting
it: controlling which symbols are visible (usually with
generate_export_header,
whose generated header then needs installing too), setting VERSION and
SOVERSION, and making sure the consumer can find the library at run time as
well as at link time.Now anyone can install the project and consume it from an unrelated project. To
install, build as usual and then run
cmake --install, choosing a prefix:cmake --build build_dir cmake --install build_dir --prefix ./stage
A prefix such as
./stage inside the project keeps the experiment self-contained,
and avoids needing root to write to /usr/local. Have a look at what appears
there: the static library under lib/, the headers under include/, and the
package files under lib/cmake/CMakeCourse/.Consuming the installed package
A separate project now needs no knowledge of where our source tree lives. It
finds the package and links the namespaced target:
find_package(CMakeCourse CONFIG REQUIRED) add_executable(consumer main.cpp) target_link_libraries(consumer PRIVATE CMakeCourse::cmake_course_lib)
The one thing to tell CMake is where to look, which is what
CMAKE_PREFIX_PATH
is for:cmake -S . -B build_dir -DCMAKE_PREFIX_PATH=/absolute/path/to/checkpoint_7/stage cmake --build build_dir
Everything else comes from the package: the include directory, the library to
link, and (through
find_dependency) Eigen. This is the same mechanism we used
back in Finding dependencies to consume Eigen and Boost, seen from the other
side.Packaging with CPack
CMake can also produce distributable archives and installers through CPack.
Set a few variables and include the module:
set(CPACK_PACKAGE_NAME "ModernCMakeCourse") set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) include(CPack)
After building, run
cpack from the build directory to produce a package:cpack --config build_dir/CPackConfig.cmake -G TGZ
CPack can generate
.tar.gz, .zip, .deb, .rpm, NSIS installers and more,
from the same install rules you already wrote.Install the library
Work in Checkpoint 6, the project you finished in the previous section. You
are going to turn
cmake_course_lib into something another project can install
and use. All of the changes are in src/CMakeLists.txt unless stated otherwise.- Give the library a namespaced alias, so consumers can use the same name whether they build this project or find an installed copy:
add_library(CMakeCourse::cmake_course_lib ALIAS cmake_course_lib) - Fix the include directories. Replace the existing
target_include_directories(... INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})with aPUBLICversion that gives one answer while building and another once installed, using$<BUILD_INTERFACE:...>and$<INSTALL_INTERFACE:...>. You will needinclude(GNUInstallDirs)for${CMAKE_INSTALL_INCLUDEDIR}. - Declare the public headers with a header file set, so CMake knows which headers to install and where they sit relative to the include root (
target_sources(... PUBLIC FILE_SET HEADERS BASE_DIRS ... FILES ...)). - Keep the warnings out of the exported interface. Change the
project_warningslink totarget_link_libraries(cmake_course_lib PRIVATE $<BUILD_INTERFACE:project_warnings>). If you skip this step, step 6 will fail: try it and read the error, it is one worth recognising. - Add the install rules:
install(TARGETS cmake_course_lib EXPORT CMakeCourseTargets ...)withARCHIVE,LIBRARY,RUNTIMEandFILE_SET HEADERSdestinations taken fromGNUInstallDirs. - Generate the package files so that
find_package(CMakeCourse)will work:install(EXPORT CMakeCourseTargets ... NAMESPACE CMakeCourse:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/CMakeCourse)- a
CMakeCourseConfig.cmake.intemplate next tosrc/CMakeLists.txt, holding@PACKAGE_INIT@, afind_dependency(Eigen3 CONFIG), and aninclude()of the targets file configure_package_config_fileandwrite_basic_package_version_filefromCMakePackageConfigHelpers, plus aninstall(FILES ...)to put both generated files alongside the targets file
- Configure, build, and install into a staging prefix inside the project, so you do not need root and can throw it away afterwards:
cmake -S . -B build_dir cmake --build build_dir cmake --install build_dir --prefix ./stage - Look at what you produced. Explore
stage/, and confirm you have:- the static library in
stage/lib/ functionality.hppandfunctionality_eigen.hppinstage/include/CMakeCourseConfig.cmake,CMakeCourseConfigVersion.cmakeandCMakeCourseTargets.cmakeinstage/lib/cmake/CMakeCourse/
OpenCMakeCourseTargets.cmakeand find theadd_library(CMakeCourse::cmake_course_lib STATIC IMPORTED)line, and the properties recording the include directory and the Eigen dependency. This file is the whole trick: it is generated from the target you described, so an installed copy behaves just like the one in your build tree.
Keep the
stage/ directory: the next challenge consumes it.Consume the installed library
Now play the part of somebody else: a developer who has our library installed,
has never seen its source tree, and just wants to use it. Start from the
stage/ prefix you installed in the previous challenge (or install Checkpoint 7
to create one).- Make a new directory somewhere outside the checkpoint you have been working in, containing just two files:
CMakeLists.txtandmain.cpp. It is a project of its own, with its owncmake_minimum_requiredandproject()calls, and it must notadd_subdirectoryanything from Checkpoint 7. - Write
main.cppso that it includesfunctionality.hppby name, callshypergeometricPmf(10'000, 4'270, 300, 128), and prints the result. - Find the package in
CMakeLists.txt:find_package(CMakeCourse CONFIG REQUIRED)CONFIGinsists on the config file we generated, rather than aFindCMakeCourse.cmakemodule, which does not exist. - Add the executable and link the namespaced target, and nothing else:
add_executable(consumer main.cpp) target_link_libraries(consumer PRIVATE CMakeCourse::cmake_course_lib)Resist the urge to add an include directory or to mention Eigen. If the install rules were right, they are unnecessary. - Configure, telling CMake where to look.
find_packagesearches a list of prefixes, and yours is not one CMake would guess, so pass it explicitly as an absolute path:cmake -S . -B build_dir -DCMAKE_PREFIX_PATH=/absolute/path/to/checkpoint_7/stageCMAKE_PREFIX_PATHtakes the prefix, not the directory containing the config file: CMake knows to look underlib/cmake/CMakeCourse/itself. - Build and run it:
cmake --build build_dir ./build_dir/consumer - Convince yourself it is really using the installed copy. Try deleting (or renaming) the
stage/prefix and configuring again from scratch: thefind_packagecall should now fail. That failure is the point of the whole section, in that your project depends on the package, not on where the source happened to live.
That's all, folks
This was only a brief look at modern CMake. There are great resources available
for a deeper dive, and here are just a few of them.
The book to buy, if you buy one:
- Professional CMake: A Practical Guide, by Craig Scott, published by Crascit. Written by a CMake co-maintainer, and revised for each CMake release, so it stays current in a way that most CMake writing does not. It covers everything in this course and a great deal more.
Official documentation:
- The CMake reference documentation, which is where you will end up whenever you need the exact behaviour of a command.
- The official CMake tutorial, a step-by-step guide maintained by Kitware alongside CMake itself.
- CMake Discourse, the community forum, where CMake's own developers answer questions.
Going further:
- An Introduction to Modern CMake, a free and regularly updated online book covering much the same target-based approach we have taken here.
- Deep CMake for Library Authors (CppCon 2019), a talk by Craig Scott which goes considerably deeper into the install and export machinery of the previous section. Some of the version numbers have moved on, but the reasoning has not.
- cmake-init, a project generator that writes out a complete, well-structured CMake project. Worth reading even if you never run it, as a worked example of how the pieces fit together.
CMake has been around a long time, and much of what you will find online is
written in the pre-target style: global variables,
include_directories,
link_libraries, and manual compiler flags. A great deal of it still works, which
is exactly what makes it hard to spot.If an example sets variables where it could set target properties, it is probably
out of date. Check the date of anything you are copying from, and prefer sources
that keep up to date with CMake releases.