Вступление
Популярность CMake растёт. Многие крупные проекты переходят с собственных инструментов для сборки на CMake. Проект Conan предлагает интеграцию с CMake для управления зависимостями.
Разработчики CMake активно развивают инструмент и добавляю новые функции, решающие общие проблемы, возникающие при сборке проектов. Переход с CMake 2 на CMake 3 был достаточно болезнен. Документация не покрывает все аспекты использования. Функционал крайне обширен, а возникающие трудности различаются от проекта к проекту. В статье я расскажу о инструментах, которые предлагает CMake 3.10 и выше. В каждой новой версии появляются новые детали или улучшаются старые. Об актуальном состоянии лучше проанализировать Changelog, так как многие улучшения последних версий весьма специфичны для отдельных проектов, как например улучшение поддержки Cuda компиляторов. Я же сфокусируюсь на общих понятиях, которые помогут организовать проект на С++ оптимальным образом с точки зрения использования CMake как основной системы сборки.
CMake предлагает широкий набор инструментов. Чтобы не потеряться в определениях, в статье сперва будут определены сущности, через которые будут объяснены конкретные примеры. Названия сущностей я продублирую на английском. Некоторые термины не имеют однозначного перевода в русский язык.
Обзор демонстрационного проекта
Примеры проблем при сборке проекта и рекомендуемые CMake пути решения взяты из моего проекта библиотеки, использующей нейронные сети для определения синхронизации между речью и изображением.
Проект написан на С++, предполагает кросс-платформенность. Содержит переиспользуемые компоненты и зависит от внешних библиотек. Предполагаемая область использования - интеграция в конечное приложение, выполняющее анализ видеопотока, поэтому проект также содержит демонстрационное приложение с примером использования API. Демо-приложение поставляется в исходных кодах вместе с двоичной библиотекой.
Структура файлов и папок проекта с именем 'neuon' следующая:
neuon
|-sources
|-res
|-src
|-CMakeLists.txt
|-headers
|-include
|-CMakeLists.txt
|-examples
|-res
|-src
|-CMakeLists.txt
|-cmake
|-FindTensorflow.cmake
|-FindJsonCpp.cmake
|-res
|-neoun-config.cmake.in
|-CMakeLists.txt
Используемые сущности и понятия
Приложения в комплекте поставки (CMake, CPack, CTest, CDash)
Предопределенные и ожидаемые файлы и их назначение
Модули, конфигурации
Объектная модель скриптов - цели, свойства
Свойства, переменные, аргументы и параметры.
Приложения в комплекте поставки (CMake, CPack, CTest, CDash)
CMake поставляет в комплекте с несколькими приложениями, предназначенными для дополнения процесса сборки проекта. За саму сборку отвечает cmake, который работает с файлом, описывающим проект. Использование cmake состоит из нескольких шагов: шаг генерации(generation), шаг сборки(build) и шаг установки(install).
Первым этапом при сборке проекта необходимо сгенерировать скрипты сборки, используя описание проекта(CMakeLists.txt) на языке CMake. Результатом генерации является скрипт или набор файлов, необходимый для запуска нижележащей системы сборки(например Makefile или VIsual Studio Solution). CMake не выполняет запуск компилятора сам, хотя может быть использован как прокси для обобщения вызовов нижележащих инструментов.
Следующим шагом происходит непосредственно сборка проекта из исходного кода. Результатом являются бинарные файлы и другие артефакты, являющиеся частью конечного продукта для потребителя. Все артефакты, несмотря на свою доступность, находятся в специальном состоянии, контролируемом CMake и как правило не пригодны к перемещению или распространению, однако, могут быть использованы для отладки локально.
Финальным шагом использования СMake является шаг установки. Установка подразумевает пересборку либо по необходимости перелинковку бинарных артефактов с целью сделать их пригодными к использованию на целевой системе. Как правило, при установке так же происходит перемещение артефактов в желаемые расположения и создании желаемой раскладки по директориям. Установка на этом этапе не имеет ничего общего с установкой дистрибутива или распаковки архива, но при некорректной конфигурации сборки может установить свежесобранные артефакты в локальную систему. При использовании шаг установки не выполняется вручную.
Базовый функционал CMake не заканчивается на консольном приложении. CMake так же предоставляет графический интерфейс для управления этапами генерации и сборки - cmake-gui. А так же предлагает широкий набор модулей(CMake modules) для использования в файлах описания проекта. Посторонние проекты могут предлагать свои конфигурации(Library Configs) для упрощения использования в связке с CMake.
CMake крайне гибкий и расширяемый инструмент. И цена за это - его перегруженность. Если проекту нужен какой-то функционал при сборке - имеет смысл изучить, что предлагает СMake. Может быть такая же проблема уже была решена и решение прошло проверку сообществом.
Приложение CTest расширяет возможности по сборке путем предоставления единого интерфейса по взаимодействию с тестами. Если проект содержит тесты, ctest играет роль единого запускающего механизма, формирующего отчет по запуску. Для использования CTest как универсального исполнителя каждый тест должен быть зарегистрирован. Согласно его регистрации, в финальный отчет попадет имя и результат выполнения теста. CTest так же обеспечивает интеграцию с CDash - панелью общего доступа к результатам запусков тестов, с управлением запусками, группировкой тестов и прочим функционалом, использующимся в автоматизированный конвейерах сборки. (CI/CD Dashboard).
CPack - это инструмент упаковки скомпилированного проекта в платформо-зависимые пакеты и установщики. CPack c одной стороны универсален для создания установщиков целевого формата, с другой стороны зависит от системы, где запускается, так как полагается на системные инструменты для создания установщика. Единый формат командной строки для генерации NSIS установщика для Windows, DEB пакета для Ubuntu и RPM пакета для Centos не подразумевает генерацию RPM при запуске на Ubuntu или Windows. Основным преимуществом CPack перед отдельными упаковщиками является то, что вся конфигурация установщика находится рядом с проектом, и использует те же механизмы, что и сам CMake. Для добавления нового формата в проект достаточно доопределить формато-специфичные переменные и CPack сделает остальную работу.
Предопределенные и ожидаемые файлы и их назначение.
CMake широко использует файловую систему и управляет многими папками и файлами. CMake поддерживает сборку вне расположения проекта. Местоположение проекта называется исходной папкой( Source Directory). При генерации CMake сохраняет файлы в папку сборки (Build Directory). В некоторых контекстах папка сборки также именуется папкой бинарных артефактов(Binary Directory), как противопоставление исходной папке. При выполнение шага установки начинает фигурировать директория установки(Install directory). Эти местоположения могут указывать в одно место, или в разные - это контролируется независимо и доступно при использовании.
Помимо непосредственно файловой системы, CMake использует механизм префиксов для гибкой настройки относительных путей. Префикс установки((CMAKE_INSTALL_PREFIX
)) определяет префикс, который будет использоваться для всех относительных путей, используемых проектом после установки. . Префикс установки и директория установки могут быть одинаковы. Но могут и различаться, что используется для кросс-компиляции или создания переносимых артефактов, не привязанных к конкретному местоположению. Префикс-путь (CMAKE_PREFIX_PATH
) имеет особенный смысл для шага генерации. Как гласит документация, это список путей, которые СМаке использует в дополнение к своему местоположению как корневую папку для поиска модулей и расширений. При необходимости указать дополнительные места, используется префикс-путь. Он не влияет на результат сборки, однако крайне важен для поиска зависимостей, интеграции со сторонними библиотеками и использовании модулей, не входящих в комплект поставки CMake либо являющимися частью проекта.
CMake использует некоторые предопределённые имена или части имени файла, наделяя эти файлы особенным смыслом.
Поддержка CMake в проектах обеспечивается CMakeLists.txt файлом. Обычно расположенным в корне проекта. Этих файлов может быть несколько и они могут быть включены друг в друга различными способами. Основная идея этого файла - он предоставляет главную точку входа для CMake и описание проекта начинается с него.
В папке сборки после генерации можно найти файл CMakeCache.txt. Этот файл содержит полное представление всего проекта, которое CMake смог разобрать и сгенерировать. Этот файл может быть отредактирован вручную, для изменения некоторых параметров и переменных. Однако, при следующей генерации изменения могут быть утеряны, если параметры и аргументы запуска CMake утилиты изменились.
Файлы, имена которых регистро-зависимо оканчиваются на Config.cmake
или -config.cmake
, являются файлами CMake совместимой конфигурации библиотек и зависимостей. (CMake config files). Эти файлы как правило распространяются вместе с библиотеками и обеспечивают интеграции библиотек в проекты использующие CMake для сборки.
Файлы, имена который выглядят как Find*.cmake
, содержать CMake модули(CMake modules). Модули расширяют функционал CMake, и используются в CMakeLists.txt для выполнения рутинных задач. Модули следует использовать как фреймворк или библиотеку функции при написании своего CMakeLists.txt
Другие файлы с расширением .cmake
предполагают произвольное содержимое написанное с использованием CMake скриптового языка. Проекты включают такие файлы в целях переиспользования частей скриптов.
Иногда в CMake проектах могут встретиться файлы с расширением .in
или .in.in
Таким образом могут именоваться шаблоны файлов, которые инстанцируются CMake при генерации. Инстанцированные файлы, затем используются проектом как текстовые артефакты, либо как файлы, актуальные в момент генерации и сборки. Например они могут содержать в себе версии и дату сборки бинарных артефактов, или шаблон CMake конфигурации, которая будет распространятся с артефактами позже.
Модули, конфигурации
CMake Модули и CMake конфигурации содержат в себе код на CMake скрипте. Несмотря на схожесть, это разные по назначению сущности. Модули, обычно расширяют поведение CMake, предоставляя функции, макросы и алгоритмы для использования в CMakeLists.txt, поставляются в комплекте с CMake, именуются как Find*.cmake и располагаются в CMAKE_PREFIX_PATH
. Модули не привязаны к конкретному проекту и должны быть проектно-независимы. Проект может дополнять список модулей по необходимости при генерации или при использовании, но рекомендуется публиковать модули с целью включить их в состав дистрибутива CMake. Модули играют роль "Стандартной библиотеки CMake". Модули поддерживаются и разрабатываются мейнтейнерами CMake.
Конфигурации же напротив, неотъемлемая часть проекта, поставляются вместе с заголовочными файлами и библиотеками импорта, и обеспечивают интеграцию библиотек в другие проекты. Конфигурация проекта не используется при сборке проекта. Конфигурация является результатом сборки проекта и входит в установщик, поддерживаются и разрабатываются авторами проекта.
Конфигурации не должны располагаться в CMAKE_PREFIX_PATH
и должны следовать соглашению о местоположении и именовании, описанном в официальной документации. (https://cmake.org/cmake/help/latest/command/find_package.html#full-signature-and-config-mode)
Конфигурации описывают зависимости проекта, с целью облегчить его использование. Конфигурации крайне чувствительны к своей реализации. Следование практикам и рекомендациям поможет делать конфигурации, которые облегчают интеграции проекта заказчикам и пользователям.
Объектная модель скриптов - цели, свойства
Начиная с версии 3 CMake сместил парадигму своих скриптов с процедурно-ориентированной на объекто-ориентированную. В версии 2, описание проекта содержало набор вызовов функций, которые выполняли настройку свойств, необходимых для сборки и использования. В версии 3, на смену переменным и свойствам пришли цели. Цель(target) в контексте CMake, это некоторая сущность, над которой можно выполнять операции, изменять ее свойства, обеспечивать ее доступность и готовность. Целями могут являются, но не ограничиваются, бинарные артефакты и исполняемые файлы проекта, заголовочные файлы проекта, дополнительные файлы, создаваемые при генерации, зависимости целей, внешние библиотеки или файлы и т.д.
У целей есть свойства(properties), которые могут быть доступны как для чтения(все из них), так и для записи(многие), с целью тонкой настройки и обеспечения желаемого результата. Свойства - это именованные поля целей. Управляются CMake на основе дефолтных значений параметров и аргументов и\или окружения сборки, а так же управляются сами проектом, в зависимости от назначения и желаемого результата.
Свойства, переменные, аргументы и параметры.
CMake обеспечивает гибкость настройки и использования предоставляя разные способы контроля и управления своим поведением.
Цели можно контролировать их свойствами. Командная строка при запуске CMake утилит может содержать аргументы командной строки(arguments), передаваемые напрямую в утилиту общепринятым способом --argument
или -f lag
. В командной строке так же могут встречаться параметры(parameters). Параметры, передаваемые в командной строке через -DNAME=VALUE
или -DNAME:TYPE=VALUE
преобразуются в переменные(variables) с тем же именем в теле скрипта. Так же параметрами могут являться переменные окружения или некоторые вхождения CMake переменных в файле CMakeCache.txt. Параметры от переменных в CMakeCache.txt практически ничем не отличаются.
CMake переменные - это переменные в общепринятом смысле слова при написании скриптов. Переменные могут быть объявлены или не быть объявлены. Могут иметь значение или не иметь значение, или иметь пустое значение. Помимо переменных унаследованных от параметров(или значений по умолчанию), в скриптах можно объявлять и использовать свои собственные переменные.
Таким образом, управление CMake утилитами выполняется через аргументы командной строки, параметры запуска, переменными в скриптах, и свойствами целей. Аргументы не оказывают влияние на состояние скриптов, параметры превращаются в переменные с тем же именем, свойства целей имеют предустановленное документированное имя и могут быть установлены в произвольные значения через вызов соответствующих функций.
Примеры задач и использование доступных инструментов
У меня в руках проект, который использует CMake. Что с ним делать?
Сгенерировать сборочные файлы в папке сборки, запустить сборку, опционально запустить установку.
cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release -Sneuon -Bcmake-release-build
cmake --build cmake-release-build -- -j8
cmake --build cmake-release-build --target install
В этом примере мы собираем релизную сборку проекта в отдельной директории. Релиз в данной ситуации подразумевает включение оптимизаций и удаления отладочной информации из бинарного артефакта. CMake имеет 5 известных значений CMAKE_BUILD_TYPE
параметра: Debug, Release, RelWithDebInfo, MinSizeRel и None - если не установить конкретное значение. Так же мы явно указываем CMAKE_INSTALL_PREFIX
/usr/local является значением по умолчанию для Unix систем, а так как запись в эту директорию требует прав суперпользователя, то последняя команда установки вернет ошибку так как не сможет записать файлы по назначению. Следует либо запустить её с правами суперпользователя(что крайне не рекомендуется, если целевая платформа имеет пакетные менеджеры), либо сменить префикс установки в место, не требующее прав суперпользователя, либо не устанавливать проект в систему, либо установить его с использованием переназначения назначения. Для make как нижележащей системы можно использовать DESTDIR: cmake --build cmake-release-build --target install -- DESTDIR=$HOME/neuon
Переназначение назначения зависит от собственно системы сборки и не каждая из них умеет такое делать.
Пример сборки под Linux из реального проекта:
# Локальная сборка зависимости проекта и установка в отдельную директорию,
# которая будет использоваться при сборке основного проекта.CMake автоматически собирает проект
# при установке по необходимости.
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=On -DCMAKE_INSTALL_PREFIX=/opt/neuon -DCMAKE_BUILD_TYPE=Release -s googletest -Bgoogletest-build
cmake --build googletest-build --target install -- DESTDIR=$PWD/deps -j8
# Сборка и упаковка в TGZ основного проекта в 8 потоков make.
cmake -DCMAKE_PREFIX_PATH=$PWD/deps -DCMAKE_INSTALL_PREFIX=/opt/neuon -DCPACK_SET_DEST_DIR=On -DCMAKE_BUILD_TYPE=Release -Dversion=0.0.0 -S neuon -B neuon-build
cmake --build neuon-build -- -j8
cd neuon-build && cpack -G "TGZ"
# CPack на конец 2020 не поддерживает -B аргумент. Необходимо запускать в папке сборки
Для генерации под Windows, используя Visual Studio:
cmake -G "Visual Studio 16 2019" -A x64 -T host=x64 -DBUILD_SHARED_LIBS=On -DCMAKE_PREFIX_PATH=d:\msvc.env\ -DCMAKE_INSTALL_PREFIX=/neuon -DFFMPEG_ROOT=d:\msvc.env\ffmpeg -S neuon -B neuon-build
Как запустить тесты с CTest?
После генерации запустить CTest в папке сборки.
ctest .
По умолчанию, CTest не выводит ход выполнения тестов. Аргументы вызова помогут достигнуть желаемого поведения.
Как пользоваться целями и свойствами?
add_library(neuon
${CMAKE_CURRENT_BINARY_DIR}/generated/version.cpp
${CMAKE_CURRENT_BINARY_DIR}/generated/birthday.cpp
src/license.cpp
src/model.cpp
src/tensorflow_api.cpp
src/tensorflow_dynamic.cpp
src/tensorflow_static.cpp
src/neuon_c.cpp
src/neuon_cxx.cpp
src/demo.cpp
src/configuration.cpp
src/speech_detection.cpp
src/face_detection.cpp
src/log.cpp
)
add_library(neuon::neuon ALIAS neuon)
target_link_libraries(neuon PRIVATE Threads::Threads JsonCpp::JsonCpp Boost::headers Aquila::Aquila dlib::dlib Tensorflow::Tensorflow Boost::filesystem spdlog::spdlog PUBLIC neuon::headers )
target_compile_features(neuon PRIVATE cxx_std_17)
set_target_properties(neuon PROPERTIES CXX_EXTENSIONS OFF)
set_target_properties(neuon PROPERTIES INSTALL_RPATH "$ORIGIN")
set_target_properties(neuon PROPERTIES C_VISIBILITY_PRESET hidden)
set_target_properties(neuon PROPERTIES CXX_VISIBILITY_PRESET hidden)
set_target_properties(neuon PROPERTIES VISIBILITY_INLINES_HIDDEN On)
target_compile_definitions(neuon PRIVATE BOOST_UBLAS_INLINE= NEUON_ORIGINAL_BUILD )
target_include_directories(neuon
PRIVATE src/
../depends/sdk/src
${CMAKE_CURRENT_BINARY_DIR}/generated
)
install(TARGETS neuon EXPORT neuon-library
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT devel
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT devel NAMELINK_COMPONENT devel
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
OBJECTS DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT devel
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(EXPORT neuon-library NAMESPACE neuon:: DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/neuon/cmake FILE neuon-targets.cmake COMPONENT devel)
install(FILES res/model.pb res/normale.json res/shape_predictor_68_face_landmarks.dat DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/neuon/res COMPONENT runtime)
Добавляем цель сборки - библиотеку. Не указываем тип библиотеки - разделяемая или статическая, оставляя на усмотрение собирающего. Наш конвейер сборки может собирать два варианта двумя разными вызовами генерации. Затем добавляем цель-псевдоним, это позволяет использовать пространство имен в CMake конфигурации проекта для более выразительного именования зависимостей. Цель-псевдоним используется тут же в проекте демо-приложением, которое при этом умеет использовать и файл конфигурации. Использование пространства имен в файле конфигурации поместит все цели в него. Без псевдонима, демо-приложение должно будет явно отличать связываться с целью родительского проекта, или с целью из конфигурации.
Указываем список библиотек и других зависимостей, с которыми необходимо линковать нашу библиотеку, и выставляем различные свойства, влияющие на параметры компиляции. В последней части, указываем как именно мы хотим устанавливать нашу библиотеку - установка цели описывается через введение экспортируемой сущности, для которой прописываются свойства, такие как папка назначения в зависимости от конкретного типа файла или типа цели или компонент для разделения установщиков на отдельные пакеты или наборы устанавливаемых файлов. Затем указывается установка экспортируемой сущности с указанием пространства имен для используемых целей.
И напоследок несколько файлов устанавливаются как есть. Это ресурсы, которые распространяются как часть проекта.
Как использовать файлы конфигурации?
Начиная с CMake 3.0 рекомендуется в файлах конфигурации объявлять цели, которые могут быть использованы потребителем. До CMake 3.0 файлы конфигурации объявляли несколько переменных, именованных по определенным правилам и эти переменные становились доступны потребителю. На настоящий момент, некоторые проект до сих пор используют только старый подход, некоторые проекты используют старый подход как дополнение к новому. Иногда появляются инициативы завернуть старые конфигурации в новые или в CMake модули. В этом примере мы уделяем внимание новому подходу, как рекомендуемому.
find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale swresample)
if(NOT TARGET neuon::neuon)
find_package(Neuon REQUIRED COMPONENTS headers neuon)
endif()
add_executable(neuon-sample
${CMAKE_CURRENT_BINARY_DIR}/generated/version.cpp
${CMAKE_CURRENT_BINARY_DIR}/generated/birthday.cpp
src/neuon_sample.cpp
depends/sdk/src/extraction.cpp
depends/sdk/src/options.cpp
depends/sdk/src/source.cpp
depends/sdk/src/demuxer.cpp
depends/sdk/src/decoder.cpp
depends/sdk/src/interruption.cpp
depends/sdk/src/track_adapter.cpp
depends/sdk/src/access_unit_adapter.cpp
depends/sdk/src/resample.cpp
)
target_link_libraries(neuon-sample PRIVATE spdlog::spdlog Threads::Threads Boost::program_options FFMPEG::avcodec FFMPEG::avformat FFMPEG::swscale FFMPEG::swresample neuon::neuon)
target_compile_features(neuon-sample PRIVATE cxx_std_11)
set_target_properties(neuon-sample PROPERTIES CXX_EXTENSIONS OFF)
target_include_directories(neuon-sample
PRIVATE src/
depends/sdk/src
${CMAKE_CURRENT_BINARY_DIR}/generated
)
find_package ищет файлы конфигурации, руководствуясь предопределенными правилами разрешения путей. Если этот CMakeLists.txt не включен в основной проект, то neuon::neuon цель будет недоступна и нам требуется явно подключить библиотеку штатным способом. В обратном случае - цель-псевдоним обеспечит нам идентичный функционал и наше приложение будет слинковано с библиотекой из той же директории сборки.
Мы заранее ввели цель-псевдоним в основном проекте библиотеки для универсальности нашего CMakeLists.txt в проекте демо-приложения. Теперь при сборке нашего демо-приложения как части сборки проекта - будет использоваться цель-псевдоним, а при сборке пользователем наша библиотека будет доступна через имя определенное в конфигурации, дополненное пространством имён.
Как добавить свои тесты в запуск CTest?
enable_testing()
find_package(GTest 1.8 CONFIG REQUIRED COMPONENTS gtest gmock gmock_main )
include(GoogleTest)
add_executable(ut_curl test/ut_curl.cpp src/curl.cpp)
target_link_libraries(ut_curl PRIVATE GTest::gmock GTest::gmock_main CURL::libcurl)
target_include_directories(ut_curl PRIVATE src/)
add_test(test_of_curl_wrapper ut_curl)
enable_testing() указывает CMake, что планируется использование CTest. add_test() регистрирует исполняемый файл, собираемый в проекте как один из тестов для запуска. Тестом может быть любая исполняемая сущность - приложение, скрипт, сторонний инструмент. CTest опирается на код возврата для определения пройден тест или нет и формирует соответственный отчет.
enable_testing()
find_package(GTest 1.8 CONFIG REQUIRED COMPONENTS gtest gmock gmock_main )
include(GoogleTest)
add_executable(ut_curl test/ut_curl.cpp src/curl.cpp)
target_link_libraries(ut_curl PRIVATE GTest::gmock GTest::gmock_main CURL::libcurl)
target_include_directories(ut_curl PRIVATE src/)
gtest_discover_tests(ut_curl)
CMake предлагает готовый модуль для работы с Google Test Framework. Если ваши тесты используют Googletest, то в исполняемом файле обычно множество юнит-тестов. Регистрация штатным способом не даст полной картины запуска юнит-тестов, так как с точки зрения CMake - одно приложение, один тест. include(GoogleTest) подключает стандартный модуль, который содержит функцию gtest_discover_tests
, которая регистрирует все тесты из собранного тестового приложения как отдельные тесты в CTest. Отчет становится гораздо более информативным.
Как конфигурировать CPack?
include(CPackComponent)
cpack_add_component(runtime)
cpack_add_component(devel DEPENDS runtime)
cpack_add_component(sample DEPENDS runtime devel)
set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY 0)
set(CPACK_PACKAGE_CONTACT "info@example.com")
set(CPACK_ARCHIVE_COMPONENT_INSTALL On)
set(CPACK_RPM_COMPONENT_INSTALL On)
set(CPACK_RPM_PACKAGE_AUTOREQ On)
set(CPACK_RPM_PACKAGE_AUTOREQPROV Off)
set(CPACK_RPM_DEVEL_PACKAGE_REQUIRES "blas, lapack, atlas, jsoncpp-devel")
set(CPACK_RPM_SAMPLE_PACKAGE_REQUIRES "ffmpeg-devel, jsoncpp-devel")
set(CPACK_DEB_COMPONENT_INSTALL On)
set(CPACK_DEBIAN_DEVEL_PACKAGE_DEPENDS "libopenblas-base, libblas3, libjsoncpp-dev, libjsoncpp1, libopenblas-dev")
set(CPACK_DEBIAN_SAMPLE_PACKAGE_DEPENDS "libavformat-dev, libavcodec-dev, libswscale-dev, libswresample-dev, libavutil-dev, libopenblas-dev")
include(CPack)
Подключив требуемые модули расширяющие CPack, необходимо выставить значения задокументированных переменных для каждого конкретного формата установщика или пакета. Некоторые значения берутся из переменных и параметров CMake, некоторые могут быть заполнены автоматически при выполнение упаковки. После выставления переменных следует подключить собственно модуль CPack через include(CPack).
Как написать конфигурацию для своего проекта?
По созданию своего файла конфигурации есть статья здесь.
В общих чертах, файл конфигурации описывает цели, которые пользователь может использовать в своем проекте. Список целей генерируется CMake при создании экспортируемой сущности, привязанной к целям. Версионирование и проверки целостности конфигурации общего назначения реализованы в модуле CMakePackageConfigHelpers.
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
configure_package_config_file(res/neuon-config.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/neuon-config.cmake INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR})
write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/neuon-config-version.cmake VERSION ${version} COMPATIBILITY SameMajorVersion)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/neuon-config.cmake ${CMAKE_CURRENT_BINARY_DIR}/neuon-config-version.cmake DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/neuon/cmake COMPONENT devel)
Шаблон конфигурации может быть похож на:
cmake_policy(PUSH)
cmake_policy(VERSION 3.10)
@PACKAGE_INIT@
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules/")
include(CMakeFindDependencyMacro)
find_dependency(Tensorflow)
include ( "${CMAKE_CURRENT_LIST_DIR}/neuon-headers-targets.cmake" )
include ( "${CMAKE_CURRENT_LIST_DIR}/neuon-targets.cmake" )
check_required_components(headers)
check_required_components(neuon)
list(REMOVE_AT CMAKE_MODULE_PATH -1)
cmake_policy(POP)
Как использовать конфигурации других проектов?
Корректно подготовленная конфигурация не требует никаких дополнительных действия для использования.
find_package(Neuon REQUIRED COMPONENTS headers neuon)
target_link_libraries(neuon-sample PRIVATE neuon::neuon)
Конфигурация декларирует какие библиотеки доступны для использования, какие дополнительные зависимости необходимо слинковать с конечным результатом, какие флаги компилятора должны быть выставлены при использовании. Где находятся заголовочные файлы, и библиотеки импорта тоже указывается посредством файла конфигурации и избавляет пользователя от ручного труда.
Как использовать стандартные CMake модули?
Несмотря на регламентированное именование модулей использование их в своих проектах двоякое. Многие стандартные модули CMake подключаются через функцию include(), однако многие модули, выполняющие поиск библиотек, не поддерживающих CMake самостоятельно через конфигурацию полагаются на find_package() в режиме модулей.
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CPackComponents)
include(CPack)
include(GoogleTest)
find_package(CURL)
find_package(Boost 1.70 MODULE REQUIRED COMPONENTS program_options log)
Библиотека Boost начиная с версии 1.70 обеспечивает поддержку CMake через конфигурации. СMake модуль обладает обратной совместимостью и умеет разрешать местонахождение Boost любой версии, используя конфигурации при наличии и создавая цели-псевдонимы в противном случае.
Как использовать модули из посторонних или личных источников?
Нередки случаи, когда стандартного модуля нет в наличии, или его состояние не удовлетворяет нуждам проекта. В таком случае проект может предоставить свои собственные CMake модули, для использования в своих конфигурациях. Ключевым моментом для использования собственных модулей является параметр CMAKE_MODULE_PATH
, перечисляющий пути поиска модулей. Проект или конфигурация проекта могут самостоятельно изменять переменную, выведенную из этого параметра. Например, можно добавить в свой проект модуль, выполняющий заворачивание всех нееобходимых флагов компиляции для использования FFMpeg в цели CMake и связывать свое приложение с этими целями, не беспокоясь о флагах линковки, флагах компилятора и всех зависимостях.
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/depends/sdk/cmake")
project(neuon-sample VERSION ${version})
find_package(Threads REQUIRED)
find_package(spdlog REQUIRED)
find_package(Boost 1.70 REQUIRED COMPONENTS program_options)
find_package(FFMPEG REQUIRED COMPONENTS avcodec avformat swscale swresample)
add_executable(neuon-sample src/neuon_sample.cpp)
target_link_libraries(neuon-sample PRIVATE spdlog::spdlog Threads::Threads Boost::program_options FFMPEG::avcodec FFMPEG::avformat FFMPEG::swscale
При изменении CMAKE_MODULE_PATH
в своих конфигурациях, чтобы избежать конфликтов модулей с проектом пользователя, можно использовать функции по работе со списками. После добавления своих путей в переменную и использования, можно удалить эти вхождения из списка.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/modules/")
include(CMakeFindDependencyMacro)
find_dependency(Tensorflow)
list(REMOVE_AT CMAKE_MODULE_PATH -1)
Как подключать и переиспользовать другие куски CMake скриптов?
CMake позволяет включать части скриптов друг в друга как есть, не выполняя дополнительной работы. Это может быть полезно для переиспользования функционала, когда использование модуля не обосновано, а дублирования хочется избежать. include() включит содержимое указанной файла в использующий файл. Включаемые файлы должны быть реентерабельны, и не изменять глобальное состояние.
include(depends/sdk/cmake/version.cmake)
configure_version(neuon)
configure_birthday(neuon)
Заключение
CMake 3.10+ однозначно полагается на объектную модель использования. Все есть цель. У цели есть свойства. Проект не должен изменять глобальное состояние. Проект описывает свои цели, и использует свойства целей для модификации своего поведения. Модули - расширяют функционал CMake. Конфигурации обеспечивают интерфейс интеграции проектов и зависимостей. Переиспользование функционала доступно и рекомендуется.
Глобальные свойства, параметры и переменные управляются CMake. Проект не должен их менять по своем усмотрению. Проект должен подстраиваться под глобальное состояние.
Сборка настраивается и модифицируется не внутри скриптов, а используя параметры и аргументы при запуске CMake. Позвольте будущему себе как автору проекта решать, разделяемая библиотека или статическая, установка в системные папки или в директории пользователя с ограниченными правами, включить максимальную оптимизацию или обеспечить отладочную информацию.
CMake предлагает очень широкий выбор доступных параметров и переменных, не стоит дублировать их в своем проекте.
Использование относительных путей предпочтительнее абсолютных.
CMake проекты могут быть включены друг в друга в произвольном порядке. Один проект не должен разрушать состояние, созданное другим.
CMake распространяется как самодостаточный архив под все популярные платформы. Файл конфигурации ограничен версией CMake потребителя, но для сборки и упаковки проекта можно притащить последнюю версию, развернуть локально как часть окружения сборки и пользоваться наиболее свежим функционалом, которые сделан, чтобы делать жизнь авторов проектов проще.
Полезные ссылки
Официальная документация CMake
Поддерживаемая сообществом при участии авторов CMake вики
Официальная страница распространения CMAKE
В чем разница между разными способами включения подпроектов?
CMake предлагает разные способы подключения проектов друг в друга. Если допустить, что проект описывается как минимум одним CMakeLists.txt, то проекты между собой могут быть связаны через add_subdirectory(), include(), ExternalProject_Add().
Основная идея в том, что CMakeLists.txt описывает проект как нечто именованное и версионированное. Директива project() устанавливает некоторые переменные, используемые CMake при генерации и сборке. Как минимум версию проекта.
include() включает содержимое как есть, а в одном CMakeLists.txt не может оказаться две активные директивы project().
ExternalProject_Add() предлагает способ сборки подпроекта из исходного кода, но не делает подпроект частью сборки основного проекта. Через ExternalProject_Add обычно подключаются сторонние зависимости, которые не могут быть удовлетворены окружением сборки или целевой системой.
add_subdirectory() единственный корректный способ подключить проект в проект, создав единое дерево зависимостей и связей между целями, сохранив при этом проектную идентичность и самодостаточность. Помимо непосредственно подключения проекта, CMake выполняет дополнительные операции по изоляции подпроектов и областей видимости.
При этом, можно встретить примеры добавлять CMakeLists.txt на каждый уровень вложенности папок - корневая папка, подпапка с файлами исходного кода, в подпапки еще глубже - и каждый из них включать друг в друга. На практике, никакой пользы от этого подхода не наблюдается. Логическое выделение подпроектов с собственным CMakeLists.txt, и корневой CMakeLists.txt включающий подпроекты и при этом выполняющий общепроектные операции по упаковке приводят к элегантно разграниченным проектам в многопроектных системах сборки как Visual Studio. Иметь одно MSVS решение(solution, ugh....) c проектами(projects) по сборке заголовочных файлов, библиотеки и демо-приложения гораздо приятнее, чем иметь с десяток искусственных проектов без четко обозначенного назначения.