Table of Contents

  1. The install() Command
  2. GNUInstallDirs Module
  3. Exporting Targets
  4. Config.cmake Packages
  5. GenerateExportHeader
  6. Installing Superbuilds
  7. Conclusion & Next Steps
Back to CMake Mastery Series

Part 21: Installing Your Project

June 4, 2026 Wasil Zafar 40 min read

Master CMake's install() command to deploy targets, headers, and configuration files. Generate relocatable Config.cmake packages so downstream projects can consume your library with find_package().

The install() Command

The install() command defines rules for installing files during the cmake --install step. It doesn't copy files during build — it generates install rules that execute later. This separation lets you build once and install to different prefixes without recompiling.

Key Insight: The install() command has multiple signatures: TARGETS for build artifacts, FILES for individual files, DIRECTORY for entire directory trees, PROGRAMS for executable scripts, and EXPORT for target export sets. Each signature maps to a different use case in the installation workflow.

Installing Targets

The most common install form places executables, libraries, and their headers in the correct directories:

cmake_minimum_required(VERSION 3.21)
project(MyLib VERSION 2.1.0 LANGUAGES CXX)

# Create a shared library
add_library(mylib SHARED
    src/mylib.cpp
    src/utils.cpp
)

# Set public headers
target_include_directories(mylib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

# Install the library target
install(TARGETS mylib
    EXPORT MyLibTargets
    RUNTIME DESTINATION bin          # DLLs on Windows
    LIBRARY DESTINATION lib          # .so on Linux
    ARCHIVE DESTINATION lib          # .a / .lib static archives
    INCLUDES DESTINATION include     # Sets interface include dirs
)

Each DESTINATION keyword specifies a subdirectory relative to CMAKE_INSTALL_PREFIX. The RUNTIME, LIBRARY, and ARCHIVE categories handle different artifact types automatically based on the platform (see install(TARGETS) docs).

Installing Files

For headers and other non-target files, use install(FILES):

# Install public headers preserving directory structure
install(FILES
    include/mylib/mylib.h
    include/mylib/utils.h
    include/mylib/types.h
    DESTINATION include/mylib
)

# Install a configuration file
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/mylib_config.h
    DESTINATION include/mylib
)

# Install with specific permissions
install(FILES scripts/setup.sh
    DESTINATION bin
    PERMISSIONS OWNER_EXECUTE OWNER_READ GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
)

Installing Directories

When you have an entire directory tree to install, install(DIRECTORY) is more efficient than listing individual files:

# Install all headers from include/ directory
install(DIRECTORY include/
    DESTINATION include
    FILES_MATCHING PATTERN "*.h"
)

# Install docs, excluding internal files
install(DIRECTORY docs/
    DESTINATION share/mylib/docs
    PATTERN "internal" EXCLUDE
    PATTERN "*.md" EXCLUDE
)

# Install with regex filtering
install(DIRECTORY assets/
    DESTINATION share/mylib/assets
    FILES_MATCHING REGEX ".*\\.(png|jpg|svg)$"
)
Install Destinations Layout
        flowchart TD
            A[CMAKE_INSTALL_PREFIX] --> B[bin/]
            A --> C[lib/]
            A --> D[include/]
            A --> E[share/]
            B --> B1[myapp executable]
            B --> B2[.dll files Windows]
            C --> C1[libmylib.so / .dylib]
            C --> C2[libmylib.a]
            C --> C3[cmake/MyLib/]
            C3 --> C3a[MyLibConfig.cmake]
            C3 --> C3b[MyLibTargets.cmake]
            C3 --> C3c[MyLibConfigVersion.cmake]
            D --> D1[mylib/]
            D1 --> D1a[mylib.h]
            D1 --> D1b[utils.h]
            E --> E1[mylib/docs/]
    

GNUInstallDirs Module

The GNUInstallDirs module provides standardized installation directory variables that respect platform conventions (e.g., lib64 on 64-bit Linux):

include(GNUInstallDirs)

# Now use CMAKE_INSTALL_* variables instead of hardcoded paths
install(TARGETS mylib
    EXPORT MyLibTargets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    FILES_MATCHING PATTERN "*.h"
)

# Install pkg-config file
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mylib.pc
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
)

Standard Directory Variables

