Файл CMakeLists.txt - это детальное описание того, как файлы исходного кода должны собираться в один или несколько бинарных файлов (т.е. исполняемых файлов или библиотек).
Как было сказано в главе “Основные команды”, любой правильно написанный CMake-файл должен содержать 3 секции:
Конфигурация
Сборка
Экспорт
Так как иногда в рамках разработки или специализированной поставки требуется использование CMake без связки с пакетным менеджером, то всегда должна присутствовать возможность равноценного использования вашего проекта как в виде подпроекта (subdirectory), так и в виде полноценной экспортированной библиотеки.
Так как одной из основных целевых ОС у нас является Astra Linux 1.7, версия CMake ограничена сверху версией 3.16.
Создание проекта
Ограничение версии CMake
Любой CMake-файл должен начинаться со следующей строчки:
cmake_minimum_required(VERSION 3.16)
Note
Как указано выше, версия CMake на Astra Linux ограничена сверху версией 3.16.
Однако, мы указываем только ограничение снизу, так как большинство разработчиков используют ОС, на которых версия CMake выше чем 3.16, и ограничение сверху вызовет проблемы с разработкой такого проекта.
Если вам все же нужно ограничить версию CMake сверху, то для этого используется следующий синтаксис:
cmake_minimum_required(VERSION 3.16...3.21)
Защита от повторного включения
Хорошей практикой является в любом файле помещать следующую строчку:
include_guard(GLOBAL)
Эта директива эквивалентна директиве препроцессора #pragma once в С++. Она позволяет одновременно существовать нескольким CMake-файлам с целями, имеющими одинаковые имена в дереве зависимостей проекта.
Tip
Если вы используете Astra Linux 1.6 и ниже, то данную директиву можно заменить на следующий код:
if(TARGET ${PROJECT_NAME}) return()endif()
Помещать этот код следует после объявления проекта, а не до, как в случае с include_guard(GLOBAL).
Проект
Перед объявлением проекта вам необходимо выбрать три имени:
Название проекта;
Название цели/целей сборки;
Пространство имен, которому эти цели принадлежат.
Они потребуются при объявлении целей сборки и в секции экспорта.
Например, если вы делаете модуль, вычисляющий собственные значения матриц для большой библиотеки, которая предоставляет различные математические функции и называется “math”, то хорошими именами будут:
math-eigen - название проекта;
eigen - название цели сборки;
math:: - пространство имен.
При правильном соблюдении этих правил ваши библиотеки всегда будут доступны извне под корректным именем вне зависимости, используете ли вы подпроекты, пакетный менеджер или экспорт в системные директории.
Объявление проекта выглядит так:
project(math-eigen # название проекта VERSION 1.0.0 # версия проекта DESCRIPTION "Eigen math library" # краткое описание проекта HOMEPAGE_URL "github.com/math/eigen" # домашняя страница (напр. репозиторий) LANGUAGES C CXX # используемые языки)set(PROJECT_NAMESPACE "math::")
Warning
В поле LANGUAGES должны находится только те языки, которые поддерживаются CMake.
Если ваш проект содержит код на JavaScript или QML, не нужно вставлять эти языки в это поле.
Поддерживаемые языки:
C, CXX
Fortran
CUDA
CSharp
Swift
OBJC, OBJCXX
ASM, ASM_NASM, ASM_MARMASM, ASM_MASM, ASM-ATT
HIP, ISPC
Important
Если вы также планируете использовать пакетный менеджер, то убедитесь, что версия вашего проекта в CMake и в файле пакетного менеджера (напр. в conanfile.py) совпадает.
Этот процесс можно автоматизировать. Подробнее: ((здесь будет ссылка)).
Стандарт С, С++
Всегда указывайте минимальный стандарт С++, который используется в проекте. Наиболее правильный способ сделать:
include(CMakePrintHelpers)...set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF POSITION_INDEPENDENT_CODE ON)cmake_print_properties(TARGETS ${PROJECT_NAME} PROPERTIES CXX_STANDARD CXX_STANDARD_REQUIRED CXX_EXTENSIONS POSITION_INDEPENDENT_CODE)
Так же корректным способом является:
if(NOT CMAKE_CXX_STANDARD) # не будет перезаписывать уже заданный стандарт
set(CMAKE_CXX_STANDARD 20) # c++20
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # выключает нестандартные расширения
# опциональный вывод стандарта в консоль
message(STATUS "[${PROJECT_NAME}] c++ standard: ${CMAKE_CXX_STANDARD}")
endif()
Также хорошей практикой будет позаботиться о пользователях MSVC, добавив следующую строчку после указания стандарта:
Всегда включайте fPIC, если не собираете приложение или shared-only библиотеку. Подробнее про fPIC можно прочитать здесь: ссылка.
Для включения используйте следующую встроенную опцию CMake:
set(CMAKE_POSITION_INDEPENDENT_CODE ON)# опциональный вывод информации об этом в консоль cmake:message(STATUS "[${PROJECT_NAME}] fpic status: ${CMAKE_POSITION_INDEPENDENT_CODE}")
MOC/RCC
В проектах, использующих Qt, необходимо явно включать препроцессинг с помощью утилит moc, rcc и, опционально, uic.
Наиболее правильный способ сделать:
include(CMakePrintHelpers)...set_target_properties(${PROJECT_NAME} PROPERTIES AUTORCC ON AUTOMOC ON AUTOUIC ON)cmake_print_properties(TARGETS ${PROJECT_NAME} PROPERTIES AUTORCC AUTOMOC AUTOUIC)
Так же корректным способом является:
set(CMAKE_AUTOMOC ON) # включает mocset(CMAKE_AUTORCC ON) # включает rccset(CMAKE_AUTOUIC ON) # включает uic (если используются qt widgets)
Поиск зависимостей
Для менеджмента зависимостей используются 4 способа:
Поиск уже установленных библиотек в системе;
Поиск уже установленных библиотек в реестре пакетного менеджера;
Включение сторонних библиотек в виде подпроектов с исходным кодом (subdirectory);
Включение сторонних библиотек с помощью модуля FetchContent.
Давайте рассмотрим плюсы и минусы каждого решения:
Возможность
Системные библиотеки
Пакетный менеджер
Подпроекты, сабмодули
FetchContent
Возможность использования собранных заранее библиотек
✅
✅
❌
❌
Быстрое обновление версий библиотеки в графе зависимостей
✅
✅
❌
❌
Несколько версий одного проекта в дереве зависимостей
✅
✅
❌
🔒
Удобство изменения исходного кода библиотеки
❌
🔒
✅
🔒
Независимость от операционной системы/платформы
❌
✅
✅
✅
Поиск через find_package
Системные библиотеки и библиотеки, установленные с помощью conan и vcpkg, необходимо искать и добавлять через команду find_package().
Например, чтобы найти библиотеку Google Test, необходимо написать директиву:
find_package(GTest REQUIRED)
Если библиотека установлена в системе, либо доступна через пакетный менеджер, то она будет найдена и доступна для линковки.
Ниже описаны примеры поиска популярных библиотек через find_package().
Boost
set(Boost_USE_RELEASE_LIBS ON) set(Boost_USE_MULTITHREADED ON)find_package(Boost 1.67.0 REQUIRED)# доступная цель линковки: ${Boost_LIBRARIES}
Note
Если вы используете Boost::ASIO, то не забудьте также произвести линковку с системными библиотеками потоков на ОС Windows:
find_package(QT NAMES Qt5 COMPONENTS Core) # Qt5 можно заменить на Qt6find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Quick Network)# доступные цели линковки:# - Qt${QT_VERSION_MAJOR}::Core# - Qt${QT_VERSION_MAJOR}::Quick# - Qt${QT_VERSION_MAJOR}::Network
Attention
Не используйте синтаксис find_package(QT NAMES Qt5 Qt6 ...).
Если ваш проект собирается как Qt5, так и Qt6, используйте опции CMake, чтобы определить, какая версия Qt будет использоваться. В противном случае это приведет к проблемам при линковке.
Предпочитайте использовать либо только Qt5, либо (если это возможно на вашей платформе) только Qt6.
Добавление зависимости через add_subdirectory
Если ваш проект достаточно большой (или если вы, к сожалению, используете сабмодули Git), то имеет смысл разделять его на подмодули. В таком случае, в проекте будет небольшое дерево зависимостей, построенное с помощью команды add_subdirectory.
Расширим пример с библиотекой math::eigen, используемый выше. Допустим, библиотека eigen имеет зависимость math::linalg, которая будет собираться в рамках этого же модуля math. Тогда хорошая структура папок будет выглядеть так:
.
└── math-eigen /
├── include
├── src
├── libs/
│ └── math-linalg/
│ ├── include
│ ├── src
│ └── CMakeLists.txt # CMakeLists подмодуля (отдельный проект)
└── CMakeLists.txt # CMakeLists основной библиотеки
Чтобы добавить эту библиотеку и иметь возможность линковки с ней, используется следующий синтаксис:
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/libs/math-linalg)# доступная цель линковки: math::linalg
Добавление цели сборки
Цели сборки делятся на 2 вида:
Исполняемый файл (executable)
Библиотека (library)
Чтобы добавить в качестве цели сборки исполняемый файл, используется простой синтаксис:
add_executable(${PROJECT_NAME})
Выполнение этой команды создаст исполняемый файл с именем проекта. Вы можете выбрать другое имя, если это требуется.
Чтобы добавить в качестве цели сборки библиотеку, нужно написать команду:
add_library(${PROJECT_NAME})
Вторым аргументом в команде add_library можно указать тип библиотеки:
STATIC - статическая библиотека (.a, .lib)
SHARED - динамическая библиотека (.dll, .so, .dylib)
INTERFACE - интерфейсная библиотека (например, не содержащая файлов исходного кода, кроме заголовков). Более подробно про интерфейсные библиотеки в следующих главах.
По умолчанию используется STATIC, если переменная BUILD_SHARED_LIBS равна OFF, и SHARED, если переменная BUILD_SHARED_LIBS равна ON.
Настоятельно рекомендуется указывать тип библиотеки явно только тогда, когда второй вариант сборки не реализован или не поддерживается.
Например, если ваша библиотека может быть собрана только в виде статической библиотеки:
add_library(${PROJECT_NAME} STATIC)
Для поддержки подпроектов мы также обязательно добавляем библиотеку-псевдоним для каждой публичной библиотеки, которую экспортируем. Например, при экспорте мы получили интерфейсную библиотеку math::eigen. В таком случае, псевдоним будет выглядеть так:
add_library(${PROJECT_NAME}) # math-eigenadd_library(math::eigen ALIAS ${PROJECT_NAME})