Хабрахабр

CMake и C++ — братья навек

Дружба навек

И очень люблю CMake, потому что он позволяет мне делать всё то, что я хочу. В процессе разработки я люблю менять компиляторы, режимы сборки, версии зависимостей, производить статический анализ, замерять производительность, собирать покрытие, генерировать документацию и т.д.

Многие ругают CMake, и часто заслуженно, но если разобраться, то не всё так плохо, а в последнее время очень даже неплохо, и направление развития вполне позитивное.

В данной заметке я хочу рассказать, как достаточно просто организовать заголовочную библиотеку на языке C++ в системе CMake, чтобы получить следующую функциональность:

  1. Сборку;
  2. Автозапуск тестов;
  3. Замер покрытия кода;
  4. Установку;
  5. Автодокументирование;
  6. Генерацию онлайн-песочницы;
  7. Статический анализ.

Кто и так разбирается в плюсах и си-мейке может просто скачать шаблон проекта и начать им пользоваться.

Содержание

  1. Проект изнутри
    1. Структура проекта
    2. Главный CMake-файл (./CMakeLists.txt)
      1. Информация о проекте
      2. Опции проекта
      3. Опции компиляции
      4. Основная цель
      5. Установка
      6. Тесты
      7. Документация
      8. Онлайн-песочница
    3. Скрипт для тестов (test/CMakeLists.txt)
      1. Тестирование
      2. Покрытие
    4. Скрипт для документации (doc/CMakeLists.txt)
    5. Скрипт для онлайн-песочницы (online/CMakeLists.txt)
  2. Проект снаружи
    1. Сборка
      1. Генерация
      2. Сборка
    2. Опции
      1. MYLIB_COVERAGE
      2. MYLIB_TESTING
      3. MYLIB_DOXYGEN_LANGUAGE
    3. Сборочные цели
      1. По умолчанию
      2. mylib-unit-tests
      3. check
      4. coverage
      5. doc
      6. wandbox
    4. Примеры
  3. Инструменты
  4. Статический анализ
  5. Послесловие

Структура проекта

.
├── CMakeLists.txt
├── README.en.md
├── README.md
├── doc
│ ├── CMakeLists.txt
│ └── Doxyfile.in
├── include
│ └── mylib
│ └── myfeature.hpp
├── online
│ ├── CMakeLists.txt
│ ├── mylib-example.cpp
│ └── wandbox.py
└── test ├── CMakeLists.txt ├── mylib │ └── myfeature.cpp └── test_main.cpp

Остальные файлы каждый желающий может посмотреть непосредственно на странице проекта-шаблона. Главным образом речь пойдёт о том, как организовать CMake-скрипты, поэтому они будут разобраны подробно.

Главный CMake-файл (./CMakeLists.txt)

Информация о проекте

CMake развивается, меняются сигнатуры команд, поведение в разных условиях. В первую очередь нужно затребовать нужную версию системы CMake. Чтобы CMake сразу понимал, чего мы от него хотим, нужно сразу зафиксировать наши к нему требования.

cmake_minimum_required(VERSION 3.13)

команду project). Затем обозначим наш проект, его название, версию, используемые языки и прочее (см.

В данном случае указываем язык CXX (а это значит C++), чтобы CMake не напрягался и не искал компилятор языка C (по умолчанию в CMake включены два языка: C и C++).

project(Mylib VERSION 1.0 LANGUAGES CXX)

Это сильно поможет в дальнейшем. Здесь же можно сразу проверить, включён ли наш проект в другой проект в качестве подпроекта.

get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)

Опции проекта

Предусмотрим две опции.

Это может понадобиться, если мы уверены, что с тестами всё в порядке, а мы хотим, например, только установить или запакетировать наш проект. Первая опция — MYLIB_TESTING — для выключения модульных тестов. Вы же не тестируете зависимости, которыми пользуетесь? Или наш проект включён в качестве подпроекта — в этом случае пользователю нашего проекта не интересно запускать наши тесты.

option(MYLIB_TESTING "Включить модульное тестирование" ON)

Кроме того, мы сделаем отдельную опцию MYLIB_COVERAGE для замеров покрытия кода тестами, но она потребует дополнительных инструментов, поэтому включать её нужно будет явно.

option(MYLIB_COVERAGE "Включить измерение покрытия кода тестами" OFF)

Опции компиляции

Ни одна мышь не проскочит. Разумеется, мы крутые программисты-плюсовики, поэтому хотим от компилятора максимального уровня диагностик времени компиляции.

