CMake find_package: CONFIG vs MODULE, Search Paths and Errors

Key takeaways

find_package tries a Find<Pkg>.cmake module first, then a <Pkg>Config.cmake file. Point it at the right prefix with CMAKE_PREFIX_PATH or <Pkg>_ROOT, link the imported target, and clear a stale <Pkg>_DIR when it keeps finding the wrong copy.

find_package(Foo REQUIRED) answers one question: where on this machine is Foo, and what do I link to use it? The answer comes back as imported targets such as Foo::Foo. Those targets carry their include directories, compile definitions and transitive dependencies, so one target_link_libraries(app PRIVATE Foo::Foo) replaces hardcoded paths that would differ between /usr/lib, /opt/homebrew/lib and C:\vcpkg\installed.

Most find_package trouble comes from not knowing how the search runs. So this guide starts there, then goes through the errors you will actually see. Every CMake error quoted below was reproduced with CMake 4.3 against a small package I installed to a scratch prefix.

The minimum you need

cmake_minimum_required(VERSION 3.24)
project(app LANGUAGES CXX)

find_package(fmt 10 CONFIG REQUIRED)         # 10.x or anything the package says is compatible
find_package(OpenSSL 3.0 REQUIRED)           # MODULE: CMake's FindOpenSSL.cmake
find_package(ZLIB)                           # optional; check ZLIB_FOUND

add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt OpenSSL::SSL)
if(ZLIB_FOUND)
  target_link_libraries(app PRIVATE ZLIB::ZLIB)
  target_compile_definitions(app PRIVATE HAVE_ZLIB)
endif()
  • REQUIRED makes a missing package a configure error. Without it you get <Pkg>_FOUND set to false and nothing else, so check the variable before you use the targets.
  • A version such as 1.4 means “at least 1.4 and compatible”. The package decides what “compatible” means through its <Pkg>ConfigVersion.cmake: SameMajorVersion rejects 2.x when you ask for 1.4. EXACT asks for an exact match, and since CMake 3.19 you can give a range: find_package(Foo 1.4...<2.0).
  • COMPONENTS a b asks for parts of a package (Boost, Qt6). OPTIONAL_COMPONENTS lists parts you can do without.

Link the target, not ${Foo_LIBRARIES}. A target also brings the include paths, definitions and dependent libraries. A variable is only a list of files, and it quietly loses anything the package needs downstream.


How the search works: MODULE vs CONFIG

There are two kinds of files that can answer a find_package call:

MODULE modeCONFIG mode
FileFind<Pkg>.cmake<Pkg>Config.cmake or <lowercase>-config.cmake (CMake 4.x also looks for .cps files)
Written byCMake, or youThe library’s own build, at install time
Found viaCMAKE_MODULE_PATH, then CMake’s own modules<Pkg>_DIR, <Pkg>_ROOT, CMAKE_PREFIX_PATH, system prefixes
AccuracyGuesses from headers and library file namesExact targets and dependencies from the real build

Default order: MODULE first, then CONFIG. A plain find_package(Foo) looks for FindFoo.cmake. It only searches for FooConfig.cmake if no such module exists. People often assume the opposite. To test it, I put a Findgreet.cmake in CMAKE_MODULE_PATH and also installed a real greetConfig.cmake on the prefix path. find_package(greet) loaded the Find module, and greet_DIR stayed empty. Adding -DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON (CMake 3.15+) switched it to the config file.

You can force either mode:

find_package(greet CONFIG REQUIRED)   # only <Pkg>Config.cmake (NO_MODULE means the same)
find_package(CURL  MODULE REQUIRED)   # only FindCURL.cmake

My advice: use CONFIG for anything that ships a config file (fmt, spdlog, nlohmann_json, Qt, Boost 1.70+, gRPC, Protobuf, anything installed by vcpkg or Conan). Use plain or MODULE calls for the system libraries where CMake’s Find module is the normal route (OpenSSL, ZLIB, CURL, Threads, Python). A bare find_package(Foo) is fine too, but know that it prefers a Find module first.

Boost: FindBoost is gone (CMP0167)

