Основы CMake

CMake — это кроссплатформенный инструмент-генератор системы сборки, используемый для управления процессом компиляции проектов на C/C++.

Записывая конфигурационные файлы CMakeLists.txt, вы можете определить правила сборки проекта, зависимости и параметры компиляции. CMake автоматически создаёт соответствующие файлы сборки для платформы (например, Makefile в Unix или решение Visual Studio в Windows).


Файл CMakeLists.txt

CMakeLists.txt — это основной конфигурационный файл CMake. Каждый проект CMake требует как минимум один файл CMakeLists.txt.

CMake читает команды в файле, чтобы понять структуру проекта, список исходных файлов и требования к компиляции.

Базовый синтаксис

CMakeLists.txt состоит из ряда команд CMake, каждая в формате:Имя команды (список аргументов)。

Ниже приведены наиболее часто используемые команды и их примеры при сборке проектов CMake.

Укажите минимальную версию CMake:

Пример

# Синтаксис: cmake_minimum_required(VERSION <version>)
# Эту команду необходимо разместить в самом начале CMakeLists.txt
# Пример: требуется CMake версии 3.10 или выше
cmake_minimum_required(VERSION 3.10)

Определите имя проекта и язык:

Пример

# Синтаксис: project(<project-name> [<language>...])
# Параметр языка необязателен. Распространённые значения: CXX (C++), C (язык C)
# После вызова project() переменные, такие как PROJECT_NAME, устанавливаются автоматически
project(MyProject CXX)

Добавьте исполняемый файл:

Пример

# Синтаксис: add_executable(<target-name> <source-files>...)
# Скомпилировать указанные исходные файлы, чтобы создать исполняемый файл
add_executable(MyApp main.cpp utils.cpp)

Добавьте библиотеку:

Пример

# Синтаксис: add_library(<target-name> [STATIC | SHARED | MODULE] <source-files>...)
# STATIC: статическая библиотека (.a / .lib), встраиваемая непосредственно в исполняемый файл во время компиляции
# SHARED: динамическая библиотека (.so / .dll), загружаемая во время выполнения
# Если тип не указан, он определяется переменной BUILD_SHARED_LIBS
add_library(MyLib STATIC library.cpp)

Связать библиотеки с целью:

Пример

# Синтаксис: target_link_libraries(<target> <libraries>...)
# Связать указанные библиотеки с целью (исполняемым файлом или другими библиотеками)
# Вы можете связать цели библиотек из собственного проекта или подключить внешние библиотеки
target_link_libraries(MyApp PRIVATE MyLib)

Задать переменные:

Пример

# Синтаксис: set(<variable-name> <value>...)
# Определить обычную переменную, на которую позже ссылаются через ${variable_name}
set(CMAKE_CXX_STANDARD 17)

# Задать несколько значений сразу (список)
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(MyApp ${SOURCES})

Укажите путь к файлу заголовка для цели:

Пример

# Синтаксис: target_include_directories(<target>
# [BEFORE | AFTER]
# [SYSTEM]
# [PUBLIC | PRIVATE | INTERFACE]
# <path>...)
# PUBLIC: Текущая цель и цели, которые зависят от неё, могут использовать этот путь
# PRIVATE: Только текущая цель может использовать этот путь
# INTERFACE: Только другие цели, зависящие от текущей цели, могут использовать этот путь
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/include)

Задайте правила установки:

Пример

# Синтаксис: install(TARGETS <target>...
# [RUNTIME DESTINATION <путь установки исполняемого файла>]
# [LIBRARY DESTINATION <путь установки разделяемой библиотеки>]
# [ARCHIVE DESTINATION <путь установки статической библиотеки>]
# [INCLUDES DESTINATION <путь установки заголовочных файлов>])
# Определите места установки для каждого типа файлов при выполнении make install
install(TARGETS MyApp RUNTIME DESTINATION bin)

Условные операторы:

Пример

# Синтаксис: if(<condition>) ... elseif(<condition>) ... else() ... endif()
# Поддерживаемые CMake условные выражения включают: сравнение, логические операции, проверки переменных и т. д.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    # Включите отладочную информацию в режиме Debug
    message(STATUS "Текущий режим сборки: Debug")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
else()
    # Включить оптимизацию в режиме Release
    message(STATUS "Текущий режим сборки: Release")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
endif()

Пользовательские команды:

Пример

# Синтаксис: add_custom_command(
# TARGET <цель>
# PRE_BUILD | PRE_LINK | POST_BUILD
# COMMAND <команда> [аргументы...]
# [COMMENT <описание>]
# [VERBATIM])
# PRE_BUILD: Выполняется перед компиляцией
# PRE_LINK: Выполняется перед линковкой
# POST_BUILD: Выполняется после завершения сборки
add_custom_command(
    TARGET MyApp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo "Сборка завершена!"
    COMMENT "Вывести информацию о завершении сборки."
)

Полный пример

Ниже приведён полный пример CMakeLists.txt, в котором используются несколько из вышеприведённых директив.

Пример

# Путь к файлу: project root/CMakeLists.txt

