
У працэсе распрацоўкі я люблю мяняць кампілятары, рэжымы зборкі, версіі залежнасцяў, вырабляць статычны аналіз, замяраць прадукцыйнасць, збіраць пакрыццё, генераваць дакументацыю і г.д. І вельмі люблю CMake, таму што ен дазваляе мне рабіць усё тое, што я хачу.
Многія лаюць CMake, і часта заслужана, але калі разабрацца, то не ўсё так дрэнна, а ў апошні час вельмі нават нядрэнна, і кірунак развіцця цалкам пазітыўны.
У дадзенай нататцы я жадаю распавесці, як досыць проста арганізаваць загалоўкавую бібліятэку на мове C++ у сістэме CMake, каб атрымаць наступную функцыянальнасць:
- Зборку;
- Аўтазапуск тэстаў;
- Замер пакрыцця кода;
- Устаноўку;
- Аўтадакументаванне;
- Генерацыю анлайн-пясочніцы;
- Статычны аналіз.
Хто і так разбіраецца ў плюсах і сі-мейку можа проста і пачаць ім карыстацца.
Змест
.
├── 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. CMake развіваецца, мяняюцца сігнатуры каманд, паводзіны ў розных умовах. Каб CMake адразу разумеў, чаго мы ад яго жадаем, трэба адразу зафіксаваць нашы да яго патрабаванні.
cmake_minimum_required(VERSION 3.13)Затым абазначым наш праект, яго назву, версію, якія выкарыстоўваюцца мовы і іншае (гл. ).
У дадзеным выпадку паказваем мову CXX (а гэта значыць C++), каб CMake не напружваўся і не шукаў кампілятар мовы C (па змаўчанні ў CMake уключаны дзве мовы: C і C++).
project(Mylib VERSION 1.0 LANGUAGES CXX)Тутака ж можна адразу праверыць, ці ўключаны наш праект у іншы праект у якасці падпраекта. Гэта моцна дапаможа ў далейшым.
get_directory_property(IS_SUBPROJECT PARENT_DIRECTORY)
Прадугледзім дзве опцыі.
Першая опцыя - - Для выключэння модульных тэстаў. Гэта можа спатрэбіцца, калі мы ўпэўненыя, што з тэстамі ўсё ў парадку, а мы хочам, напрыклад, толькі ўсталяваць ці запакетаваць наш праект. Або наш праект уключаны ў якасці падпраекта - у гэтым выпадку карыстачу нашага праекта не цікава запускаць нашы тэсты. Вы ж не тэстуеце залежнасці, якімі карыстаецеся?
option(MYLIB_TESTING "Включить модульное тестирование" ON)Акрамя таго, мы зробім асобную опцыю для замераў пакрыцця кода тэстамі, але яна запатрабуе дадатковых прылад, таму ўключаць яе трэба будзе відавочна.
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 мае на ўвазе, што загалоўкі, уласцівасці і да т.п. перадаюцца праз адну адзіную мэту. Такім чынам, дастаткова сказаць , і ўсе загалоўкі, якія асацыіраваны з мэтай dependency, будуць даступныя для зыходнікаў, якія належаць мэты target. І не патрабуецца ніякіх [target_]include_directories. Гэта будзе прадэманстравана ніжэй пры разборы .
Таксама варта звярнуць увагу на т.зв. .
Дадзеная каманда асацыюе патрэбныя нам загалоўкі з нашай інтэрфейснай бібліятэкай, прычым, у выпадку, калі наша бібліятэка будзе падключана да якой-небудзь мэты ў рамках адной іерархіі CMake, то з ёй будуць асацыіраваны загалоўкі з дырэкторыі. ${CMAKE_CURRENT_SOURCE_DIR}/include, а калі наша бібліятэка ўстаноўлена ў сістэму і падключана ў іншы праект з дапамогай каманды , то з ёй будуць асацыіраваны загалоўкі з дырэкторыі include адносна дырэкторыі ўстаноўкі.
target_include_directories(mylib INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)Усталюем стандарт мовы. Зразумела, самы апошні. Пры гэтым не проста ўключаем стандарт, але і распаўсюджваем яго на тых, хто будзе выкарыстоўваць нашу бібліятэку. Гэта дасягаецца за кошт таго, што ўсталяваная ўласцівасць мае катэгорыю. INTERFACE (гл. ).
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-праект з дапамогай каманды , мы не пераходзім далей па іерархіі, і скрыпт, у якім апісаны каманды для генерацыі і запуску тэстаў, проста не запускаецца.
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()
Перш за ўсё знаходзім пакет з патрэбным тэставым фрэймворкам (заменіце на свой каханы).
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. Важна толькі адзначыць, што па выніках будзе створана мэта , з дапамогай якой зручна запускаць замер пакрыцця.
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()
.
find_package(Doxygen)Далей правяраем, ці ўсталяваная карыстачом зменная з мовай. Калі так, то не чапаем, калі не, то бярэм рускі. Затым які канфігуруецца файлы сістэмы Doxygen. Усе неабходныя зменныя, у тым ліку і мова трапляюць туды ў працэсе канфігурацыі (гл. ).
Пасля чаго ствараем мэту , Якая будзе запускаць генераванне дакументацыі. Паколькі генераванне дакументацыі – не самая вялікая неабходнасць у працэсе распрацоўкі, то па змаўчанні мэта ўключана не будзе, яе давядзецца запускаць відавочна.
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 ()
Тут знаходзім трэці Пітон і ствараем мэту , якая генеруе запыт, які адпавядае 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].
cmake -S ... -B ... -DMYLIB_COVERAGE=ON [прочие опции ...]Уключае мэту , з дапамогай якой можна запусціць замер пакрыцця кода тэстамі.
cmake -S ... -B ... -DMYLIB_TESTING=OFF [прочие опции ...]Дае магчымасць выключыць зборку модульных тэстаў і мэта . Як следства, выключаецца замер пакрыцця кода тэстамі (гл. ).
Таксама тэсціраванне аўтаматычна адключаецца ў выпадку, калі праект падключаецца ў іншы праект якасці падпраекта з дапамогай каманды .
cmake -S ... -B ... -DMYLIB_DOXYGEN_LANGUAGE=English [прочие опции ...]Пераключае мову дакументацыі, якую генеруе мэту на зададзены. Спіс даступных моў гл. .
Па змаўчанні ўключаны руская.
cmake --build path/to/build/directory
cmake --build path/to/build/directory --target allКалі мэта не пазначана (што эквівалентна мэты all), збірае ўсё, што можна, а таксама выклікае мэту .
cmake --build path/to/build/directory --target mylib-unit-testsКампілюе модульныя тэсты. Уключана па змаўчанні.
cmake --build путь/к/сборочной/директории --target checkЗапускае сабраныя (збірае, калі яшчэ не) модульныя тэсты. Уключана па змаўчанні.
Глядзіце таксама .
cmake --build путь/к/сборочной/директории --target coverageАналізуе запушчаныя (запускае, калі яшчэ не) модульныя тэсты на прадмет пакрыцця кода тэстамі пры дапамозе праграмы .
Выхлап пакрыцця будзе выглядаць прыкладна так:
------------------------------------------------------------------------------
GCC Code Coverage Report
Directory: /path/to/cmakecpptemplate/include/
------------------------------------------------------------------------------
File Lines Exec Cover Missing
------------------------------------------------------------------------------
mylib/myfeature.hpp 2 2 100%
------------------------------------------------------------------------------
TOTAL 2 2 100%
------------------------------------------------------------------------------Мэта даступная толькі пры ўключанай опцыі .
Глядзіце таксама .
cmake --build путь/к/сборочной/директории --target docЗапускае генерацыю дакументацыі да кода пры дапамозе сістэмы .
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
3.13
Насамрэч версія CMake 3.13 патрабуецца толькі для запуску некаторых кансольных каманд, апісаных у дадзенай даведцы. З пункту гледжання сінтаксісу CMake-скрыптоў дастаткова версіі 3.8, калі генерацыю выклікаць іншымі спосабамі.
Бібліятэка тэсціравання
Тэставанне можна адключаць (гл. ).
Для пераключэння мовы, на якой будзе згенеравана дакументацыя, прадугледжана опцыя .
Інтэрпрэтатар ЯП
Для аўтаматычнай генерацыі .
З дапамогай CMake і пары добрых інструментаў можна забяспечыць статычны аналіз з мінімальнымі рухамі цела.
Cppcheck
У CMake убудавана падтрымка прылады для статычнага аналізу .
Для гэтага трэба скарыстацца опцыяй :
cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_CPPCHECK="cppcheck;--enable=all;-Iпуть/к/исходникам/include"Пасля гэтага статычны аналіз будзе аўтаматычна запускацца кожны раз падчас кампіляцыі і перакампіляванні зыходнікаў. Нічога дадатковага рабіць ня трэба.
Лапатка
Пры дапамозе цудоўнай прылады таксама можна запускаць статычны аналіз у два рахункі:
scan-build cmake -S путь/к/исходникам -B путь/к/сборочной/директории -DCMAKE_BUILD_TYPE=Debug
scan-build cmake --build путь/к/сборочной/директорииТут, у адрозненне ад выпадку з Cppcheck, патрабуецца кожны раз запускаць зборку праз scan-build.
CMake - вельмі магутная і гнуткая сістэма, якая дазваляе рэалізоўваць функцыянальнасць на любы густ і колер. І, хоць, сінтаксіс часам пакідае жадаць лепшага, усё ж не такі страшны чорт, як яго малююць. Карыстайцеся сістэмай зборкі CMake на карысць супольнасці і з карысцю для здароўя.
→
Крыніца: habr.com
