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. доктар
      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 сэрвісу Скрыня для палачак, і адсылае яго. У адказ прыходзіць спасылка на гатовую пясочніцу.

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.

доктар

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

Запускае генерацыю дакументацыі да кода пры дапамозе сістэмы Doxygen.

wandbox

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

Адказ ад сэрвісу выглядае прыкладна так:

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

Для гэтага выкарыстоўваецца сэрвіс Скрыня для палачак. Не ведаю, наколькі ў іх гумовыя серверы, але думаю, што марнатравіць дадзенай магчымасцю не варта.

прыклады

Зборка праекта ў адладкавым рэжыме з замерам пакрыцця

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

Дадаць каментар