CMake і C++ - брати навіки

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. перевірка
      4. охоплення
      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
)

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

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

Основна ціль

Наша бібліотека складається тільки з заголовних файлів, а значить, ми не маємо вихлопу у вигляді статичних або динамічних бібліотек. З іншого боку, щоб використовувати нашу бібліотеку зовні, її потрібно встановити, потрібно, щоб її можна було виявити в системі та підключити до свого проекту, і при цьому разом з нею були прив'язані ці самі заголовки, а також, можливо, якісь додаткові властивості.

З цією метою створюємо інтерфейсну бібліотеку.

add_library(mylib INTERFACE)

Прив'язуємо заголовки до нашої інтерфейсної бібліотеки.

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

Також варто звернути увагу на т.зв. выражения-генераторы: $<...>.

Ця команда асоціює потрібні нам заголовки з нашою інтерфейсною бібліотекою, причому, якщо наша бібліотека буде підключена до будь-якої мети в рамках однієї ієрархії CMake, то з нею будуть асоційовані заголовки з директорії ${CMAKE_CURRENT_SOURCE_DIR}/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.2.3 REQUIRED), і при цьому реальна версія встановленої бібліотеки виявиться несумісною з версією 1.2.3, CMake автоматично згенерує помилку. Тобто, не потрібно буде стежити за версіями вручну.

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

Компілює модульні випробування. Увімкнено за замовчуванням.

перевірка

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

Запускає зібрані (збирає, якщо ще) модульні тести. Увімкнено за замовчуванням.

Див також mylib-unit-tests.

охоплення

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. CMake 3.13

    Насправді версія CMake 3.13 потрібна лише для запуску деяких консольних команд, описаних у цій довідці. З погляду синтаксису CMake-скриптів достатньо версії 3.8, якщо генерацію викликати іншими способами.

  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"

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

Кланг

За допомогою чудового інструменту scan-build теж можна запускати статичний аналіз на два рахунки:

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

Тут, на відміну від випадку з Cppcheck, потрібно щоразу запускати збірку через scan-build.

Післямова

CMake - дуже потужна та гнучка система, що дозволяє реалізовувати функціональність на будь-який смак та колір. І, хоча, синтаксис часом залишає бажати кращого, все ж таки не такий страшний чорт, як його малюють. Користуйтеся системою складання CMake на благо суспільства та з користю для здоров'я.

Завантажити шаблон проекту

Джерело: habr.com

Додати коментар або відгук