Основы 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:
Пример
# Эту команду необходимо разместить в самом начале CMakeLists.txt
# Пример: требуется CMake версии 3.10 или выше
cmake_minimum_required(VERSION 3.10)
Определите имя проекта и язык:
Пример
# Параметр языка необязателен. Распространённые значения: CXX (C++), C (язык C)
# После вызова project() переменные, такие как PROJECT_NAME, устанавливаются автоматически
project(MyProject CXX)
Добавьте исполняемый файл:
Пример
# Скомпилировать указанные исходные файлы, чтобы создать исполняемый файл
add_executable(MyApp main.cpp utils.cpp)
Добавьте библиотеку:
Пример
# STATIC: статическая библиотека (.a / .lib), встраиваемая непосредственно в исполняемый файл во время компиляции
# SHARED: динамическая библиотека (.so / .dll), загружаемая во время выполнения
# Если тип не указан, он определяется переменной BUILD_SHARED_LIBS
add_library(MyLib STATIC library.cpp)
Связать библиотеки с целью:
Пример
# Связать указанные библиотеки с целью (исполняемым файлом или другими библиотеками)
# Вы можете связать цели библиотек из собственного проекта или подключить внешние библиотеки
target_link_libraries(MyApp PRIVATE MyLib)
Задать переменные:
Пример
# Определить обычную переменную, на которую позже ссылаются через ${variable_name}
set(CMAKE_CXX_STANDARD 17)
# Задать несколько значений сразу (список)
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(MyApp ${SOURCES})
Укажите путь к файлу заголовка для цели:
Пример
# [BEFORE | AFTER]
# [SYSTEM]
# [PUBLIC | PRIVATE | INTERFACE]
# <path>...)
# PUBLIC: Текущая цель и цели, которые зависят от неё, могут использовать этот путь
# PRIVATE: Только текущая цель может использовать этот путь
# INTERFACE: Только другие цели, зависящие от текущей цели, могут использовать этот путь
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/include)
Задайте правила установки:
Пример
# [RUNTIME DESTINATION <путь установки исполняемого файла>]
# [LIBRARY DESTINATION <путь установки разделяемой библиотеки>]
# [ARCHIVE DESTINATION <путь установки статической библиотеки>]
# [INCLUDES DESTINATION <путь установки заголовочных файлов>])
# Определите места установки для каждого типа файлов при выполнении make install
install(TARGETS MyApp RUNTIME DESTINATION bin)
Условные операторы:
Пример
# Поддерживаемые 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()
Пользовательские команды:
Пример
# 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, в котором используются несколько из вышеприведённых директив.
Пример
# 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, чтобы новое значение по умолчанию вступило в силу.
Пример
# Доступные типы: 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/, и 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()Она ищет установленные в системе библиотеки и задаёт соответствующие переменные (такие как пути к заголовочным файлам, пути к файлам библиотек и имена целевых библиотек).
Пример
# 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.
Пример
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.