add_compile_options( -Werror -Wall -Wextra -Wpedantic -Wcast-align -Wcast-qual -Wconversion -Wctor-dtor-privacy -Wenum-compare -Wfloat-equal -Wnon-virtual-dtor -Wold-style-cast -Woverloaded-virtual -Wredundant-decls -Wsign-conversion -Wsign-promo
)

По умолчанию в CMake они включены. Расширения тоже отключим, чтобы полностью соответствовать стандарту языка C++.

if(NOT CMAKE_CXX_EXTENSIONS) set(CMAKE_CXX_EXTENSIONS OFF)
endif()

Основная цель

С другой стороны, чтобы использовать нашу библиотеку снаружи, её нужно установить, нужно, чтобы её можно было обнаружить в системе и подключить к своему проекту, и при этом вместе с ней были привязаны эти самые заголовки, а также, возможно, какие-то дополнительные свойства. Наша библиотека состоит только из заголовочных файлов, а значит, мы не располагаем каким-либо выхлопом в виде статических или динамических библиотек.

Для этой цели создаём интерфейсную библиотеку.

add_library(mylib INTERFACE)

Привязываем заголовки к нашей интерфейсной библиотеке.

передаются через одну единственную цель. Современное, модное, молодёжное использование CMake подразумевает, что заголовки, свойства и т.п. И не требуется никаких [target_]include_directories. Таким образом, достаточно сказать target_link_libraries(target PRIVATE dependency), и все заголовки, которые ассоциированы с целью dependency, будут доступны для исходников, принадлежащих цели target. Это будет продемонстрировано ниже при разборе CMake-скрипта для модульных тестов.

выражения-генераторы: $<...>. Также стоит обратить внимание на т.н.

Данная команда ассоциирует нужные нам заголовки с нашей интерфейсной библиотекой, причём, в случае, если наша библиотека будет подключена к какой-либо цели в рамках одной иерархии CMake, то с ней будут ассоциированы заголовки из директории $/include, а если наша библиотека установлена в систему и подключена в другой проект с помощью команды find_package, то с ней будут ассоциированы заголовки из директории include относительно директории установки.

target_include_directories(mylib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include>
)

Разумеется, самый последний. Установим стандарт языка. Это достигается за счёт того, что установленное свойство имеет категорию INTERFACE (см. При этом не просто включаем стандарт, но и распространяем его на тех, кто будет использовать нашу библиотеку. команду target_compile_features).

target_compile_features(mylib INTERFACE cxx_std_17)

Причём для красоты он будет в специальном "пространстве имён". Заводим псевдоним для нашей библиотеки. Как в Бусте, например. Это будет полезно, когда в нашей библиотеке появятся разные модули, и мы заходим подключать их независимо друг от друга.

add_library(Mylib::mylib ALIAS mylib)

Установка

Тут всё просто. Установка наших заголовков в систему. Говорим, что папка со всеми заголовками должна попасть в директорию include относительно места установки.

install(DIRECTORY include/mylib DESTINATION include)

Далее сообщаем системе сборки о том, что мы хотим иметь возможность в сторонних проектах звать команду find_package(Mylib) и получать цель Mylib::mylib.

install(TARGETS mylib EXPORT MylibConfig)
install(EXPORT MylibConfig NAMESPACE Mylib:: DESTINATION share/Mylib/cmake)

Когда в стороннем проекте мы вызовем команду find_package(Mylib 1. Следующее заклинание нужно понимать так. 3 REQUIRED), и при этом реальная версия установленной библиотеки окажется несовместимой с версией 1. 2. 3, CMake автоматически сгенерирует ошибку. 2. То есть не нужно будет следить за версиями вручную.

include(CMakePackageConfigHelpers)
write_basic_package_version_file("${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY AnyNewerVersion
)
install(FILES "${PROJECT_BINARY_DIR}/MylibConfigVersion.cmake" DESTINATION share/Mylib/cmake)

Тесты

Если тесты выключены явно с помощью соответствующей опции или наш проект является подпроектом, то есть подключён в другой CMake-проект с помощью команды add_subdirectory, мы не переходим дальше по иерархии, и скрипт, в котором описаны команды для генерации и запуска тестов, просто не запускается.

if(NOT MYLIB_TESTING) message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT) message(STATUS "Mylib не тестируется в режиме подмодуля")
else() add_subdirectory(test)
endif()

Документация

Документация также не будет генерироваться в случае подпроекта.

if(NOT IS_SUBPROJECT) add_subdirectory(doc)
endif()

Онлайн-песочница

Аналогично, онлайн-песочницы у подпроекта тоже не будет.

if(NOT IS_SUBPROJECT) add_subdirectory(online)
endif()

Скрипт для тестов (test/CMakeLists.txt)

Тестирование

Первым делом находим пакет с нужным тестовым фреймворком (замените на свой любимый).