VariableDefaultPurpose
CMAKE_INSTALL_BINDIRbinExecutables, DLLs
CMAKE_INSTALL_LIBDIRlib (or lib64)Libraries
CMAKE_INSTALL_INCLUDEDIRincludeHeaders
CMAKE_INSTALL_DATADIRshareRead-only data
CMAKE_INSTALL_MANDIRshare/manMan pages
CMAKE_INSTALL_DOCDIRshare/doc/PROJECTDocumentation
CMAKE_INSTALL_SYSCONFDIRetcConfiguration files

CMAKE_INSTALL_PREFIX

The CMAKE_INSTALL_PREFIX variable controls where everything gets installed. All DESTINATION paths are relative to this prefix:

# Set prefix at configure time
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/opt/myproject

# Or override at install time
cmake --install build --prefix /tmp/staging

# Default values by platform:
# Linux/macOS: /usr/local
# Windows: C:/Program Files/${PROJECT_NAME}
Best Practice: Always use GNUInstallDirs variables rather than hardcoding paths like "lib" or "bin". This ensures your project installs correctly on all platforms including distributions that use lib64 for 64-bit libraries (Fedora, openSUSE, etc.).

Exporting Targets

Installing binaries is only half the story. For libraries, you need to export CMake target information so downstream projects can use find_package() to discover and link against your library.

The EXPORT Keyword

The EXPORT keyword in install(TARGETS) associates targets with a named export set:

# Associate targets with an export set named "MyLibTargets"
install(TARGETS mylib mylib_utils
    EXPORT MyLibTargets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

# You can add to the same export set from subdirectories
# In src/CMakeLists.txt:
install(TARGETS mylib_core
    EXPORT MyLibTargets
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)

install(EXPORT)

The install(EXPORT) command generates and installs a CMake file that recreates the imported targets:

# Generate and install the targets file
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE MyLib::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyLib
)

# The generated MyLibTargets.cmake will contain:
# add_library(MyLib::mylib SHARED IMPORTED)
# set_target_properties(MyLib::mylib PROPERTIES
#     INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
#     IMPORTED_LOCATION "${_IMPORT_PREFIX}/lib/libmylib.so"
# )
Hands-On Basic Library Installation
Install and Verify a Library

Create a simple library, install it, then verify the install tree:

# Build and install to a staging directory
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=$PWD/install
cmake --build build
cmake --install build

# Verify the installation layout
find install/ -type f | sort
# Expected output:
# install/include/mylib/mylib.h
# install/lib/libmylib.so
# install/lib/cmake/MyLib/MyLibConfig.cmake
# install/lib/cmake/MyLib/MyLibTargets.cmake
# install/lib/cmake/MyLib/MyLibConfigVersion.cmake
install() staging verification

Generating Config.cmake Packages

A proper CMake package consists of three files that allow find_package(MyLib) to work seamlessly:

The Config File

Use CMakePackageConfigHelpers to generate a relocatable Config.cmake:

include(CMakePackageConfigHelpers)
include(GNUInstallDirs)

# Write the config file from a template
configure_package_config_file(
    cmake/MyLibConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyLib
)

# Install the generated config file
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyLib
)

The template file cmake/MyLibConfig.cmake.in:

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/MyLibTargets.cmake")

check_required_components(MyLib)

Package Version File

Use write_basic_package_version_file() to create a version compatibility file:

# Generate version file
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion
)

# Install the version file
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyLib
)

The COMPATIBILITY argument controls version matching:

Modefind_package(MyLib 2.1) matches
AnyNewerVersion2.1, 2.5, 3.0, 4.0
SameMajorVersion2.1, 2.5 (not 3.0)
SameMinorVersion2.1, 2.1.5 (not 2.2)
ExactVersion2.1 only
find_package() Resolution Flow
        flowchart TD
            A["find_package(MyLib 2.0 REQUIRED)"] --> B{Search CMAKE_PREFIX_PATH}
            B --> C[Find MyLibConfig.cmake]
            C --> D[Check MyLibConfigVersion.cmake]
            D -->|Version OK| E[Include MyLibTargets.cmake]
            D -->|Version Mismatch| F[Continue Searching]
            E --> G["MyLib::mylib target available"]
            G --> H["target_link_libraries(app PRIVATE MyLib::mylib)"]
            F --> I{More paths?}
            I -->|Yes| B
            I -->|No| J[FATAL_ERROR: Package not found]
    

