
У процесі розробки я люблю змінювати компілятори, режими збирання, версії залежностей, робити статичний аналіз, вимірювати продуктивність, збирати покриття, генерувати документацію тощо. І дуже люблю 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