find_package(doctest 2.3.3 REQUIRED)

Обычно непосредственно в исполняемый бинарник я добавляю только файл, в котором будет функция main. Создаём наш исполняемый файл с тестами.

add_executable(mylib-unit-tests test_main.cpp)

Но так делать не обязательно. А файлы, в которых описаны сами тесты, добавляю позже.

target_sources(mylib-unit-tests PRIVATE mylib/myfeature.cpp)

Обратите внимание, что к нашему бинарнику мы привязали только нужные нам CMake-цели, и не вызывали команду target_include_directories. Подключаем зависимости. Заголовки из тестового фреймворка и из нашей Mylib::mylib, а также параметры сборки (в нашем случае это стандарт языка C++) пролезли вместе с этими целями.

target_link_libraries(mylib-unit-tests PRIVATE Mylib::mylib doctest::doctest
)

Это значит, что сборка по умолчанию инициирует запуск тестов, то есть мы никогда не забудем их запустить. Наконец, создаём фиктивную цель, "сборка" которой эквивалентна запуску тестов, и добавляем эту цель в сборку по умолчанию (за это отвечает атрибут ALL).

add_custom_target(check ALL COMMAND mylib-unit-tests)

Покрытие

В детали вдаваться не буду, потому что они относятся больше к инструменту для замеров покрытия, чем к CMake. Далее включаем замер покрытия кода, если задана соответствующая опция. Важно только отметить, что по результатам будет создана цель coverage, с помощью которой удобно запускать замер покрытия.

find_program(GCOVR_EXECUTABLE gcovr)
if(MYLIB_COVERAGE AND GCOVR_EXECUTABLE) message(STATUS "Измерение покрытия кода тестами включено") target_compile_options(mylib-unit-tests PRIVATE --coverage) target_link_libraries(mylib-unit-tests PRIVATE gcov) add_custom_target(coverage COMMAND ${GCOVR_EXECUTABLE} --root=${PROJECT_SOURCE_DIR}/include/ --object-directory=${CMAKE_CURRENT_BINARY_DIR} DEPENDS check )
elseif(MYLIB_COVERAGE AND NOT GCOVR_EXECUTABLE) set(MYLIB_COVERAGE OFF) message(WARNING "Для замеров покрытия кода тестами требуется программа gcovr")
endif()

Скрипт для документации (doc/CMakeLists.txt)

Нашли Doxygen.

find_package(Doxygen)

Если да, то не трогаем, если нет, то берём русский. Дальше проверяем, установлена ли пользователем переменная с языком. Все нужные переменные, в том числе и язык попадают туда в процессе конфигурации (см. Затем конфигурируем файлы системы Doxygen. команду configure_file).

Поскольку генерирование документации — не самая большая необходимость в процессе разработки, то по умолчанию цель включена не будет, её придётся запускать явно. После чего создаём цель doc, которая будет запускать генерирование документации.

if (Doxygen_FOUND) if (NOT MYLIB_DOXYGEN_LANGUAGE) set(MYLIB_DOXYGEN_LANGUAGE Russian) endif() message(STATUS "Doxygen documentation will be generated in ${MYLIB_DOXYGEN_LANGUAGE}") configure_file(Doxyfile.in Doxyfile) add_custom_target(doc COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
endif ()

Скрипт для онлайн-песочницы (online/CMakeLists.txt)

В ответ приходит ссылка на готовую песочницу. Тут находим третий Питон и создаём цель wandbox, которая генерирует запрос, соответствующий API сервиса Wandbox, и отсылает его.

find_program(PYTHON3_EXECUTABLE python3)
if(PYTHON3_EXECUTABLE) set(WANDBOX_URL "https://wandbox.org/api/compile.json") add_custom_target(wandbox COMMAND ${PYTHON3_EXECUTABLE} wandbox.py mylib-example.cpp "${PROJECT_SOURCE_DIR}" include | curl -H "Content-type: application/json" -d @- ${WANDBOX_URL} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} DEPENDS mylib-unit-tests )
else() message(WARNING "Для создания онлайн-песочницы требуется интерпретатор ЯП python 3-й версии")
endif()

Теперь рассмотрим, как этим всем пользоваться.

Сборка

Сборка данного проекта, как и любого другого проекта на системе сборки CMake, состоит из двух этапов:

Генерация

cmake -S путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Если команда выше не сработала из-за старой версии CMake, попробуйте опустить -S:

cmake путь/к/исходникам -B путь/к/сборочной/директории [опции ...]

Подробнее про опции.

Сборка проекта

cmake --build путь/к/сборочной/директории [--target target]

Подробнее про сборочные цели.

