Table of Contents

  1. find_package() Basics
  2. How find_package Searches
  3. Config Mode (Modern)
  4. Module Mode (Legacy)
  5. Writing a Find Module
  6. pkg-config Integration
  7. Finding Programs
  8. Finding Python
  9. Common Libraries
  10. Troubleshooting
  11. Conclusion & Next Steps
Back to CMake Mastery Series

Part 9: Detecting External Libraries and Programs

June 4, 2026 Wasil Zafar 45 min read

Master find_package() in Module and Config modes, write custom Find modules, integrate pkg-config, find programs, and troubleshoot dependency detection in CMake projects.

find_package() Basics

Real-world C and C++ projects rarely exist in isolation. They depend on external libraries — compression (ZLIB), networking (OpenSSL), testing (GTest), and many more. CMake's find_package() command is the standard mechanism for locating these dependencies, setting up include paths, and linking libraries — all in a portable way.

Key Insight: find_package() operates in two distinct modes: Module mode (searches for FindXXX.cmake scripts) and Config mode (searches for XXXConfig.cmake files installed by the package). Modern CMake strongly prefers Config mode because it provides imported targets with full usage requirements.

The simplest form of find_package():

# CMakeLists.txt — Basic find_package usage
cmake_minimum_required(VERSION 3.16)
project(FindDemo LANGUAGES CXX)

# Find ZLIB — REQUIRED means configure fails if not found
find_package(ZLIB REQUIRED)

add_executable(compress_demo main.cpp)
# Link using the imported target (modern approach)
target_link_libraries(compress_demo PRIVATE ZLIB::ZLIB)

REQUIRED and COMPONENTS

The REQUIRED keyword causes CMake to halt configuration with an error if the package isn't found. Without it, CMake sets <Package>_FOUND to FALSE and continues — useful for optional dependencies.

# CMakeLists.txt — COMPONENTS and version requirements
cmake_minimum_required(VERSION 3.16)
project(BoostDemo LANGUAGES CXX)

# Find Boost with specific components and minimum version
find_package(Boost 1.74 REQUIRED COMPONENTS filesystem system regex)

add_executable(fs_demo main.cpp)
target_link_libraries(fs_demo PRIVATE
    Boost::filesystem
    Boost::system
    Boost::regex
)

The COMPONENTS keyword specifies which parts of a multi-component library you need. Only listed components are required. Use OPTIONAL_COMPONENTS for nice-to-have parts:

# CMakeLists.txt — Optional component handling
cmake_minimum_required(VERSION 3.16)
project(QtDemo LANGUAGES CXX)

find_package(Qt6 REQUIRED COMPONENTS Core Widgets OPTIONAL_COMPONENTS Network)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE Qt6::Core Qt6::Widgets)

if(Qt6_Network_FOUND)
    target_link_libraries(app PRIVATE Qt6::Network)
    target_compile_definitions(app PRIVATE HAS_NETWORK=1)
endif()

Version Requirements

You can specify exact or minimum version requirements. CMake checks the version information provided by the package's config file:

# CMakeLists.txt — Version constraints
cmake_minimum_required(VERSION 3.16)
project(VersionDemo LANGUAGES CXX)

# Minimum version
find_package(OpenSSL 1.1 REQUIRED)

# Exact version (rarely used — too restrictive)
find_package(Protobuf 3.21.0 EXACT REQUIRED)

# Version range (CMake 3.19+)
find_package(fmt 8.0...<10.0 REQUIRED)

message(STATUS "OpenSSL version: ${OPENSSL_VERSION}")
message(STATUS "Protobuf version: ${Protobuf_VERSION}")

How find_package Searches

Understanding the search order is essential for debugging "package not found" errors. The search differs between Module mode and Config mode.