# 1. Укажите минимальную версию CMake (должна быть указана в самом верху)
cmake_minimum_required(VERSION 3.10)

# 2. Определите имя проекта, версию и язык
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# 3. Установите стандарт C++ в C++17 и обеспечьте его обязательное использование
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 4. Добавить цель статической библиотеки
add_library(MyLib STATIC
    src/lib/core.cpp      # Реализация основной функциональности
    src/lib/helper.cpp    # Реализация вспомогательной функции
)

# 5. Добавить цель исполняемого файла
add_executable(MyApp
    src/main.cpp           # Точка входа программы
    src/utils.cpp          # Служебные функции
)

# 6. Укажите путь к публичным заголовочным файлам для цели библиотеки
# PUBLIC означает, что цели, использующие MyLib, также могут получить доступ к этому пути
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)

# 7. Укажите путь к приватным заголовочным файлам для исполняемого файла
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src)

# 8. Слинковать MyLib с MyApp
target_link_libraries(MyApp PRIVATE MyLib)

# 9. Задать правила установки
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(TARGETS MyLib ARCHIVE DESTINATION lib)

Типичные команды для сборки этого проекта следующие:

# 在项目根目录下创建 build 目录(推荐外部构建)
$ mkdir build && cd build

# 生成构建文件
$ cmake ..

# 编译项目
$ cmake --build .

# 安装到系统目录(可选)
$ cmake --install .

Переменные и кэш

CMake использует переменные для хранения и передачи информации о конфигурации. Переменные можно определять и использовать в CMakeLists.txt.

Переменные делятся на две категории:обычные переменныеикэш-переменные, и их области видимости и время жизни различаются.

Обычные переменные

Обычные переменные определяются в CMakeLists.txt, а их область видимости — текущий каталог и его подкаталоги.

Обычные переменные пересчитываются при каждом запуске CMake и не сохраняются.

Пример

# Определить строковую переменную
set(MY_APP_NAME "example-app")

# Определите переменную-список (разделитель — точка с запятой или пробелы)
set(SOURCE_FILES main.cpp utils.cpp helper.cpp)

# Использование переменных: ссылка через ${variable_name}
message(STATUS "Имя проекта: ${MY_APP_NAME}")

# Использование переменных-списков: перебор каждого исходного файла
foreach(FILE ${SOURCE_FILES})
    message(STATUS "Исходный файл: ${FILE}")
endforeach()

Кэш-переменные

Кэш-переменные хранятся в файле CMakeCache.txt и сохраняют свои значения между несколькими запусками CMake.

Кэш-переменные часто используются, чтобы позволить пользователям настраивать параметры сборки на этапе конфигурации CMake, например включать или отключать возможности или указывать пути установки.

Значения кэш-переменных сохраняются. Если изменить значение по умолчанию кэш-переменной в CMakeLists.txt, необходимо удалить CMakeCache.txt или повторно запустить cmake, чтобы новое значение по умолчанию вступило в силу.

Пример

# Синтаксис: set(<variable_name> <default_value> CACHE <type> <description>)
# Доступные типы: STRING, BOOL, PATH, FILEPATH
# Если переменная уже существует в CMakeCache.txt, set не перезапишет существующее значение

# Определите кэш-переменную типа STRING (изменяемый пользователем путь установки)
set(MY_INSTALL_PATH "/usr/local" CACHE PATH "Путь установки программы")

# Определите кэш-переменную типа BOOL (переключатель функции)
set(ENABLE_LOGGING ON CACHE BOOL "Включать ли функцию логирования")

# Используйте кэш-переменные так же, как любую другую переменную
if(ENABLE_LOGGING)
    message(STATUS "Функция логирования включена")
    add_definitions(-DENABLE_LOGGING)
endif()

Пользователи могут изменять переменные кэша с помощью параметров командной строки-D:

# 在 cmake 命令中通过 -D 修改缓存变量
$ cmake .. -DMY_INSTALL_PATH=/opt/example -DENABLE_LOGGING=OFF

Пути поиска заголовочных файлов

В CMake есть два способа указать пути, по которым компилятор ищет заголовочные файлы:include_directories()иtarget_include_directories()。

Понимание разницы между ними очень важно для написания чистых, поддерживаемых проектов CMake.

Сравнение include_directories и target_include_directories

Оба могут добавлять пути поиска заголовочных файлов для компилятора, но они различаются по области действия и уровню контроля.

Характеристика include_directories() target_include_directories()
Область действия Глобальная область действия: влияет на все цели в текущем каталоге и его подкаталогах Применяется только к указанной цели, не влияет на другие цели
Рекомендация Не рекомендуется, кроме случаев поддержки устаревших проектов CMake Рекомендуется использовать в первую очередь, соответствует лучшим практикам современного CMake
Связь с целью Не связана напрямую с конкретной целью, может случайно повлиять на другие цели Явно привязано к указанной цели, зависимости видны с первого взгляда
Сопровождаемость Плохо: легко приводит к загрязнению глобальных путей, проблемы сложно отлаживать Хорошо: пути привязаны к целям, логика ясна
Управление видимостью Невозможно точно контролировать область распространения Точно контролируется с помощью PUBLIC, PRIVATE, INTERFACE

