Основные правила

Файл 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, добавив следующую строчку после указания стандарта:

if("${CMAKE_GENERATOR}" MATCHES "^Visual Studio")
  set(CMAKE_GENERATOR_PLATFORM "x64" CACHE STRING "" FORCE)
endif()

Настройка окружения

fPIC

Всегда включайте 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)    # включает moc
set(CMAKE_AUTORCC ON)    # включает rcc
set(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:

if(WIN32)
  target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 wsock32)
endif()

Также не забудьте добавить данное условие, если используете ASIO:

if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  target_compile_definitions(${PROJECT_NAME} PRIVATE -DBOOST_ASIO_HAS_STD_INVOKE_RESULT=1)
endif()

Оно необходимо для корректной совместимости со стандартной библиотекой llvm.

Protobuf
find_package(Protobuf REQUIRED)
# доступные цели линковки: 
# - protobuf::libprotobuf
# - protobuf::libprotoc
gRPC
find_package(Protobuf REQUIRED)
find_package(gRPC REQUIRED)
# доступные цели линковки:
# - protobuf::libprotobuf
# - protobuf::libprotoc
# - gRPC::grpc
# - gRPC::grpc++

Note

Для возможности собирать проект, использующий gRPC в режиме Debug, необходимо добавить следующие строчки:

if(Protobuf_VERSION VERSION_GREATER_EQUAL 4)
  target_link_libraries(${PROJECT_NAME} PUBLIC absl::log_internal_check_op)
endif()  
Системный интерпретатор Python
find_package(Python3 REQUIRED COMPONENTS Interpreter)

Если интерпретатор был найден, то он будет доступен через переменную ${Python3_EXECUTABLE}:

execute_process(
  COMMAND ${Python3_EXECUTABLE} --version
  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
Qt
find_package(QT NAMES Qt5 COMPONENTS Core) # Qt5 можно заменить на Qt6
find_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-eigen
add_library(math::eigen ALIAS ${PROJECT_NAME})