CMake 3.30 removed its FindBoost module (policy CMP0167), because Boost 1.70 and later ship BoostConfig.cmake. How your project behaves depends on cmake_minimum_required:

  • Minimum 3.30 or newer: find_package(Boost) goes straight to CONFIG. If Boost is missing, the message lists BoostConfig.cmake / boost-config.cmake, not FindBoost variables.
  • Older minimum: the old FindBoost still runs, and you get Policy CMP0167 is not set: The FindBoost module is removed. as a dev warning. The hint variables BOOST_ROOT, BOOST_INCLUDEDIR and Boost_NO_SYSTEM_PATHS only work with FindBoost.
find_package(Boost 1.80 CONFIG REQUIRED COMPONENTS filesystem program_options)
target_link_libraries(app PRIVATE Boost::filesystem Boost::program_options)
# Header-only libraries (asio, optional, ...) need only Boost::headers

Leave system out of COMPONENTS. Boost.System has been header-only since 1.69, and newer releases stopped shipping the old stub library, so asking for that component can make an otherwise working Boost fail to resolve.


Telling CMake where to look

In CONFIG mode, CMake takes each prefix it knows about and looks under paths like <prefix>/lib/cmake/<Pkg>*/, <prefix>/lib/<arch>/cmake/<Pkg>*/, <prefix>/share/<Pkg>*/cmake/ and (on Windows) <prefix>/<Pkg>*/. These are the ways to control it, roughly in the order CMake checks them:

VariableWhat it isNotes
<Pkg>_DIRDirectory that directly contains <Pkg>Config.cmakeCached after the first success, see stale cache
<Pkg>_ROOT (CMake var or env var)A prefix for this one packageCMP0074, CMake 3.12+. Upper-case <PKG>_ROOT also works since 3.27 (CMP0144)
CMAKE_PREFIX_PATH (CMake var, then env var)List of prefixes for every packageSeparate entries with ; in CMake. The env var uses the platform path separator
PATH entriesbin/sbin entries are turned into prefixesThis is why tools on your PATH can change results
System prefixes/usr, /usr/local, C:/Program Files/...CMAKE_SYSTEM_PREFIX_PATH
# One package in a custom location
cmake -S . -B build -Dgreet_ROOT=/opt/greet-1.4

# Several private prefixes
cmake -S . -B build -DCMAKE_PREFIX_PATH="/opt/greet;/opt/other"

# Package managers: use their toolchain instead of paths
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake

When you can’t see why something was or wasn’t found, run cmake --debug-find-pkg=greet (CMake 3.23+). It prints every source it checked, including <PackageName>_ROOT CMake variable, CMAKE_PREFIX_PATH variable, Env variable greet_DIR and so on, plus the actual candidate directories. --debug-find does the same for every find call, which gives you much more output.


Common libraries

# OpenSSL (FindOpenSSL; hint: -DOPENSSL_ROOT_DIR=...)
find_package(OpenSSL 3.0 REQUIRED)
target_link_libraries(app PRIVATE OpenSSL::SSL OpenSSL::Crypto)

# Qt 6 (config package; one call, several components)
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
qt_standard_project_setup()                 # Qt 6.3+: turns on AUTOMOC/AUTOUIC
qt_add_executable(gui main.cpp)
target_link_libraries(gui PRIVATE Qt6::Widgets)

# GoogleTest (FindGTest forwards to GTestConfig when available)
find_package(GTest REQUIRED)
target_link_libraries(tests PRIVATE GTest::gtest_main)   # names used since CMake 3.20
include(GoogleTest)
gtest_discover_tests(tests)

# OpenCV (config package; OpenCV_LIBS holds imported target names)
find_package(OpenCV REQUIRED COMPONENTS core imgproc)
target_link_libraries(app PRIVATE ${OpenCV_LIBS})

# Libraries that only ship a .pc file: ask for an imported target
find_package(PkgConfig REQUIRED)
pkg_check_modules(SQLITE REQUIRED IMPORTED_TARGET sqlite3)
target_link_libraries(app PRIVATE PkgConfig::SQLITE)

Before CMake 3.20, FindGTest named its targets GTest::GTest and GTest::Main. If an old tutorial’s names don’t resolve, that’s why. Leave IMPORTED_TARGET off pkg_check_modules and you’re back to juggling SQLITE_INCLUDE_DIRS, SQLITE_LIBRARIES and SQLITE_LDFLAGS separately. Forgetting the LDFLAGS part is a classic cause of link errors that only show up on one distro.

