У процесі розробки я люблю змінювати компілятори, режими збирання, версії залежностей, робити статичний аналіз, вимірювати продуктивність, збирати покриття, генерувати документацію тощо. І дуже люблю CMake, тому що вона дозволяє мені робити все те, що я хочу.
Багато хто лає CMake, і часто заслужено, але якщо розібратися, то не все так погано, а останнім часом навіть дуже непогано, і напрямок розвитку цілком позитивний.
У даній замітці я хочу розповісти, як досить просто організувати заголовну бібліотеку мовою C++ в системі CMake, щоб отримати таку функціональність:
Складання;
Автозапуск тестів;
Вимір покриття коду;
Встановлення;
Автодокументування;
генерацію онлайн-пісочниці;
Статичний аналіз.
Хто і так розуміється на плюсах і сім'ї може просто завантажити шаблон проекту та почати ним користуватися.
Головним чином мова піде про те, як організувати CMake-скрипти, тому вони будуть детально розібрані. Інші файли кожен бажаючий може подивитися безпосередньо на сторінці проекту-шаблону.
Насамперед потрібно зажадати потрібну версію системи CMake. CMake розвивається, змінюються сигнатури команд, поведінка у різних умовах. Щоб CMake одразу розумів, чого ми від нього хочемо, потрібно одразу зафіксувати наші до нього вимоги.
cmake_minimum_required(VERSION 3.13)
Потім позначимо наш проект, його назву, версію, мови та інше (див. команду project).
В даному випадку вказуємо мову CXX (а це означає C++), щоб CMake не напружувався і не шукав компілятор мови C (за замовчуванням CMake включені дві мови: C і C++).
project(Mylib VERSION 1.0 LANGUAGES CXX)
Тут же можна відразу перевірити, чи включений наш проект до іншого проекту як підпроект. Це дуже допоможе надалі.
Перша опція - MYLIB_TESTING - Для вимкнення модульних тестів. Це може знадобитися, якщо ми впевнені, що з тестами все гаразд, а ми хочемо, наприклад, лише встановити чи запакетувати наш проект. Або наш проект включений як підпроект – у цьому випадку користувачеві нашого проекту не цікаво запускати наші тести. Ви не тестуєте залежності, якими користуєтеся?
Крім того, ми зробимо окрему опцію MYLIB_COVERAGE для вимірів покриття коду тестами, але вона вимагатиме додаткових інструментів, тому включати її потрібно буде явно.
Зрозуміло, ми є крутими програмістами-плюсовиками, тому хочемо від компілятора максимального рівня діагностик часу компіляції. Жодна миша не проскочить.
Наша бібліотека складається тільки з заголовних файлів, а значить, ми не маємо вихлопу у вигляді статичних або динамічних бібліотек. З іншого боку, щоб використовувати нашу бібліотеку зовні, її потрібно встановити, потрібно, щоб її можна було виявити в системі та підключити до свого проекту, і при цьому разом з нею були прив'язані ці самі заголовки, а також, можливо, якісь додаткові властивості.
З цією метою створюємо інтерфейсну бібліотеку.
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 щодо директорії встановлення.
Встановимо стандарт мови. Зрозуміло, останній. При цьому не просто включаємо стандарт, а й поширюємо його на тих, хто використовуватиме нашу бібліотеку. Це досягається за рахунок того, що встановлена властивість має категорію INTERFACE (Див. команду target_compile_features).
Заводимо псевдонім для нашої бібліотеки. Причому для краси він буде у спеціальному просторі імен. Це буде корисно, коли в нашій бібліотеці з'являться різні модулі, і ми заходимо підключати їх незалежно один від одного. Як у Бусті, наприклад.
Встановлення наших заголовків у систему. Тут все просто. Говоримо, що папка з усіма заголовками має потрапити до директорії include щодо місця встановлення.
Далі повідомляємо системі складання про те, що ми хочемо мати можливість у сторонніх проектах звати команду find_package(Mylib) і отримувати мету Mylib::mylib.
Наступне заклинання слід розуміти так. Коли у сторонньому проекті ми викличемо команду find_package(Mylib 1.2.3 REQUIRED), і при цьому реальна версія встановленої бібліотеки виявиться несумісною з версією 1.2.3, CMake автоматично згенерує помилку. Тобто, не потрібно буде стежити за версіями вручну.
Якщо тести вимкнені явно за допомогою відповідної опції або наш проект є підпроектом, тобто підключений до іншого CMake-проекту за допомогою команди add_subdirectory, ми не переходимо далі по ієрархії, і скрипт, в якому описані команди для генерації та запуску тестів, просто не запускається.
if(NOT MYLIB_TESTING)
message(STATUS "Тестирование проекта Mylib выключено")
elseif(IS_SUBPROJECT)
message(STATUS "Mylib не тестируется в режиме подмодуля")
else()
add_subdirectory(test)
endif()
Підключаємо залежності. Зверніть увагу, що до нашого бінарника ми прив'язали лише потрібні нам CMake-мети, і не викликали команду target_include_directories. Заголовки з тестового фреймворку та з нашої Mylib::mylib, і навіть параметри складання (у разі це стандарт мови C++) пролізли разом із цими цілями.
Нарешті, створюємо фіктивну мету, «складання» якої еквівалентна запуску тестів, і додаємо цю мету у складання за замовчуванням (за це відповідає атрибут 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()
Далі перевіряємо, чи встановлена користувачем змінна з мовою. Якщо так, то не чіпаємо, якщо ні, то беремо російську. Потім конфігуруємо файли системи 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 ()
Тут знаходимо третій Пітон і створюємо мету 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 3.13 потрібна лише для запуску деяких консольних команд, описаних у цій довідці. З погляду синтаксису CMake-скриптів достатньо версії 3.8, якщо генерацію викликати іншими способами.
Після цього статичний аналіз автоматично запускатиметься щоразу під час компіляції та перекомпіляції вихідників. Нічого додаткового не потрібно робити.
Кланг
За допомогою чудового інструменту scan-build теж можна запускати статичний аналіз на два рахунки:
CMake - дуже потужна та гнучка система, що дозволяє реалізовувати функціональність на будь-який смак та колір. І, хоча, синтаксис часом залишає бажати кращого, все ж таки не такий страшний чорт, як його малюють. Користуйтеся системою складання CMake на благо суспільства та з користю для здоров'я.