Using Installed Packages
Learn how to consume a CMake package you've installed, completing the producer-consumer cycle. We'll cover using find_package()
and how to point CMake to your library's location
In the previous lesson, we did the hard work of turning our GreeterLib
into a distributable CMake package. We created an "install-tree" containing our compiled library, public headers, and the special ...Config.cmake
files that power the find_package()
command.
In this lesson, we'll switch hats and become a consumer of our own library. We will modify our GreeterApp
to find and use the installed version of GreeterLib
, just as if it were any other third-party dependency like Boost or spdlog.
This will not only verify that our packaging work was successful but also teach you the final, crucial step in the dependency management puzzle: how to tell CMake where to look for the packages you've installed.
Using the GreeterLib
Package
We should test that anyone interested in our new, sharable library can use it how we expect. Let's update GreeterApp
to consume our library in this form, as if it were a third-party dependency.
Using find_package()
First, in our root CMakeLists.txt
, we'll CMake to try to find an installed GreeterLib
package before reverting to the build-tree dependency using add_subdirectory()
:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
find_package(GreeterLib)
if (GreeterLib_FOUND)
message(STATUS "Using Installed GreeterLib")
else()
add_subdirectory(greeter)
endif()
add_subdirectory(app)
This approach avoids our testing environment getting stuck in a situation where we can't build our project because we haven't installed our package, and we can't install our package because our project can't be built.
We can tell which version of our library is being used by looking for the the warning message generated by find_package()
when it fails, or the STATUS
message we added when GreeterLib_FOUND
is true.
Updating the Link
Over in app/CMakeLists.txt
, we'll need to update our target_link_libraries()
command to use the new Greeter::Lib
name:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
target_link_libraries(GreeterApp Greeter::Lib)
Running the Configure Step
The final problem is that CMake doesn't know where our package is. If we try to configure the project, our find_package()
command will fail with a warning before falling back to add_subdirectory()
:
cmake ..
CMake Warning at CMakeLists.txt:12 (find_package):
By not providing "FindGreeterLib.cmake" in CMAKE_MODULE_PATH this project
has asked CMake to find a package configuration file provided by
"GreeterLib", but CMake did not find one.
Helping find_package()
Find Things
We need to tell find_package()
where to search, and the error message gives us some hints.
CMake stores the locations where it will search for packages as a list called CMAKE_PREFIX_PATH
. We can append our /install
directory to that list:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
list(APPEND CMAKE_PREFIX_PATH "${PROJECT_SOURCE_DIR}/install")
find_package(GreeterLib)
# ...
Alternatively, we can provide the specific location of GreeterLib
's installed configuration file (GreeterLibConfig.cmake
) by setting the GreeterLib_DIR
variable:
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter VERSION "1.0")
set(GreeterLib_DIR
"${PROJECT_SOURCE_DIR}/install/lib/cmake/GreeterLib"
)
find_package(GreeterLib)
# ...
If we don't want to update our CMakeLists.txt
file, we can set cached values using either approach when configuring on the command line:
cmake -DCMAKE_PREFIX_PATH="./install" ..
cmake -DGreeterLib_DIR="./install/lib/cmake/GreeterLib" ..
Implementing any of these techniques should be enough to let our find_package()
command finally succeed:
cmake ..
-- Using Installed GreeterLib
Success! CMake now finds our GreeterLibConfig.cmake
, loads the imported Greeter::Lib
target, and successfully configures the project. We can now build and run GreeterApp
, and prove that our GreeterLib
can now also be used as a standalone, sharable package:
cmake --build .
./app/GreeterApp
Hello from the modular greeter library!
Installing Arbitrary Files
A complete package often includes more than just compiled binaries and public headers. You may want to distribute important supplementary materials like:
- A
LICENSE
file - A
README
file with usage instructions - Assets such as images and fonts that the program needs to run
The install()
command has subcommands specifically for handling these arbitrary files and directories.
Installing Individual Files
The install(FILES ...)
command is the most direct way to install one or more specific files. Let's say we add a LICENSE
file to our project:
LICENSE
Permission is hereby granted to...
We can add an install()
rule to our CMakeLists.txt
to place this file in a share/doc/[ProjectName]
directory within our install-tree. This is a common convention on Unix-like systems.
install(FILES "${PROJECT_SOURCE_DIR}/LICENSE"
DESTINATION "${CMAKE_INSTALL_DATADIR}/doc/${PROJECT_NAME}"
)
Here, ${CMAKE_INSTALL_DATADIR}
is another helpful variable from the GNUInstallDirs
module, which typically resolves to share
.
Installing Entire Directories
If you need to install a whole folder of content, such as generated documentation or a set of example projects, install(DIRECTORY ...)
is the right tool. We've already used it to install our headers:
install(DIRECTORY "${PROJECT_SOURCE_DIR}/include"
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
This command recursively copies the entire include
directory to the destination.
You can also be more selective. Imagine you have an assets
directory that contains both source files (.png
) and intermediate files (.psd
) that you don't want to distribute. You can use PATTERN
to filter what gets copied.
# Only install the .png files from the assets directory
install(DIRECTORY "${PROJECT_SOURCE_DIR}/assets/"
DESTINATION "${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/assets"
FILES_MATCHING
PATTERN "*.png"
)
The trailing slash on "assets/"
is important. It tells CMake to copy the contents of the directory, not the directory itself. Without it, you would get an extra assets
folder at the destination (e.g., .../assets/assets/
).
Summary
In this lesson, we completed the producer-consumer cycle. We learned how to use the package we created in the previous lesson, treating it as a true third-party dependency.
- Finding Packages: We used
find_package()
to locate our installed library. - Find-or-Build Pattern: A robust approach is to first try
find_package()
and fall back toadd_subdirectory()
if the installed package isn't found. - Helping CMake Find Packages: We learned the two primary ways to tell
find_package()
where to look: setting the genericCMAKE_PREFIX_PATH
or the specificPackageName_DIR
variable, either in theCMakeLists.txt
or on the command line. - Installing Arbitrary Files: We saw how to use
install(FILES ...)
andinstall(DIRECTORY ...)
to include non-target files like documentation and assets in our final installation.
Fetching External Dependencies
Learn how to create self-contained, reproducible builds by fetching dependencies from source using FetchContent()
and ExternalProject_Add()
.