If you don’t know a package’s target names, open the installed <Pkg>Config.cmake or <Pkg>Targets.cmake and search for add_library(. That is quicker than guessing from docs.


Errors and fixes

”Could not find a package configuration file provided by”

CMake Error at CMakeLists.txt:3 (find_package):
  By not providing "FindNoSuchPkg.cmake" in CMAKE_MODULE_PATH this project
  has asked CMake to find a package configuration file provided by
  "NoSuchPkg", but CMake did not find one.

  Could not find a package configuration file provided by "NoSuchPkg"
  (requested version 1.2) with any of the following names:

    NoSuchPkg.cps
    nosuchpkg.cps
    NoSuchPkgConfig.cmake
    nosuchpkg-config.cmake

  Add the installation prefix of "NoSuchPkg" to CMAKE_PREFIX_PATH or set
  "NoSuchPkg_DIR" to a directory containing one of the above files.

The first paragraph tells you that no Find module existed, so CMake fell back to CONFIG and failed there too. Fixes, in order:

  1. Is the development package installed? Distros split runtime and headers: libssl-dev / openssl-devel, libboost-filesystem-dev, qt6-base-dev. The runtime package alone ships no config file.
  2. Is the prefix on the search path? Pass -D<Pkg>_ROOT= or -DCMAKE_PREFIX_PATH= with the directory that contains lib/cmake/.... Don’t pass the lib/cmake/<Pkg> directory itself. That one belongs in <Pkg>_DIR.
  3. Is the name spelled the way the file is? The name is case-sensitive: find_package(nlohmann_json) and find_package(GTest), not json or gtest.
  4. Does the architecture or configuration match? A 32-bit install, or an install made with a different compiler or ABI, can be skipped with no explanation, because the version file rejects it. --debug-find-pkg shows it as considered but not accepted.

”compatible with requested version”

CMake Error at CMakeLists.txt:3 (find_package):
  Could not find a configuration file for package "greet" that is compatible
  with requested version "2.0".

  The following configuration files were considered but not accepted:

    <prefix>/lib/cmake/greet/greetConfig.cmake, version: 1.4.2
      The version found is not compatible with the version requested.

This is actually good news: CMake found the package, so your paths are right. The package’s ConfigVersion.cmake refused the request. The installed version is 1.4.2, the version file says SameMajorVersion, and 2.0 is a different major version. Either install a matching version, or widen the request, for example find_package(greet 1.4...<3 CONFIG).

It keeps finding the wrong copy

After a successful search, CMake stores <Pkg>_DIR in CMakeCache.txt. On later runs it uses that cached directory and doesn’t search again. I tested it: I configured against one prefix, then reconfigured the same build tree with CMAKE_PREFIX_PATH pointing at a second copy, and greet_DIR still pointed at the first prefix. Only cmake -U greet_DIR (or deleting the build directory) made it search again.

I have lost time to this more than once. You install a newer library, or switch from the system copy to a vcpkg one, and the build keeps linking the old one while every setting you look at seems right. When the path is wrong, check <Pkg>_DIR in the cache first. Also watch for PATH: a Conda or Anaconda bin directory on PATH is a common reason CMake picks that environment’s OpenSSL or Qt over the system one.

CMake Error at app/CMakeLists.txt:2 (target_link_libraries):
  Target "app" links to:

    greet::greet

  but the target was not found.  Possible reasons include:

    * There is a typo in the target name.
    * A find_package call is missing for an IMPORTED target.
    * An ALIAS target is missing.

I reproduced this with find_package(greet ...) in deps/CMakeLists.txt and the executable in a sibling app/ directory. Imported targets are directory-scoped: they’re visible in the directory that called find_package and its subdirectories, not in sibling directories. Move the call to the top-level CMakeLists.txt, or use find_package(greet CONFIG REQUIRED GLOBAL) (CMake 3.24+). You get the same error if the name really is a typo, so check the Targets file for the exact spelling. Note that this only fails at the generate step, after “Configuring done”.

Found, but the variables are empty

A Find module and a config file for the same package can set different variables. FindBoost set Boost_INCLUDE_DIRS, while BoostConfig mainly defines targets. Code that reads ${Foo_LIBRARIES} can therefore break just because the other mode won. Linking the targets avoids the whole problem. If you must read variables, force the mode explicitly so it can’t change under you.

Optional dependency silently off in CI

find_package(Foo) with no REQUIRED fails without an error. That’s what it’s for, but it also means a missing dev package in the CI image turns a feature off with no warning. Print the choice (message(STATUS "Foo: ${Foo_FOUND}")), or put it behind an option that becomes REQUIRED when the option is ON:

option(APP_WITH_TLS "Build TLS support" ON)
if(APP_WITH_TLS)
  find_package(OpenSSL 3.0 REQUIRED)       # user asked for it: fail loudly
  target_link_libraries(app PRIVATE OpenSSL::SSL)
  target_compile_definitions(app PRIVATE APP_WITH_TLS)
endif()

Making your own library findable

If other projects consume your library, install a config package so they can write find_package(greet CONFIG). The following was built with g++ 10.3 and consumed from a separate project with -Dgreet_ROOT=<prefix>:

cmake_minimum_required(VERSION 3.20)
project(greet VERSION 1.4.2 LANGUAGES CXX)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

add_library(greet src/greet.cpp)
add_library(greet::greet ALIAS greet)          # same name in-tree and installed
target_compile_features(greet PUBLIC cxx_std_17)
target_include_directories(greet PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

install(TARGETS greet EXPORT greetTargets)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT greetTargets NAMESPACE greet::
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/greet)

configure_package_config_file(cmake/greetConfig.cmake.in
  ${CMAKE_CURRENT_BINARY_DIR}/greetConfig.cmake
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/greet)
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/greetConfigVersion.cmake
  COMPATIBILITY SameMajorVersion)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/greetConfig.cmake
              ${CMAKE_CURRENT_BINARY_DIR}/greetConfigVersion.cmake
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/greet)
# cmake/greetConfig.cmake.in
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# find_dependency(OpenSSL 3.0)   # every PUBLIC/INTERFACE dependency goes here
include("${CMAKE_CURRENT_LIST_DIR}/greetTargets.cmake")
check_required_components(greet)

