stunpix

Персональный бортжурнал

Слабые стороны 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'ы.

Коментарии