Pull to refresh

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

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

Для сборки в несколько потоков еще можно использовать 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-файла (хотя сначала раздельно писал, но мне это уже надоело, потому что забывал версию обновить то тут, то там).

Sign up to leave a comment.

Articles