One thing people often miss is find_dependency. If greet publicly links OpenSSL::SSL, the exported targets refer to OpenSSL::SSL, and consumers get “target was not found” errors unless your config file finds OpenSSL for them. For a static library this applies even to PRIVATE dependencies, because the linker still needs them.

The ALIAS means a parent project can use add_subdirectory(greet) or FetchContent and link the same greet::greet name. Since CMake 3.24, FetchContent_Declare(greet ... FIND_PACKAGE_ARGS 1.4) tries find_package(greet 1.4) first and only downloads the library if that fails.


Writing a Find module when there is no config

For an old library that ships only headers and a .so/.lib, write a small Find module that creates an imported target. That way the rest of your build still links targets:

# cmake/FindLegacyLib.cmake
find_path(LegacyLib_INCLUDE_DIR NAMES legacy/legacy.h)
find_library(LegacyLib_LIBRARY NAMES legacy legacyd)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LegacyLib
  REQUIRED_VARS LegacyLib_LIBRARY LegacyLib_INCLUDE_DIR)

if(LegacyLib_FOUND AND NOT TARGET LegacyLib::LegacyLib)
  add_library(LegacyLib::LegacyLib UNKNOWN IMPORTED)
  set_target_properties(LegacyLib::LegacyLib PROPERTIES
    IMPORTED_LOCATION "${LegacyLib_LIBRARY}"
    INTERFACE_INCLUDE_DIRECTORIES "${LegacyLib_INCLUDE_DIR}")
endif()
mark_as_advanced(LegacyLib_INCLUDE_DIR LegacyLib_LIBRARY)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(LegacyLib REQUIRED)
target_link_libraries(app PRIVATE LegacyLib::LegacyLib)

Don’t hardcode PATHS /usr/lib /usr/local/lib. Those are searched anyway, and hardcoding them hides the LegacyLib_ROOT / CMAKE_PREFIX_PATH behavior users expect. find_path and find_library also respect <Pkg>_ROOT when called from inside a Find module. The NOT TARGET guard stops a second find_package call in another directory from failing with “target already exists”.

Keep a module like this in your own repo only until upstream ships a config file. Also don’t name it after a module CMake already provides, such as FindOpenSSL.cmake, unless you really mean to replace CMake’s version. Anything in CMAKE_MODULE_PATH takes priority.