Значение PUBLIC / PRIVATE / INTERFACE

target_include_directories()Ключевое преимущество в том, что распространение путей к заголовочным файлам можно точно контролировать с помощью ключевых слов видимости.

Ключевое слово Доступно для текущей цели Доступно для других целей, которые зависят от этой цели Типичный сценарий
PRIVATE Да нет Внутренние заголовочные файлы, необходимые только для собственной реализации
PUBLIC Да Да Публичные заголовочные файлы API библиотеки, необходимые как ей самой, так и потребителям
INTERFACE нет Да Библиотека только из заголовочных файлов

Пример

# Сценарий: заголовочные файлы MyLib находятся в каталоге include/
# Самой MyLib нужен include/, и MyApp, использующему MyLib, также нужен include/

# PUBLIC: и сама библиотека, и потребители могут получить доступ к этому пути
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)

# PRIVATE: доступ есть только у самой MyApp (заголовочные файлы внутренних служебных функций)
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src/internal)

# INTERFACE: MyLib — библиотека только из заголовочных файлов, её не нужно компилировать саму, она только передаёт путь потребителям
add_library(HeaderOnlyLib INTERFACE)
target_include_directories(HeaderOnlyLib INTERFACE ${PROJECT_SOURCE_DIR}/header_only)

Поиск библиотек и пакетов

Многие проекты зависят от внешних библиотек (таких как Boost, OpenCV). CMake предоставляет командыfind_package()для автоматического обнаружения и настройки этих внешних зависимостей.

Команда find_package

find_package()Она ищет установленные в системе библиотеки и задаёт соответствующие переменные (такие как пути к заголовочным файлам, пути к файлам библиотек и имена целевых библиотек).

Пример

# Базовое использование: поиск библиотеки Boost (без ограничения версии)
# REQUIRED означает прерывание конфигурации CMake, если ничего не найдено
find_package(Boost REQUIRED)

# Указать минимальную версию: требовать версию Boost не ниже 1.70
find_package(Boost 1.70 REQUIRED)

# Указать путь поиска: искать OpenCV в пользовательском пути
find_package(OpenCV REQUIRED PATHS /path/to/opencv)

Использование найденной библиотеки

find_package()В случае успеха обычно предоставляется следующее для использования:

Предоставленное содержимое Пример Описание
Импортированные цели Boost::Boost Рекомендуемый в современном CMake подход, используемый напрямую через target_link_libraries
Переменные пути к заголовочным файлам ${Boost_INCLUDE_DIRS} Устаревшее использование, применяется с include_directories
Переменные пути к файлам библиотек ${Boost_LIBRARY_DIRS} Устаревшее использование, применяется с link_directories

Настоятельно рекомендуется использовать методИмпортированные цели(например, Boost::Boost), а не вручную собирать пути к заголовочным и библиотечным файлам. Импортированные цели автоматически обрабатывают все параметры компиляции и компоновки, снижая количество ошибок конфигурации.

Пример использования сторонней библиотеки

Ниже приведён полный пример, показывающий, как подключить и использовать библиотеку Boost в проекте CMake.

Пример

# Путь к файлу: project root/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyBoostProject CXX)

# Установить стандарт C++
set(CMAKE_CXX_STANDARD 17)

# Найти библиотеку Boost; завершить с ошибкой, если не найдена
find_package(Boost REQUIRED)

# Добавить исполняемый файл
add_executable(MyApp main.cpp)

# Скомпоновать с Boost, используя импортированную цель
# Boost::Boost автоматически задаёт пути к заголовочным файлам и пути к библиотечным файлам
target_link_libraries(MyApp PRIVATE Boost::Boost)

Примечания

Предпочитайте семейство команд target_xxx.include_directories() и link_directories() — это глобальные команды, которые легко приводят к загрязнению путей. В больших проектах всегда используйте target_include_directories() и target_link_libraries(); они делают зависимости более понятными и простыми в сопровождении.

Всегда указывайте стандарт C++.Если CMAKE_CXX_STANDARD не задан, компилятор будет использовать стандарт по умолчанию (обычно C++98 или C++14, в зависимости от версии компилятора). Также рекомендуется установить CMAKE_CXX_STANDARD_REQUIRED в ON, чтобы сборка сразу завершалась ошибкой, если компилятор не поддерживает указанный стандарт.

Понимайте разницу между PUBLIC / PRIVATE / INTERFACE.Неправильное использование этих ключевых слов — самая распространённая ошибка новичков в CMake. Простой способ запомнить: нужно и самому, и потребителям → PUBLIC; нужно только самому → PRIVATE; нужно только потребителям (например, библиотекам, состоящим только из заголовочных файлов) → INTERFACE.

Значения по умолчанию кэш-переменных не обновляются автоматически.После изменения значения по умолчанию для set(... CACHE ...) в CMakeLists.txt существующий CMakeCache.txt не будет перезаписан. Нужно удалить CMakeCache.txt в каталоге сборки или весь каталог сборки и заново запустить cmake.

Другие расширения