GenerateExportHeader

For shared libraries, you need proper symbol visibility macros. The GenerateExportHeader module generates these automatically:

include(GenerateExportHeader)

add_library(mylib SHARED src/mylib.cpp)

# Generate mylib_export.h with visibility macros
generate_export_header(mylib
    EXPORT_FILE_NAME include/mylib/mylib_export.h
    EXPORT_MACRO_NAME MYLIB_API
)

# Make sure the generated header is accessible
target_include_directories(mylib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

# Install the generated export header
install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/include/mylib/mylib_export.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib
)

Then in your library headers:

#include "mylib/mylib_export.h"

class MYLIB_API MyClass {
public:
    void doSomething();
    static int getVersion();
};

// For functions
MYLIB_API void mylib_initialize();
MYLIB_API const char* mylib_version_string();
Hands-On Complete Package Installation
Full find_package()-Compatible Library

Combine all the pieces into a complete installable library that downstream projects consume with find_package():

cmake_minimum_required(VERSION 3.21)
project(MathUtils VERSION 1.2.0 LANGUAGES CXX)

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(GenerateExportHeader)

add_library(mathutils SHARED src/mathutils.cpp)
generate_export_header(mathutils
    EXPORT_FILE_NAME include/mathutils/export.h)

target_include_directories(mathutils PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>
    $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

target_compile_features(mathutils PUBLIC cxx_std_17)

# Install targets with export set
install(TARGETS mathutils EXPORT MathUtilsTargets
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})

# Install headers
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/mathutils/export.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mathutils)

# Generate and install package config
install(EXPORT MathUtilsTargets
    FILE MathUtilsTargets.cmake
    NAMESPACE MathUtils::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathUtils)

configure_package_config_file(
    cmake/MathUtilsConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfig.cmake
    INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathUtils)

write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfigVersion.cmake
    VERSION ${PROJECT_VERSION}
    COMPATIBILITY SameMajorVersion)

install(FILES
    ${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfig.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/MathUtilsConfigVersion.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MathUtils)
find_package Config.cmake export

Installing Superbuilds

Superbuilds (projects using ExternalProject_Add) have a unique installation challenge: the external projects install to their own prefix during build, and the top-level project must aggregate these installations:

# In the superbuild's top-level CMakeLists.txt
set(STAGED_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/staged)

ExternalProject_Add(dep_zlib
    URL https://zlib.net/zlib-1.3.tar.gz
    CMAKE_ARGS
        -DCMAKE_INSTALL_PREFIX=${STAGED_INSTALL_PREFIX}
        -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
)

ExternalProject_Add(my_project
    SOURCE_DIR ${CMAKE_SOURCE_DIR}/src
    CMAKE_ARGS
        -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}
        -DCMAKE_PREFIX_PATH=${STAGED_INSTALL_PREFIX}
    DEPENDS dep_zlib
)

# Install staged dependencies alongside your project
install(DIRECTORY ${STAGED_INSTALL_PREFIX}/
    DESTINATION .
    USE_SOURCE_PERMISSIONS
)
Official Reference: See the complete install() documentation for all signatures including SCRIPT, CODE, and IMPORTED_RUNTIME_ARTIFACTS forms.
Hands-On Consumer Project Test
Consume Your Installed Package

After installing your library, test it from a separate project:

# consumer/CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(Consumer LANGUAGES CXX)

find_package(MathUtils 1.0 REQUIRED)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE MathUtils::mathutils)
# Build the consumer, pointing to your install
cmake -S consumer -B consumer/build \
    -DCMAKE_PREFIX_PATH=$PWD/install
cmake --build consumer/build
./consumer/build/app
find_package consumer CMAKE_PREFIX_PATH

Conclusion & Next Steps

You now understand how to create professional CMake installations that follow platform conventions and work seamlessly with find_package(). Key takeaways:

  • install(TARGETS) handles executables and libraries with platform-aware destination splitting
  • GNUInstallDirs provides portable directory variables (never hardcode "lib" or "bin")
  • install(EXPORT) + Config.cmake + ConfigVersion.cmake = complete package for downstream
  • GenerateExportHeader manages symbol visibility for shared libraries
  • Always use generator expressions (BUILD_INTERFACE/INSTALL_INTERFACE) for include directories