Comments 29
Что тут можно сказать. Я вообще на С++ особенно не пишу, но когда пишу, делаю сборку на CMake. И многое из того, что я нашёл в статье я очень рад узнать, спасибо. Такие вот простые вещи, как cmake --build, например.
Про make install я всё же поспорю. Безусловно, если собирается какая-то программа, которой нет в репозитории, её стоит паковать checkinstall'ом (при отсутствии поддержки правильной сборки в формат дистрибутива). Но нередка ситуация, когда нужно пересобрать уже имеющуюся в системе библиотеку с другими флагами или вообще нужна более новая версия. Я для себя выбрал два подхода:
1) конфигурим с префиксом /opt/build/%%softwarename%% (например, /opt/build/ffmpeg), на весь /opt ставим владельцем основного юзера, тем более, этот каталог принадлежит корневой системе, которая на SSD. Ставим через make install, без sudo. Далее по необходимости можно использовать при сборке PKG_CONFIG_PATH, чтобы найти именно эту библиотеку, либо специфичные настройки для модулей CMake (типа OPENSSL_ROOT_DIR). Из плюсов — удаление сводится к удалению каталога, т.к. мы ставили из-под юзера, в какие-либо системные файлы собранное прописаться чисто физически не могло, из минусов — каждую такую библиотеку надо указывать отдельно;
2) мне часто нужно собирать как можно более статичный бинарник, чтобы его можно было закинуть на целевой сервер или ПК коллеги, и он бы там просто работал без доустановок кучи библиотек, а размер бинаря значения обычно не имеет. Для этого конфигурим с префиксом /opt/build/static и точно так же make install'им из-под юзера. Получается такой второй рут со сборной солянкой из библиотек и софта, плюсы и минусы, соответственно, обратны первому варианту.
Вообще, проблема сборки статических бинарников освещена довольно слабо, а хитростей там много — например, слишком старая версия libstdc++ (на Ubuntu 14.04), ненайденные приложенные библиотеки, которые не получается слинковать статически, из-за отсутствующего RPATH (это вообще странная тема для CMake, ставится абсолютно неинтуитивно), ненайденные символы из libdl и libpthread, последний вообще может быть слинкован и статически, и динамически одновременно, что и приводит к таким ошибкам и т.д. Конкретно CMake не очень любит библиотеки .a и предпочитает им .so, если есть оба варианта. В целом, форсировать .a можно, но не кроссплатформенно, т.к. расширение различное на WIn и Mac, а какой-то единой рукоятки «линкуй всё статически» почему-то или не завезли, или я её не нашёл, или она работает по принципу «всё или ничего», а это не всегда возможно, например, PulseAudio уж точно статически линковать не стоит, равно как и библиотеки из Xorg. Пока оптимальный вариант — задавать этот /opt/build/static/lib через link_directories, и если там не хватает нужной .a библиотеки, можно просто скопировать или слинковать её из основной системы.
Ещё бы хотелось почитать про управление зависимостями уровня Maven, но я ничего живого кроме conan.io не нашёл, да и тот что-то не сильно упрощает сборку, если не наоборот. Сам CMake, вроде бы, позволяет подключать git-репозитории как зависимости, но я в эту тему не углублялся. Вообще, похоже, для C/C++ проектов у CMake сейчас альтернатив не видно, хотя до уже упомянутого Maven ещё идти и идти, к сожалению.
Сам им не владею.
P.S. Проекты собираю по старинке с помощью GNU Make/QMake.
https://www.youtube.com/watch?v=ckO98bRzL9Y
А вообще, мой опыт использования открытых библиотек Яндекса говорит о том, что сборочные файлы cmake они пишут откровенно плохо.
Не освещены такие крутые штуки CMake как ExternalProject, ExternalData, custom_target, CMakeConfig (find_package) и т. д. и т. п. CMake очень спорный инструмент, с одной стороны вызывает восхищение его возможности, а с другой отвращение его перегруженность, уродливость и нелогичность. Без документации я не могу запомнить как пользоваться его стандартными функциями set, list, string, и т. д. У каждой несколько сигнатур, и я каждый раз лезу в документацию, чтобы выяснить как писать "name value CACHE INTERNAL comment" или "name value comment INTERNAL CACHE".
include(scripts/functions.cmake)
Можно использовать CMAKE_MODULE_PATH
и тогда можно не писать путь в include:
set(CMAKE_MODULE_PATH scripts)
include(functions)
Совет №6: используйте функции
Там ещё есть парсер аргументов функции: модуль CMakeParseArguments :)
Совет №7 (спорный): не перечисляйте исходные файлы по одному
Возможно, могу ошибаться или не правильно понял идею в статье, но в этом случае при изменениях в файлах, cmake об этом не узнает и не станет пересобирать/дособирать проект. Нужно делать rebuild.
В крупных открытых проектах, например в KDE, применение своих функций может быть дурным тоном.
Вот уж нет, любой крупный проект, использующий CMake, обрастает невероятным количеством функций, макросов и костылей. Посмотрите на Insight Toolkit. Это как раз тот проект для сборки которого CMake изначально и создавался. Там ад, в системе сборки разобраться сложнее чем в самом проекте.
1) Откройте для себя наконец Ninja (cmake -G Ninja ..). Скорость сборки возрастает существенно.
2) По возможности распиливайте проект на библиотеки и тяните их через git submodules.
3) Если тест можно написать на Python, пишите на Python.
4) И вообще раз заговорили про C++: сразу прикручивайте clang-format, clang static analyzer, cppcheck и valgrind, мерьте степень покрытия кода с помощью lcov.
У себя в бложике я описывал эти моменты более подробно.
1) Откройте для себя наконец Ninja (cmake -G Ninja ..). Скорость сборки возрастает существенно.
У нас несколько лет проекту, поэтому в CMakeLists.txt понаписано уже. Поэтому просто использовав "-G Ninja" получаем на выходе довольно невнятные ошибки об отсутствии правил. Так и живем с make -j 8 :)
С git submodules одна загвоздка… (может, недорыл, а может так оно и есть) — всё получается уж больно независимо.
Есть прога foo версии 4, собирается с подмодулем библиотеки bar версии 3.
А при этом старая версия foo версии 2 требует bar версии 1 и с более свежими версиями не собирается.
- Как это связано в подмодулях? Если я зачекаутю foo на версию 2 — зачекаутится ли bar и другие подмодули автоматически до тех самых совместимых версий, или же нужно где-то хранить граф этих зависимостей и вручную по нему ходить?
При желании можно сократить заклинание конфигурирования и сборки до двух:
cmake -B%папка_для_сборки% -H%папка_с_исходниками% cmake --build %папка_для_сборки% --config %конфигурация_сборки%
, например:
cmake -Bbuild -H. cmake --build build --config Debug
Это две команды последовательно выполнять? Почему тогда не на разных строках?
И почему директория для сборки два раза?
Поехал перенос строк.
cmake -B %папка_для_сборки% -H%папка_с_исходниками%
cmake --build %папка_для_сборки% --config %конфигурация_сборки%
Пример:
cmake -B build/debug -H.
cmake --build build/debug --config Debug
Про перенос понятно. Вопрос зачем два раза указывать директорию для сборки всё ещё открыт :).
Так в первый раз указываешь куда сгенерировать всякие сборочные файлы и прочий мусор. А второй начинается акт сборки на основании сгенерированных файлов.
Можно так сделать:
cmake -B build/debug -H.
cmake -B build/release -H.
cmake --build build/debug --config Debug
cmake --build build/release --config Release
ну, возможно кому-то проще и cmake туда рулить.
Но лично мне кажется, что лучше создать целевую папку, перейти в неё — и дальше без всяких дополнительных ключей, там и собирать.
Я лучше оформлю это в одну единицу и вызову для debug и release. А если понадобится добавить ещё и superdebug — добавлю эту самую одну строчку.
А так, как вы привели в пример — это нужно "расчёску" добавлять, и ещё и нигде не ошибиться.
Для сборки в несколько потоков еще можно использовать cmake --build --parallel
.
Мало где упоминается одна из важнейших возможностей CMake — INTERFACE targets
, которые могут служить своеобразным контейнером для свойств сборки (путей поиска заголовочных файлов, флагов компиляции, и пр.):
add_library(super INTERFACE)
target_link_directories(super INTERFACE <dirs>)
target_compile_options(super INTERFACE <opts>)
# $A inherits properties from $super
add_library(A <srcs>)
target_link_libraries(A PRIVATE super)
Что касается перечисления файлов с исходным кодом с помощью file(GLOB)
, я сталкивался с двумя проблемами:
- Очевидна необходимость следить за актуальностью проекта вручную (эта проблема вроде решается с
file(GLOB <pattern> CONFIGURE_DEPENDS)
с CMake 3.12). file(GLOB)
не сортирует перечисляемые файлы, что может привести к невоспроизводимым сборкам (эта проблема вроде решена встроенной сортировкой файлов по именам с CMake 3.6), если не сортировать полученный список файлов вручную clist(SORT)
.
Другими словами унаследовать пути до инклудников из подпроекта?
А то чего то задалбливает в корневом проекте все пути до инклудов всех включенных подпроектов перечислять, неправильно это как то.
Про совет 7: Я так делаю только для заголовков, чтобы они корректно отображались в среде разработки. А что на счёт исходников, тут зависит от проекта и структуры папок. Бывают проекты, где исходники должны включаться в сборку условно, или какая-то фишка проекта включена или выключена, или в зависимости от операционной системы и аппаратной платформы, чтобы не включать всё и вся сразу бездумно. Я сам для своих дел использую среду разработки Qt Creator, и она прекрасно интегрируется с CMake, и прям отдельные проекты для разных платформ мне не нужно, исключение лишь Android, но CMake-проект прямо включён в сборку Gradle, и версия приложения читается скриптом через регулярку из CMake-файла (хотя сначала раздельно писал, но мне это уже надоело, потому что забывал версию обновить то тут, то там).
Современный CMake: 10 советов по улучшению скриптов сборки