find_package() Search Order
        flowchart TD
            A["find_package(Foo)"] --> B{Module Mode First?}
            B -->|Yes| C["Search CMAKE_MODULE_PATH
for FindFoo.cmake"] C --> D{Found?} D -->|Yes| E["Execute FindFoo.cmake"] D -->|No| F["Search CMake built-in
modules directory"] F --> G{Found?} G -->|Yes| E G -->|No| H["Fall through to
Config Mode"] B -->|CONFIG keyword| H H --> I["Search CMAKE_PREFIX_PATH"] I --> J["Search Foo_DIR"] J --> K["Search system paths
(/usr/lib/cmake, etc.)"] K --> L{FooConfig.cmake
found?} L -->|Yes| M["Import targets &
set variables"] L -->|No| N["FATAL_ERROR if REQUIRED
else set Foo_FOUND=FALSE"]

CMAKE_PREFIX_PATH

The CMAKE_PREFIX_PATH variable is the most common way to tell CMake where to find libraries installed in non-standard locations:

# Point CMake to custom install locations
cmake -B build \
    -DCMAKE_PREFIX_PATH="/opt/custom-libs;/home/user/local;C:/libs" \
    -S .

# Multiple paths separated by semicolons (CMake list syntax)
# Or set as environment variable:
export CMAKE_PREFIX_PATH="/opt/custom-libs:/home/user/local"
cmake -B build -S .
# CMakeLists.txt — Programmatically appending to prefix path
cmake_minimum_required(VERSION 3.16)
project(PrefixDemo LANGUAGES CXX)

# Append a project-local deps directory
list(APPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/third_party/install")

find_package(MyLib REQUIRED)

System Paths

CMake automatically searches platform-specific system paths. On Linux, this includes:

  • /usr/lib/cmake/ and /usr/lib64/cmake/
  • /usr/local/lib/cmake/
  • /usr/share/cmake/Modules/
  • Paths in the PATH environment variable (with /bin stripped, /lib/cmake appended)

On Windows, CMake also searches the registry and Program Files directories.

Package_DIR Variable

For Config mode, you can directly specify the directory containing the config file:

# Directly tell CMake where FooConfig.cmake lives
cmake -B build -DFoo_DIR="/path/to/foo/lib/cmake/Foo" -S .

# This skips all other search logic for this specific package

Config Mode (Modern)

Config mode is the preferred approach in modern CMake. When a library is installed properly, it provides a <Package>Config.cmake (or <package>-config.cmake) file that defines imported targets with complete usage requirements.

PackageConfig.cmake Files

A Config file is generated during a library's install step. Here's what a typical one provides:

# Example: What MyLibConfig.cmake typically does internally
# (You don't write this — the library's CMake install() generates it)

# Create an imported target
add_library(MyLib::MyLib SHARED IMPORTED)

set_target_properties(MyLib::MyLib PROPERTIES
    IMPORTED_LOCATION "/usr/local/lib/libmylib.so"
    INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include"
    INTERFACE_COMPILE_DEFINITIONS "MYLIB_SHARED"
    INTERFACE_LINK_LIBRARIES "Threads::Threads;ZLIB::ZLIB"
)

The version file (<Package>ConfigVersion.cmake) enables version checking:

# CMakeLists.txt — Using a library that provides Config mode
cmake_minimum_required(VERSION 3.16)
project(ConfigDemo LANGUAGES CXX)

# Config mode: CMake searches for fmtConfig.cmake
find_package(fmt 9.0 REQUIRED CONFIG)

add_executable(app main.cpp)
# Imported target carries includes, definitions, and link deps
target_link_libraries(app PRIVATE fmt::fmt)

Imported Targets

Imported targets are the key benefit of Config mode. They encapsulate everything needed to use a library:

Best Practice: Always prefer imported targets (Package::Component) over raw variables (${Package_LIBRARIES}, ${Package_INCLUDE_DIRS}). Imported targets propagate usage requirements transitively and work correctly with generator expressions.
# CMakeLists.txt — Imported targets vs legacy variables
cmake_minimum_required(VERSION 3.16)
project(TargetDemo LANGUAGES CXX)

find_package(OpenSSL REQUIRED)

add_executable(app main.cpp)

# MODERN (preferred): Use imported targets
target_link_libraries(app PRIVATE OpenSSL::SSL OpenSSL::Crypto)

# LEGACY (avoid): Manual include/link — misses transitive deps
# target_include_directories(app PRIVATE ${OPENSSL_INCLUDE_DIR})
# target_link_libraries(app PRIVATE ${OPENSSL_LIBRARIES})
Module Mode vs Config Mode
        flowchart LR
            subgraph Module["Module Mode (Legacy)"]
                direction TB
                M1["FindFoo.cmake script"]
                M2["Sets Foo_FOUND, Foo_LIBRARIES,
Foo_INCLUDE_DIRS variables"] M3["May or may not create
imported targets"] M1 --> M2 --> M3 end subgraph Config["Config Mode (Modern)"] direction TB C1["FooConfig.cmake installed
by the library itself"] C2["Creates Foo::Foo and
Foo::Component targets"] C3["Full usage requirements:
includes, defs, link deps"] C1 --> C2 --> C3 end Module -.->|"Fallback"| Config

Module Mode (Legacy)

Module mode uses FindXXX.cmake scripts — either ones you write, ones in CMAKE_MODULE_PATH, or ones bundled with CMake itself. This mode predates Config mode and is still used for libraries that don't provide their own CMake configuration.

FindXXX.cmake Modules

CMake searches for Find modules in this order:

  1. Directories listed in CMAKE_MODULE_PATH (your project's custom modules)
  2. CMake's built-in module directory (ships with CMake installation)
# CMakeLists.txt — Using CMAKE_MODULE_PATH for custom Find modules
cmake_minimum_required(VERSION 3.16)
project(ModuleDemo LANGUAGES CXX)

# Tell CMake where our custom Find modules live
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")

# Now CMake will search cmake/modules/FindMyCustomLib.cmake
find_package(MyCustomLib REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE ${MyCustomLib_LIBRARIES})
target_include_directories(app PRIVATE ${MyCustomLib_INCLUDE_DIRS})

CMake-Bundled Find Modules

CMake ships with Find modules for many common libraries. You can list them all:

# List all built-in Find modules (CMake 3.28+)
cmake --help-module-list | grep "^Find"

# Get help on a specific Find module
cmake --help-module FindOpenSSL
cmake --help-module FindThreads
cmake --help-module FindPython3

Some notable bundled Find modules: FindThreads, FindOpenSSL, FindZLIB, FindCurses, FindPython3, FindBoost, FindOpenGL, FindPkgConfig.

Writing a Find Module

When a library doesn't provide Config mode support and CMake doesn't bundle a Find module, you write your own. Here's the complete pattern:

find_path and find_library

Hands-On Custom Find Module
Writing FindLibUUID.cmake

Create a Find module for libuuid — a library that's commonly available on Linux but lacks CMake config files.

find_path find_library imported target
# cmake/modules/FindLibUUID.cmake — Complete Find module template
#[=======================================================================[.rst:
FindLibUUID
-----------

Find the UUID library (libuuid).

Imported Targets
^^^^^^^^^^^^^^^^

``LibUUID::LibUUID``
  The UUID library, if found.

Result Variables
^^^^^^^^^^^^^^^^

``LibUUID_FOUND``
  True if the library was found.
``LibUUID_INCLUDE_DIRS``
  Include directories for uuid.h.
``LibUUID_LIBRARIES``
  Libraries to link against.
``LibUUID_VERSION``
  Version string (if detectable).

#]=======================================================================]

include(FindPackageHandleStandardArgs)

# Find the header
find_path(LibUUID_INCLUDE_DIR
    NAMES uuid/uuid.h
    PATHS
        /usr/include
        /usr/local/include
        /opt/local/include
    DOC "Path to uuid/uuid.h"
)

# Find the library
find_library(LibUUID_LIBRARY
    NAMES uuid
    PATHS
        /usr/lib
        /usr/lib64
        /usr/local/lib
        /opt/local/lib
    DOC "Path to libuuid"
)

# Handle REQUIRED, QUIET, and version arguments
find_package_handle_standard_args(LibUUID
    REQUIRED_VARS LibUUID_LIBRARY LibUUID_INCLUDE_DIR
)

# Create imported target if found
if(LibUUID_FOUND AND NOT TARGET LibUUID::LibUUID)
    add_library(LibUUID::LibUUID UNKNOWN IMPORTED)
    set_target_properties(LibUUID::LibUUID PROPERTIES
        IMPORTED_LOCATION "${LibUUID_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${LibUUID_INCLUDE_DIR}"
    )
endif()

# Set output variables
mark_as_advanced(LibUUID_INCLUDE_DIR LibUUID_LIBRARY)
set(LibUUID_INCLUDE_DIRS ${LibUUID_INCLUDE_DIR})
set(LibUUID_LIBRARIES ${LibUUID_LIBRARY})

find_package_handle_standard_args

The find_package_handle_standard_args() function (from the FindPackageHandleStandardArgs module) handles all the boilerplate: checking required variables, printing status messages, respecting QUIET/REQUIRED, and setting <Package>_FOUND.

# CMakeLists.txt — Using our custom Find module
cmake_minimum_required(VERSION 3.16)
project(UUIDDemo LANGUAGES C)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")

find_package(LibUUID REQUIRED)

add_executable(uuid_gen main.c)
target_link_libraries(uuid_gen PRIVATE LibUUID::LibUUID)

pkg-config Integration

Many Unix libraries provide .pc files for pkg-config. CMake can leverage these through the FindPkgConfig module — useful as a fallback when no CMake config exists.

pkg_check_modules

# CMakeLists.txt — Using pkg-config through CMake
cmake_minimum_required(VERSION 3.16)
project(PkgConfigDemo LANGUAGES C)

# Load the PkgConfig module
find_package(PkgConfig REQUIRED)

# Check for libcurl using pkg-config
pkg_check_modules(CURL REQUIRED IMPORTED_TARGET libcurl)

add_executable(downloader main.c)
# IMPORTED_TARGET creates PkgConfig::CURL
target_link_libraries(downloader PRIVATE PkgConfig::CURL)
Note: The IMPORTED_TARGET keyword (CMake 3.6+) creates a proper imported target. Without it, you get only variables: CURL_LIBRARIES, CURL_INCLUDE_DIRS, CURL_CFLAGS, etc.

pkg_search_module

Use pkg_search_module when a library might be known by different names across distributions:

# CMakeLists.txt — Searching multiple pkg-config names
cmake_minimum_required(VERSION 3.16)
project(AudioDemo LANGUAGES C)

find_package(PkgConfig REQUIRED)

# Try multiple names — stops at first match
pkg_search_module(AUDIO REQUIRED IMPORTED_TARGET
    libpulse           # PulseAudio
    alsa               # ALSA
    jack               # JACK Audio
)

add_executable(audio_player main.c)
target_link_libraries(audio_player PRIVATE PkgConfig::AUDIO)

message(STATUS "Found audio library: ${AUDIO_MODULE_NAME}")

Finding Programs

CMake can also locate external programs needed during the build (code generators, documentation tools, etc.) using find_program():

# CMakeLists.txt — Finding and using external programs
cmake_minimum_required(VERSION 3.16)
project(GenDemo LANGUAGES CXX)

# Find protobuf compiler
find_program(PROTOC_EXECUTABLE
    NAMES protoc
    DOC "Protocol Buffers compiler"
)

if(NOT PROTOC_EXECUTABLE)
    message(FATAL_ERROR "protoc not found — install protobuf-compiler")
endif()

message(STATUS "Found protoc: ${PROTOC_EXECUTABLE}")

# Use in custom command
add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/message.pb.cc
           ${CMAKE_CURRENT_BINARY_DIR}/message.pb.h
    COMMAND ${PROTOC_EXECUTABLE}
        --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
        --proto_path=${CMAKE_CURRENT_SOURCE_DIR}
        ${CMAKE_CURRENT_SOURCE_DIR}/message.proto
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/message.proto
    COMMENT "Generating protobuf sources"
)

add_executable(app main.cpp ${CMAKE_CURRENT_BINARY_DIR}/message.pb.cc)
target_include_directories(app PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
Hands-On Finding Doxygen for Documentation
Conditionally Building Documentation

Use find_package(Doxygen) to conditionally add a documentation target only when Doxygen is installed.

find_package optional add_custom_target
# CMakeLists.txt — Optional Doxygen documentation
cmake_minimum_required(VERSION 3.16)
project(DocsDemo LANGUAGES CXX)

add_executable(app main.cpp)

# Doxygen is optional — don't use REQUIRED
find_package(Doxygen OPTIONAL_COMPONENTS dot)

if(DOXYGEN_FOUND)
    set(DOXYGEN_GENERATE_HTML YES)
    set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/docs)

    doxygen_add_docs(docs
        ${CMAKE_SOURCE_DIR}/src
        COMMENT "Generating API documentation"
    )
    message(STATUS "Doxygen found — 'cmake --build build --target docs' available")
else()
    message(STATUS "Doxygen not found — documentation target disabled")
endif()

Finding Python

CMake 3.12+ provides the modern FindPython3 module (replacing the older FindPythonInterp and FindPythonLibs):

# CMakeLists.txt — Finding Python 3 with components
cmake_minimum_required(VERSION 3.16)
project(PyDemo LANGUAGES CXX)

# Find Python interpreter, development headers, and NumPy
find_package(Python3 REQUIRED COMPONENTS Interpreter Development NumPy)

message(STATUS "Python3 executable: ${Python3_EXECUTABLE}")
message(STATUS "Python3 version: ${Python3_VERSION}")
message(STATUS "Python3 include dirs: ${Python3_INCLUDE_DIRS}")
message(STATUS "NumPy include dirs: ${Python3_NumPy_INCLUDE_DIRS}")

# Build a Python extension module
Python3_add_library(mymodule MODULE src/mymodule.cpp)
target_link_libraries(mymodule PRIVATE Python3::NumPy)
# CMakeLists.txt — Using Python as a build-time tool
cmake_minimum_required(VERSION 3.16)
project(PyToolDemo LANGUAGES CXX)

find_package(Python3 REQUIRED COMPONENTS Interpreter)

# Use Python in custom commands
add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated.h
    COMMAND Python3::Interpreter ${CMAKE_SOURCE_DIR}/scripts/generate.py
        --output ${CMAKE_BINARY_DIR}/generated.h
    DEPENDS ${CMAKE_SOURCE_DIR}/scripts/generate.py
    COMMENT "Running code generator"
)

add_executable(app main.cpp ${CMAKE_BINARY_DIR}/generated.h)
target_include_directories(app PRIVATE ${CMAKE_BINARY_DIR})

Common Libraries

Threads (FindThreads)

Threading is so fundamental that CMake provides a dedicated module. It handles platform differences (pthreads on Unix, Win32 threads on Windows):

# CMakeLists.txt — Finding and using threads
cmake_minimum_required(VERSION 3.16)
project(ThreadDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# FindThreads sets Threads::Threads imported target
find_package(Threads REQUIRED)

add_executable(worker main.cpp)
target_link_libraries(worker PRIVATE Threads::Threads)
# On Linux this adds -pthread; on Windows it's a no-op

ZLIB and OpenSSL

# CMakeLists.txt — ZLIB and OpenSSL together
cmake_minimum_required(VERSION 3.16)
project(SecureCompress LANGUAGES CXX)

find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)

add_executable(secure_archive main.cpp)
target_link_libraries(secure_archive PRIVATE
    ZLIB::ZLIB
    OpenSSL::SSL
    OpenSSL::Crypto
)

message(STATUS "ZLIB version: ${ZLIB_VERSION_STRING}")
message(STATUS "OpenSSL version: ${OPENSSL_VERSION}")

Curses

# CMakeLists.txt — Finding Curses/NCurses
cmake_minimum_required(VERSION 3.16)
project(TuiApp LANGUAGES C)

find_package(Curses REQUIRED)

add_executable(tui_app main.c)
target_include_directories(tui_app PRIVATE ${CURSES_INCLUDE_DIRS})
target_link_libraries(tui_app PRIVATE ${CURSES_LIBRARIES})
Hands-On Multi-Library Project
Combining Multiple Dependencies

Build a project that uses Threads, ZLIB, and OpenSSL together, demonstrating how imported targets compose cleanly.

find_package imported targets composition
# CMakeLists.txt — Composing multiple library dependencies
cmake_minimum_required(VERSION 3.16)
project(MultiDep LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

find_package(Threads REQUIRED)
find_package(ZLIB REQUIRED)
find_package(OpenSSL 1.1 REQUIRED)

add_executable(server
    src/main.cpp
    src/connection.cpp
    src/compression.cpp
)

# Each imported target carries its full usage requirements
target_link_libraries(server PRIVATE
    Threads::Threads
    ZLIB::ZLIB
    OpenSSL::SSL
    OpenSSL::Crypto
)

Troubleshooting find_package

When find_package() fails, CMake provides powerful debugging tools:

# Enable debug output for all find_package calls (CMake 3.17+)
cmake -B build --debug-find -S .

# Or enable for a specific package only
cmake -B build -DCMAKE_FIND_DEBUG_MODE=TRUE -S .

# Check what paths CMake is searching
cmake -B build --debug-find-pkg=OpenSSL -S .
# CMakeLists.txt — Debugging find_package in CMakeLists.txt
cmake_minimum_required(VERSION 3.17)
project(DebugFind LANGUAGES CXX)

# Enable debug output programmatically
set(CMAKE_FIND_DEBUG_MODE TRUE)

find_package(SomeLib REQUIRED)

set(CMAKE_FIND_DEBUG_MODE FALSE)
Common Errors and Fixes:
  • "Could not find <Package>" — Set CMAKE_PREFIX_PATH or <Package>_DIR to the install location
  • "Found unsuitable version" — Install a newer version or relax the version constraint
  • "Missing component" — Install the development package (e.g., libssl-dev not just libssl)
  • Config mode skipped — Library may only support Module mode; check with --debug-find
  • Wrong library found — Use CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=FALSE and set explicit paths
# Practical troubleshooting commands
# 1. Verify the library is installed
dpkg -L libssl-dev | grep cmake    # Debian/Ubuntu
rpm -ql openssl-devel | grep cmake  # RHEL/Fedora

# 2. Find .cmake config files manually
find /usr -name "OpenSSLConfig.cmake" 2>/dev/null
find /usr -name "FindOpenSSL.cmake" 2>/dev/null

# 3. Check pkg-config as fallback
pkg-config --modversion openssl
pkg-config --cflags --libs openssl

# 4. Clear CMake cache and retry
rm -rf build/CMakeCache.txt build/CMakeFiles/
cmake -B build -DCMAKE_PREFIX_PATH="/custom/path" -S .

Conclusion & Next Steps

Detecting external libraries is one of the most common tasks in CMake — and also one of the most frustrating when things go wrong. The key takeaways:

  • Config mode is preferred — it provides imported targets with full usage requirements
  • Module mode is the fallback — for libraries that don't ship CMake configuration
  • Always use imported targets (Package::Component) over raw variables
  • CMAKE_PREFIX_PATH is your primary tool for pointing CMake to non-standard installs
  • --debug-find is invaluable for troubleshooting search failures
  • Writing a Find module follows a standard pattern: find_path + find_library + find_package_handle_standard_args
Official Reference: See the find_package() documentation and the cmake-packages(7) manual for the complete specification.