How CMake's find_package Really Works: A Worked Example with Threads
CMake's find_package is a cached search that yields imported targets. A worked Threads example shows how REQUIRED, version checks, cache resets, and verbose link inspection prevent the usual integration failures.
17 Jul 2025, 10:52 UTC

When a CMake build fails with "Could NOT find Threads" or links the wrong library version, the fix is rarely to install something new. It is usually to understand what find_package actually does: it runs a search script, records results in the cache, and (in modern CMake) hands you an imported target to link against. Once you know that mechanism, most third-party integration problems become predictable.
This article walks through the mechanism using Threads — the threading library module shipped with CMake itself — because it behaves exactly like a third-party package while requiring nothing extra to install. The same reasoning applies to find_package(Boost), find_package(OpenSSL), or any config-file package.
What find_package actually does
find_package(<Name>) has two search strategies, tried in order:
- Module mode: CMake looks for a file named
Find<Name>.cmake, first in your project'sCMAKE_MODULE_PATH, then in CMake's own module directory. This is howThreads,ZLIB, andOpenSSLare found. - Config mode: CMake searches for
<Name>Config.cmakeor<name>-config.cmakefiles installed by the library itself, under prefixes listed inCMAKE_PREFIX_PATHand standard system locations.
Either way, the result is a set of variables (traditionally <Name>_FOUND, include dirs, library paths) and, in modern usage, one or more imported targets such as Threads::Threads. An imported target is a pre-built library described to CMake: it carries its include directories, compile flags, and link flags, so consumers just link to it by name.
A worked configuration
A minimal, correct usage looks like this:
cmake_minimum_required(VERSION 3.14)
project(demo LANGUAGES CXX)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE) # prefer pthreads when both exist
find_package(Threads REQUIRED)
add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE Threads::Threads)
Three things matter here:
REQUIREDmakes configuration abort with an error if the library is missing. Omit it and CMake prints a status message and continues — your target then links against nothing and fails later at build time with undefined references, which is harder to diagnose.Threads::Threadsis the imported target. Linking it adds the correct flag for the platform:-pthreadon Linux, nothing extra on Windows (where threading is in the system runtime), or the appropriate framework on macOS. Never hardcode-lpthreadyourself.LANGUAGES CXXinproject()matters. TheFindThreadsmodule probes the compiler to determine the right flag, so a working C or C++ compiler must be enabled before the call.
Run it from the project root (no special permissions needed):
cmake -S . -B build
cmake --build build
Version requirements and cache behavior
You can request a minimum version: find_package(OpenSSL 1.1 REQUIRED). Whether the version is actually checked depends on the find module or config file supporting it — many older Find*.cmake modules ignore the version argument entirely. For config-mode packages, the library's <Name>ConfigVersion.cmake does the comparison.
The subtler issue is the cache. Results of the search — paths like OPENSSL_ROOT_DIR or internal variables such as CMAKE_HAVE_THREADS_LIBRARY — are stored in CMakeCache.txt. On subsequent runs, find_package reuses those cached paths instead of searching again. This is why stale state causes so much confusion:
- You install a newer library version, re-run CMake, and it still finds the old one — because the old path is cached.
- A leftover
CMAKE_PREFIX_PATHorCMAKE_LIBRARY_PATHfrom a previous experiment (or from your shell environment) steers the search to an unintended installation.
The reliable reset is to delete the cache and reconfigure:
rm -rf build/CMakeCache.txt build/CMakeFiles
cmake -S . -B build
Then verify what was actually found. Check build/CMakeCache.txt for the resolved paths, and confirm real linkage by inspecting the link command:
# Makefile generator:
cmake --build build -- VERBOSE=1
# Ninja generator:
ninja -C build -t commands | grep demo
You should see -pthread (or the platform equivalent) on the link line for your executable. If the flag is absent, the target was not linked — regardless of what configure-time messages said.
Common mistakes and platform limits
Assuming success means linkage
On Windows, find_package(Threads) can report success because thread support is part of the system, yet a project that also pulls in a POSIX-style library may fail to link. If you need explicit control, CMAKE_USE_WIN32_THREADS can be set before the find call; for genuinely external dependencies, FetchContent (which builds the dependency from source as part of your project) sidesteps search-path problems entirely.
Searching before enabling a language
Calling find_package(Threads) before project() (or in a project with no compiled language enabled) leaves the module unable to probe the compiler, producing missing-target or false-negative errors. Always call it after project().
Cross-compiling with default search paths
When cross-compiling, the default search locations point at the host system, not the target. Provide explicit locations:
find_package(Threads REQUIRED
PATHS ${CMAKE_SYSROOT}/usr/lib
NO_DEFAULT_PATH)
or, more commonly, set CMAKE_PREFIX_PATH and CMAKE_FIND_ROOT_PATH in your toolchain file so every find_package call respects the target sysroot.
Linking variables instead of targets
Old-style code links ${CMAKE_THREAD_LIBS_INIT} directly. That works but drops compile options (on some platforms -pthread is needed at compile time too, not just link time). The imported target carries both, which is why Threads::Threads is the recommended form.
Checking your result
Three quick checks confirm a healthy integration: cmake --version shows 3.14 or newer (imported targets and modern module behavior are solid there); the cache contains the expected resolved paths after configure; and the verbose link line contains the expected flag. If any check fails, delete the cache before reconfiguring — re-running on a dirty cache is the single most common reason a correct fix appears not to work.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.