Опции

MYLIB_COVERAGE

cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]

Включает цель coverage, с помощью которой можно запустить замер покрытия кода тестами.

MYLIB_TESTING

cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]

Как следствие, выключается замер покрытия кода тестами (см. Предоставляет возможность выключить сборку модульных тестов и цель check. MYLIB_COVERAGE).

Также тестирование автоматически отключается в случае, если проект подключается в другой проект качестве подпроекта с помощью команды add_subdirectory.

MYLIB_DOXYGEN_LANGUAGE

cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]

Список доступных языков см. Переключает язык документации, которую генерирует цель doc на заданный. на сайте системы Doxygen.

По умолчанию включён русский.

Сборочные цели

По умолчанию

cmake --build path/to/build/directory
cmake --build path/to/build/directory --target all

Если цель не указана (что эквивалентно цели all), собирает всё, что можно, а также вызывает цель check.

mylib-unit-tests

cmake --build path/to/build/directory --target mylib-unit-tests

Включено по умолчанию. Компилирует модульные тесты.

check

cmake --build путь/к/сборочной/директории --target check

Включено по умолчанию. Запускает собранные (собирает, если ещё не) модульные тесты.

также mylib-unit-tests. См.

coverage

cmake --build путь/к/сборочной/директории --target coverage

Анализирует запущенные (запускает, если ещё не) модульные тесты на предмет покрытия кода тестами при помощи программы gcovr.

Выхлоп покрытия будет выглядеть примерно так:

------------------------------------------------------------------------------ GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File Lines Exec Cover Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp 2 2 100% ------------------------------------------------------------------------------
TOTAL 2 2 100%
------------------------------------------------------------------------------

Цель доступна только при включённой опции MYLIB_COVERAGE.

также check. См.

doc

cmake --build путь/к/сборочной/директории --target doc

Запускает генерацию документации к коду при помощи системы Doxygen.

wandbox

cmake --build путь/к/сборочной/директории --target wandbox

Ответ от сервиса выглядит примерно так:

{ "permlink" : "QElvxuMzHgL9fqci", "status" : "0", "url" : "https://wandbox.org/permlink/QElvxuMzHgL9fqci"
}

Не знаю, насколько у них резиновые сервера, но думаю, что злоупотреблять данной возможностью не стоит. Для этого используется сервис Wandbox.

Примеры

Сборка проекта в отладочном режиме с замером покрытия

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DMYLIB_COVERAGE=ON
cmake --build путь/к/сборочной/директории --target coverage --parallel 16

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

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DMYLIB_TESTING=OFF -DCMAKE_INSTALL_PREFIX=путь/к/установойной/директории
cmake --build путь/к/сборочной/директории --target install

Сборка в выпускном режиме заданным компилятором

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_PREFIX_PATH=путь/к/директории/куда/установлены/зависимости
cmake --build путь/к/сборочной/директории --parallel 4

Генерирование документации на английском

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Release -DMYLIB_DOXYGEN_LANGUAGE=English
cmake --build путь/к/сборочной/директории --target doc

  1. 13 CMake 3.

    13 требуется только для запуска некоторых консольных команд, описанных в данной справке. На самом деле версия CMake 3. 8, если генерацию вызывать другими способами. С точки зрения синтаксиса CMake-скриптов достаточно версии 3.

  2. Библиотека тестирования doctest

    опцию MYLIB_TESTING). Тестирование можно отключать (см.

  3. Doxygen

    Для переключения языка, на котором будет сгенерирована документация, предусмотрена опция MYLIB_DOXYGEN_LANGUAGE.

  4. Интерпретатор ЯП Python 3

    Для автоматической генерации онлайн-песочницы.

С помощью CMake и пары хороших инструментов можно обеспечить статический анализ с минимальными телодвижениями.

Cppcheck

В CMake встроена поддержка инструмента для статического анализа Cppcheck.

Для этого нужно воспользоваться опцией CMAKE_CXX_CPPCHECK:

cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"

Ничего дополнительного делать не нужно. После этого статический анализ будет автоматически запускаться каждый раз во время компиляции и перекомпиляции исходников.

Clang

При помощи чудесного инструмента scan-build тоже можно запускать статический анализ в два счёта:

scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директории

Здесь, в отличие от случая с Cppcheck, требуется каждый раз запускать сборку через scan-build.

И, хотя, синтаксис порой оставляет желать лучшего, всё же не так страшен чёрт, как его малюют. CMake — очень мощная и гибкая система, позволяющая реализовывать функциональность на любой вкус и цвет. Пользуйтесь системой сборки CMake на благо общества и с пользой для здоровья.

→ Скачать шаблон проекта

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть