A consumer of a CMake library wants to write two lines:
find_package(SimpleMath CONFIG REQUIRED)
target_link_libraries(consumer_app PRIVATE SimpleMath::SimpleMath)
Then #include <simplemath/simplemath.hpp>, build, and have the right headers, the right libraries, and every transitive dependency picked up automatically — with no manual include paths and no manual -L flags. Two lines, one header, done.
This post is everything the producer of SimpleMath has to ship so those two lines work — and why each piece exists. The library itself does almost nothing; its add and divide functions are deliberately trivial because the packaging is the point. Each section that follows answers one question of the form what would break without this?
1. The consumer’s two lines
Here is the entire consumer-side project. CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(Consumer LANGUAGES CXX)
find_package(SimpleMath CONFIG REQUIRED)
add_executable(consumer_app main.cpp)
target_link_libraries(consumer_app PRIVATE SimpleMath::SimpleMath)
main.cpp:
#include <simplemath/simplemath.hpp>
#include <iostream>
int main() {
std::cout << simplemath::add(2.0, 3.0) << "\n";
std::cout << simplemath::divide(10.0, 2.0) << "\n";
return 0;
}
That is the target. Everything from §2 onwards is the producer’s responsibility — what SimpleMath’s build has to do, and what files it has to install, so the consumer never has to know where SimpleMath lives, what its include directories are, or that it depends on spdlog.
2. What find_package(... CONFIG ...) is looking for
When CMake sees find_package(SimpleMath CONFIG REQUIRED), it walks a small set of well-known directories — entries in CMAKE_PREFIX_PATH, the system prefix, and a handful of platform-specific spots — looking for a file named SimpleMathConfig.cmake or simplemath-config.cmake, conventionally under a lib/cmake/SimpleMath/ subdirectory of an install prefix. The first match wins. CMake executes that file, and whatever it does — define an imported target, set some variables, pull in dependencies — is now part of the consumer’s project.
If nothing matches, the call fails with the canonical CMake error:
CMake Error: Could not find a package configuration file provided by "SimpleMath"
with any of the following names:
SimpleMathConfig.cmake
simplemath-config.cmake
This error means exactly one of two things. Either SimpleMath is not installed, or it is installed somewhere CMake is not searching. The fix for the second case is to point CMake at the install prefix:
cmake -S . -B build -DCMAKE_PREFIX_PATH="/path/to/SimpleMath/install"
The producer’s job, then, is to ship a SimpleMathConfig.cmake at a discoverable path. The next section is about what that file has to do once CMake finds it.
3. What the Config file has to deliver
The Config file’s job is to produce one thing: the imported target SimpleMath::SimpleMath, fully equipped. By the time the file finishes executing, target_link_libraries(consumer_app PRIVATE SimpleMath::SimpleMath) has to be enough to give the consumer the library to link against, the include directories to compile against, the compile features the library was built with, and any transitive dependencies the library itself needs.
Modern CMake is targets-first for exactly this reason. Usage requirements — public include directories, compile features, public link dependencies — are attached to the target itself rather than copy-pasted into every consumer. The consumer says “I link against this target” and inherits everything the target’s author marked PUBLIC or INTERFACE. There are no manual include_directories() calls or hand-maintained variables to keep in sync.
The :: namespace prefix matters here too. CMake treats anything containing :: as an imported target by convention; if the target doesn’t exist, the call is a hard error at configure time. Without the namespace, target_link_libraries(consumer_app PRIVATE SimpleMath) would fall through to looking for a system library called libSimpleMath — silently, with no error, until the link stage.
So the producer needs to ship a Config file that defines SimpleMath::SimpleMath with all of its usage requirements baked in, on whatever machine the consumer happens to be using. That requirement is what shapes the next two sections.
4. Why Targets.cmake is generated, not hand-written
A naive first attempt at the Config file would be to write the imported target by hand:
add_library(SimpleMath::SimpleMath STATIC IMPORTED)
set_target_properties(SimpleMath::SimpleMath PROPERTIES
IMPORTED_LOCATION "/home/alice/install/lib/libSimpleMath.a"
INTERFACE_INCLUDE_DIRECTORIES "/home/alice/install/include"
)
This works on Alice’s machine. It breaks the moment anyone else installs the package, because the absolute paths are baked in. The Config file has to be relocatable: the same file shipped in the install tree has to work no matter where that install tree lives.
CMake solves this with install(EXPORT). After defining the build-tree target, the producer adds:
install(EXPORT SimpleMathTargets
FILE SimpleMathTargets.cmake
NAMESPACE SimpleMath::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/SimpleMath
)
CMake generates SimpleMathTargets.cmake automatically — same imported-target structure as the hand-written version, but with paths computed at consume time relative to the package’s own location on disk. Move the install tree somewhere else, and the generated targets file still works.
The BUILD_INTERFACE and INSTALL_INTERFACE generator expressions on target_include_directories exist for a closely related reason: the build tree’s include directory is not the install tree’s include directory. In-tree consumers (someone using add_subdirectory(SimpleMath) from a parent project) need ${CMAKE_CURRENT_SOURCE_DIR}/include. Installed consumers need ${CMAKE_INSTALL_PREFIX}/include. The same target, with the same target_include_directories call, has to work in both contexts:
target_include_directories(SimpleMath
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
BUILD_INTERFACE evaluates only when the target is consumed from the build tree. INSTALL_INTERFACE evaluates only when the generated Targets.cmake is loaded. Forgetting INSTALL_INTERFACE is one of the most common modern-CMake bugs: the package installs cleanly, the consumer’s find_package succeeds, and then #include <simplemath/simplemath.hpp> fails with “no such file” because the imported target has no public include directories.
Forgetting NAMESPACE SimpleMath:: in the install(EXPORT) call is the other classic. The exported target is then called SimpleMath instead of SimpleMath::SimpleMath — which works, until two installed packages each define a target called SimpleMath and the consumer hits a redefinition error at configure time. The full set of relevant calls is laid out in CMake’s importing/exporting guide; §7 below is the working assembly.
5. Transitive dependencies
The consumer links SimpleMath::SimpleMath. SimpleMath itself links spdlog::spdlog. The consumer never called find_package(spdlog). What happens?
Without intervention: a configure-time error. CMake reads SimpleMathTargets.cmake, sees that SimpleMath::SimpleMath has INTERFACE_LINK_LIBRARIES containing spdlog::spdlog, looks for a target by that name in the consumer’s project, doesn’t find it, and aborts. The targets file knows the name of its dependencies, but it doesn’t pull them in.
That is what SimpleMathConfig.cmake is for. The Config file runs before Targets.cmake is included, and it is responsible for ensuring every transitive dependency the targets file is going to mention is already a real target in the consumer’s project. Hence the .cmake.in template:
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(spdlog CONFIG REQUIRED)
include("${CMAKE_CURRENT_LIST_DIR}/SimpleMathTargets.cmake")
Three things are doing work here. find_dependency is a thin wrapper around find_package that propagates REQUIRED and QUIET from the consumer’s original find_package(SimpleMath ...) call — so a find_package(SimpleMath) without REQUIRED can soft-fail when spdlog is missing, exactly the same way it would soft-fail if SimpleMath itself were missing.
@PACKAGE_INIT@ is a placeholder that configure_package_config_file substitutes with the relocation boilerplate — a few lines of CMake that compute paths relative to the Config file’s own location, which is why ${CMAKE_CURRENT_LIST_DIR} resolves correctly no matter where the install tree ends up. (This is the same mechanism that makes Targets.cmake relocatable; the Config file inherits it for free.)
The template lives at cmake/SimpleMathConfig.cmake.in in the producer’s source tree. configure_package_config_file turns it into SimpleMathConfig.cmake at build time, and install(FILES ...) ships it alongside the targets file.
Forget the find_dependency line, and the consumer’s find_package(SimpleMath) succeeds — only to fail moments later with target "spdlog::spdlog" not found when the targets file is loaded.
6. Versioning
A consumer might write find_package(SimpleMath 1.2 REQUIRED). CMake needs some way to decide whether the version of SimpleMath it just found on disk is acceptable.
The answer is a separate file, SimpleMathConfigVersion.cmake, generated by write_basic_package_version_file:
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/SimpleMathConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
CMake reads this file first, before the rest of the package config — that is why it lives in its own file rather than baked into Config.cmake. The version file sets two variables, PACKAGE_VERSION_COMPATIBLE and PACKAGE_VERSION_EXACT, and CMake uses them to decide whether to keep searching the prefix path or accept the candidate it has.
The COMPATIBILITY argument selects the policy:
SameMajorVersion— accept any installed version with the same MAJOR component and a MINOR ≥ requested. The right default for libraries that follow semantic versioning: requesting1.2is happy with1.4but not with2.0.SameMinorVersion— stricter. Useful for libraries that don’t promise minor-version compatibility.ExactVersion— pin to one release. Almost never what a library author wants.AnyNewerVersion— accept any version ≥ requested. Loose; only sensible for libraries with truly stable APIs.
SameMajorVersion is almost always the right choice. The cost of being wrong is that consumers requesting older versions fail to find a newer install they would actually have been compatible with, or that consumers requesting anything succeed against a release that has since broken their use case.
7. The producer’s CMakeLists.txt
With every piece motivated, the full producer-side CMakeLists.txt reads as a sequence of consequences rather than a list of incantations.
The library source is unremarkable. include/simplemath/simplemath.hpp:
#pragma once
namespace simplemath {
double add(double a, double b);
double divide(double numerator, double denominator);
} // namespace simplemath
src/simplemath.cpp:
#include <simplemath/simplemath.hpp>
#include <spdlog/spdlog.h>
#include <stdexcept>
namespace simplemath {
double add(double a, double b) {
const auto result = a + b;
spdlog::debug("add({}, {}) = {}", a, b, result);
return result;
}
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
spdlog::error("divide({}, {}): division by zero", numerator, denominator);
throw std::invalid_argument("division by zero");
}
const auto result = numerator / denominator;
spdlog::debug("divide({}, {}) = {}", numerator, denominator, result);
return result;
}
} // namespace simplemath
The directory layout:
SimpleMath/
├─ CMakeLists.txt
├─ cmake/
│ └─ SimpleMathConfig.cmake.in
├─ include/
│ └─ simplemath/simplemath.hpp
└─ src/
└─ simplemath.cpp
And the full producer CMakeLists.txt, with each non-trivial block annotated with the section that motivated it:
cmake_minimum_required(VERSION 3.20)
project(SimpleMath VERSION 1.0.0 LANGUAGES CXX)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
add_library(SimpleMath src/simplemath.cpp)
add_library(SimpleMath::SimpleMath ALIAS SimpleMath) # in-tree consumers see the namespaced name too — see §3
target_compile_features(SimpleMath PUBLIC cxx_std_17)
find_package(spdlog CONFIG REQUIRED)
target_link_libraries(SimpleMath PUBLIC spdlog::spdlog) # name will appear in Targets.cmake — see §5
target_include_directories(SimpleMath
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # see §4
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> # see §4
)
install(TARGETS SimpleMath
EXPORT SimpleMathTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
set(SIMPLEMATH_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/SimpleMath")
install(EXPORT SimpleMathTargets # generates the relocatable Targets.cmake — see §4
FILE SimpleMathTargets.cmake
NAMESPACE SimpleMath::
DESTINATION ${SIMPLEMATH_INSTALL_CMAKEDIR}
)
write_basic_package_version_file( # see §6
"${CMAKE_CURRENT_BINARY_DIR}/SimpleMathConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
configure_package_config_file( # turns the .in template into the consumed Config.cmake — see §5
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/SimpleMathConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/SimpleMathConfig.cmake"
INSTALL_DESTINATION ${SIMPLEMATH_INSTALL_CMAKEDIR}
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/SimpleMathConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/SimpleMathConfigVersion.cmake"
DESTINATION ${SIMPLEMATH_INSTALL_CMAKEDIR}
)
Build and install:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="$PWD/install"
cmake --build build
cmake --install build
The resulting install tree:
install/
├─ include/simplemath/simplemath.hpp
└─ lib/cmake/SimpleMath/
├─ SimpleMathConfig.cmake
├─ SimpleMathConfigVersion.cmake
└─ SimpleMathTargets.cmake
The library binary lives wherever GNUInstallDirs resolves CMAKE_INSTALL_LIBDIR to on the platform — lib/, lib64/, or similar — alongside the cmake/ subdirectory.
8. The four files, end to end
Four files do all the work.
SimpleMathTargets.cmake— generated byinstall(EXPORT). Defines the imported targetSimpleMath::SimpleMathwith relocatable paths, public include directories, and the link interface.SimpleMathConfig.cmake— generated fromSimpleMathConfig.cmake.in. Pulls in transitive dependencies viafind_dependency, then loadsTargets.cmake.SimpleMathConfigVersion.cmake— generated bywrite_basic_package_version_file. Answersfind_package’s version compatibility query before any of the other files are read.- The consumer’s
find_package(SimpleMath CONFIG REQUIRED)— locatesConfig.cmakeon the prefix path, runs the version check, runs the dependency lookup, loads the targets file. After it returns,SimpleMath::SimpleMathexists in the consumer’s project with everything attached.
The flow, condensed:
consumer: find_package(SimpleMath CONFIG REQUIRED)
│
├─ search CMAKE_PREFIX_PATH for SimpleMathConfig.cmake
├─ run SimpleMathConfigVersion.cmake — version compatible?
├─ run SimpleMathConfig.cmake
│ ├─ find_dependency(spdlog) — recurse
│ └─ include SimpleMathTargets.cmake — defines SimpleMath::SimpleMath
└─ return — SimpleMath::SimpleMath is now a real target
consumer: target_link_libraries(consumer_app PRIVATE SimpleMath::SimpleMath)
└─ inherits include dirs, compile features, transitive deps
A few topics deliberately left out are worth pointing at next. Shared vs static libraries — BUILD_SHARED_LIBS, POSITION_INDEPENDENT_CODE, and the soname conventions that install(TARGETS) handles automatically — change the install layout but not the four-file structure. Multi-config generators like Ninja Multi-Config and Visual Studio install per-configuration libraries side by side, which adds a layer to the install tree but is otherwise transparent to consumers. Package managers like vcpkg and Conan consume the exact same Config.cmake interface this post produces — that interoperation is the whole point of getting the four files right.
Every piece of the producer’s setup — the BUILD_INTERFACE/INSTALL_INTERFACE split, the namespace alias, the .cmake.in template, the version file, the export — exists to keep the consumer’s two lines working.