Слабые стороны cmake
Прим. от 29.10.2015: Ничего не стоит на месте, мы приобретаем новый опыт, программы совершенствуются и проблемы описанные в этой заметке давно не соответствуют действительности за давностью лет.
Хочу рассказать о слабых местах cmake. Возможно эта заметка поможет тому, кто начинает новый проект и нужно принять решение собирать его используя cmake или нет.
Два слова о том где я использую cmake и почему на мой опыт можно принимать во внимание. Как я уже писал где-то ранее в блоге, мой основной проект - это браузер webkit для автомобильных систем и для сборок мы используем cmake. Для сборки используется Ubuntu, а целевые платформы это qnx arm/i386 и linux arm. В нашем случае cmake генерирует makefile'ы, а дальнейшая сборка идёт при помощи обычного make. Чтобы представить весь зоопарк, который нам нужно собрать скажу только, что только в пределах одной платформы webkit собирается в 2-3 конфигурациях с разным функционалом. Решение взять cmake было продиктовано тем, что в webkit комьюнити уже имеется вариант сборки при помощи cmake. Поначалу мы использовали cmake скрипты из комьюнити, но когда список конфигураций перевалил за десяток - стало резко неудобно, поскольку cmake скрипты превратились в лапшу из наших патчей. В итоге я переписал с нуля все cmake скрипты для наших нужд.
Чем больше я углублялся в сборку на базе cmake, тем явственнее вылазили его слабые места. У cmake есть много вещей на которые можно ткнуть пальцем и сказать "это должно быть проще/лучше/понятнее", но в целом это можно так или иначе решить. То, слабое место о котором я хотел бы рассказать - это кросскомпиляция.
Toolchain
Первое с чем сталкивается разработчик кросплатформенного проекта - это toolchain, т.е. набор утилит с помощью которого производится сборка под целевую платформу и архитектуру. К примеру в мире скриптов configure
принято, что toolchain можно переопределить с помощью переменных окружения, например CC
и LD
, которые указывают компилятор и линковщик. В мире cmake можно забыть об этом - cmake не только не видит этих общепринятых переменных окружения, но и своих не имеет. Идеология cmake подразумевает, что ваша основаная задача - это собрать проект только для той платформы на которой запускается cmake, т.е. текущей.
Для понимания слабой стороны, надо в понять как работает cmake. В двух словах это можно описать примерно так: cmake определяет на какой платформе он запустился, в зависимости от платформы подгружает свои конфигурационные скрипты, выбирает стандартный для данной платформы компилятор+линковщик, в соответствии с платформой/компилятором устанавливает набор разных опций, затем несколько раз запускает компилятор, чтобы собрать о нём информацию, затем загружает главный скрипт CMakefile.txt
, приступает к его разбору и наконец генерирует makefiles (либо проекты для IDE). Всё это удобно пока ваша целевая платформа та, на которой вы собираетесь.
Что надо сделать чтобы запустить кросскомпиляцию под совершенно другую архитектуру и платформу? Тут мы сталкиваемся с первой непродуманностью: кросскомпиляция, по всей видимости, изначально не задумывалась (это моё субъективное мнение) и создаётся впечатление, что её прилепили гараздо позже как смогли. Так вот, чтобы указать список компиляторов и линковщиков для cmake надо создать специальный файл, к примеру toolchain.cmake
, с таким содержимым:
set(CMAKE_C_COMPILER my_arm_gcc)
set(CMAKE_CXX_COMPILER my_arm_g++)
set(CMAKE_AR my_arm_ar)
После того как файл создан, надо запустить cmake и указать ему этот файл в переменной CMAKE_TOOLCHAIN_FILE
:
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake
Т.е. чтобы собрать под другую платформу, требуется создать дополнительный файл, присвоить его имя переменной CMAKE_TOOLCHAIN_FILE
, которую можно передать только в аргументах cmake. Иначе никак. Я это нахожу достаточно неудобным. Возможно, те кто уже имел дело с cmake спросят: «а почему бы не указать переменные CMAKE_C_COMPILER, CMAKE_CXX_COMPILER и другие непосредственно внутри основного скрипта? Зачем этот отдельный файл?». Короткий ответ: из-за особенности cmake не получится.
Развёрнутый ответ: это требуется из-за того, что cmake работает с некоторыми переменными иначе чем со всеми остальными. В cmake есть некоторое количество переменных которые cmake отслеживает и если они в какой-то момент изменились, то выполняется некоторое действие. Так, например, если установить CMAKE_C_COMPILER
или CMAKE_CXX_COMPILER
в новое значение, то cmake это отследит и запустит диагностику его возможностей. Поэтому, после установки этих переменных происходит перезапуск стадии диагностики и последующая повторная загрузка основного CMakefile.txt в котором... снова установка переменной компилятора. В общем чистой воды рекурсия. Чтобы её избежать и требуется внешний файл, который загрузится до основного скрипта только один раз и никаких перезапусков основного скрипта не потребуется.
Замена линковщика
У cmake есть ещё одна неприятная особенность – у него нельзя штатными средствами изменить линковщик отличный от того, что выбрал cmake. На linux/unix платформах gcc является универсальным фасадом для целого набора утилит и в зависимости от того, что будет на входе, gcc может либо компилировать исходный код, либо линковать объектные файлы в финальный исполняемый бинарник. Поэтому если ваш основной toolchain основан на gcc, то cmake для линковки и компиляции будет использовать только фасад gcc. Отсюда вытекает неожиданное следствие - переменная CMAKE_C_COMPILER
определяет не только компилятор, но и линковщик. Переменных чтобы установить линковщик попросту нет.
Чем же это плохо? Пример из нашего проекта: мы столкнулись с тем, что qnx toolchain существует только 32 разрядный и при debug сборке некоторые статические библиотеки превышают 2Гб. Как следствие у 32 разрядного qnx toolchain начинаются проблемы с их линковкой. Было принято решение заменить штатный qnx линковщик на более современный gold
, но нас ждал большой сюрприз, когда выяснилось, что мы этого не можем сделать без глубоких хаков.
На счёт глубоких хаков. После некоторых поисков и изучения внутренних скриптов cmake я обнаружил, что стадию линковки всё же можно изменить переопределив переменную CMAKE_C_LINK_EXECUTABLE
(важно: эта переменная - полная командная строка с аргументами для линковки, а не просто имя линковщика). Но даже используя эту переменную нам не получилось толком подменить линковщик на gold, поскольку cmake считает, что линковщик только gcc совместимый и передаёт ему аргументы как для gcc, а gold их не понимает. В итоге мы отказались от этой затеи и для debug сборки мы разделили большие библиотеки на более маленькие.
Выводы
Исходя из личного опыта я бы не рекомендовал cmake для сложных проектов c кросскомпиляцией под множественные платформы и большим разнообразием toolchain'ов. До поры до времени, вы сможете решать встающие перед вами ограничения, но рано или поздно вы упрётесь во что-то, что не сможете обойти без грязного хака (при условии что вы найдёте его). В этой заметке я описал только два существенных упущения в дизайне cmake, но на самом деле их немного больше чем два, но писать об этом можно долго. В таких проектах всё же лучше использовать старый добрый make поскольку он позволяет гараздо обширнее контролировать этапы сборки и toolchain'ы.