Как стать автором
Обновить

Комментарии 29

Что тут можно сказать. Я вообще на С++ особенно не пишу, но когда пишу, делаю сборку на CMake. И многое из того, что я нашёл в статье я очень рад узнать, спасибо. Такие вот простые вещи, как cmake --build, например.

Вот по cmake --build. В чем проблема вызова make руками?

На винде нет make, при генерации проекта указываешь -G"Visual Studio 15 2017", а при вызове cmake --build делает чорную магию. Удобно, когда пишешь универсальные скрипты. Пример

В том, что в одном случае это make, в другом mingw32-make, а в третьем вообще nmake :).

Про 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 ещё идти и идти, к сожалению.

Приходится использовать CMake, но очень уж убогий и неудобный у него синтаксис. В этом плане мне гораздо больше нравится SCons.
Я пробовал разобраться в SCons, но так мануалы и не осилил, оказалось быстрее на чистом питоне скрипт написать.
Да, с качественной документацией у него действительно проблема. Но после того, как разобрался во всех необходимых тонкостях, для моих целей вполне неплохо.
Тогда нужен пост на эту тему!
Думаю, что было бы здорово, если бы кто-то написал подобную статью про SCons.
Сам им не владею.

P.S. Проекты собираю по старинке с помощью GNU Make/QMake.
Хороший доклад на руcском языке про cmake:
https://www.youtube.com/watch?v=ckO98bRzL9Y
На самом деле, только основы послушать. Я пробежался по кадрам, cmake там используется именно тот, что они пишут в минимальной версии — 2.8 Это очень старо, начиная с 3 версии добавилось много киллер фич, поэтому их и нужно использовать. Все, о чем говорится в статье, кроме target_compile_features, добавилось с версии 3.0 (а target_compile_features добавилось в 3.1).

А вообще, мой опыт использования открытых библиотек Яндекса говорит о том, что сборочные файлы 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 :)

UPD: Хм, ничего в скриптах не менял. Но на машине с Fedora 26, где последние cmake и ninja, всё вдруг само заработало.
Ninja действительно дает выигрыш, но не сильно большой. ~20 секунд на ~4 минутах сборки всего проекта. Но у нас там CPU — узкое место (шаблонов много...).

С 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 — добавлю эту самую одну строчку.
А так, как вы привели в пример — это нужно "расчёску" добавлять, и ещё и нигде не ошибиться.

Это вопрос фломастеров. Лично я стараюсь избегать состояний. В моём восприятии смена текущей папки — состояние.

add_test — хороший совет. лайк.

Для сборки в несколько потоков еще можно использовать 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), я сталкивался с двумя проблемами:


  1. Очевидна необходимость следить за актуальностью проекта вручную (эта проблема вроде решается с file(GLOB <pattern> CONFIGURE_DEPENDS) с CMake 3.12).
  2. file(GLOB) не сортирует перечисляемые файлы, что может привести к невоспроизводимым сборкам (эта проблема вроде решена встроенной сортировкой файлов по именам с CMake 3.6), если не сортировать полученный список файлов вручную c list(SORT).
А есть ли способ из sub_directory получить его include_directories?
Другими словами унаследовать пути до инклудников из подпроекта?
А то чего то задалбливает в корневом проекте все пути до инклудов всех включенных подпроектов перечислять, неправильно это как то.

Про совет 7: Я так делаю только для заголовков, чтобы они корректно отображались в среде разработки. А что на счёт исходников, тут зависит от проекта и структуры папок. Бывают проекты, где исходники должны включаться в сборку условно, или какая-то фишка проекта включена или выключена, или в зависимости от операционной системы и аппаратной платформы, чтобы не включать всё и вся сразу бездумно. Я сам для своих дел использую среду разработки Qt Creator, и она прекрасно интегрируется с CMake, и прям отдельные проекты для разных платформ мне не нужно, исключение лишь Android, но CMake-проект прямо включён в сборку Gradle, и версия приложения читается скриптом через регулярку из CMake-файла (хотя сначала раздельно писал, но мне это уже надоело, потому что забывал версию обновить то тут, то там).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории