Найти в Дзене
coding style

CMake - Практическое руководство. Глава 15. Типы сборки (Крэйг Скотт, перевод на русский язык)

В этой и следующей главах рассматриваются две тесно связанные темы. Тип сборки (также известный как конфигурация сборки или схема сборки в некоторых IDE) - это элемент управления высокого уровня, который выбирает различные варианты поведения компилятора и компоновщика. Манипулирование типом сборки является темой этой главы, в то время как в следующей главе представлены более конкретные детали управления опциями компилятора и компоновщика. Вместе эти главы охватывают материал, который каждый разработчик CMake обычно использует для своих проектов, кроме самых тривиальных. Тип сборки способен так или иначе повлиять практически на все, что связано со сборкой. В первую очередь он напрямую влияет на поведение компилятора и компоновщика, но также оказывает влияние на структуру каталогов, используемых в проекте. Это, в свою очередь, может повлиять на то, как разработчик настраивает свою локальную среду разработки, так что влияние типа сборки может быть весьма существенным. Разработчики обычно
Оглавление

В этой и следующей главах рассматриваются две тесно связанные темы. Тип сборки (также известный как конфигурация сборки или схема сборки в некоторых IDE) - это элемент управления высокого уровня, который выбирает различные варианты поведения компилятора и компоновщика. Манипулирование типом сборки является темой этой главы, в то время как в следующей главе представлены более конкретные детали управления опциями компилятора и компоновщика. Вместе эти главы охватывают материал, который каждый разработчик CMake обычно использует для своих проектов, кроме самых тривиальных.

15.1. Основные понятия

Тип сборки способен так или иначе повлиять практически на все, что связано со сборкой. В первую очередь он напрямую влияет на поведение компилятора и компоновщика, но также оказывает влияние на структуру каталогов, используемых в проекте. Это, в свою очередь, может повлиять на то, как разработчик настраивает свою локальную среду разработки, так что влияние типа сборки может быть весьма существенным.

Разработчики обычно считают, что сборки бывают двух видов: debug (отладочные) или release (релизные). В отладочной сборке флаги компилятора устанавливаются для записи информации, которую отладчики могут использовать для сопоставления машинных инструкций с исходным кодом. Оптимизация в таких сборках часто отключается, чтобы сопоставление машинных инструкций с местоположением исходного кода было прямым и легко отслеживалось при выполнении программы. В релизной сборке, с другой стороны, оптимизация включена полностью, а отладочная информация не генерируется.

Это примеры того, что CMake называет типом сборки. Хотя проекты могут определять любые типы сборки по своему усмотрению, типов сборки предоставляемых CMake по умолчанию обычно достаточно для большинства проектов:

  • Debug Без оптимизаций и с полной отладочной информацией; обычно используется во время разработки и отладки, так как обычно обеспечивает самое быстрое время сборки и наилучшие возможности интерактивной отладки.
  • Release Этот тип сборки обычно обеспечивает полную оптимизацию для повышения скорости и не содержит отладочной информации, хотя некоторые платформы могут генерировать отладочные символы при определенных обстоятельствах. Обычно этот тип сборки используется при создании программного обеспечения для финального выпуска.
  • RelWithDebInfo Это некоторый компромисс между двумя предыдущими. Его цель - обеспечить производительность, близкую к производительности сборки Release, но при этом обеспечить некоторый уровень отладки. Как правило, применяется большинство оптимизаций для повышения скорости, но также включается большинство отладочных функций. Поэтому этот тип сборки наиболее полезен, когда производительность сборки Debug неприемлема даже для сеанса отладки. Обратите внимание, что настройки по умолчанию для RelWithDebInfo отключают инструкции assert.
  • MinSizeRel Этот тип сборки обычно используется только для сред с ограниченными ресурсами, таких как встраиваемые устройства. Код оптимизируется для размера, а не для скорости, и отладочная информация не создается.

Каждый тип сборки приводит к различному набору флагов компилятора и компоновщика. Он также может изменять другие поведения, например, изменять, какие исходные файлы будут компилироваться или с какими библиотеками будет производиться компоновка. Эти детали будут рассмотрены в следующих нескольких разделах, но прежде чем приступить к обсуждению, необходимо понять, как выбрать тип сборки и как избежать некоторых распространенных проблем.

15.1.1. Генераторы с одной конфигурацией

В разделе 2.3, «Генерация файлов проекта», были представлены различные типы генераторов проектов. Некоторые из них, например Makefiles и Ninja, поддерживают только один тип сборки для каждого каталога сборки. Для этих генераторов тип сборки выбирается установкой кэш-переменной CMAKE_BUILD_TYPE. Например, чтобы сконфигурировать и затем собрать проект с помощью Ninja, можно использовать такие команды:

-2

Переменную кэша CMAKE_BUILD_TYPE можно также изменить в приложении CMake GUI, а не из командной строки, но конечный эффект будет тот же. В CMake 3.22 и более поздних версиях, если переменная кэша CMAKE_BUILD_TYPE не установлена, она будет инициализирована из переменной окружения CMAKE_BUILD_TYPE (если она определена).

Вместо того чтобы переключаться между разными типами сборки в одном каталоге, альтернативной стратегией является создание отдельных каталогов для каждого типа сборки, использующих одни и те же исходники. Такая структура каталогов может выглядеть примерно так:

-3

При частом переключении между типами сборки такая схема позволяет избежать необходимости постоянно перекомпилировать одни и те же исходные тексты только из-за изменения флагов компилятора. Это также позволяет генератору с одной конфигурацией эффективно действовать как генератор с несколькими конфигурациями. Среды IDE, такие как Qt Creator, поддерживают переключение между каталогами сборки так же легко, как Xcode или Visual Studio позволяют переключаться между схемами сборки или конфигурациями.

15.1.2. Генераторы с несколькими конфигурациями

Некоторые генераторы, в частности Xcode и Visual Studio, поддерживают несколько конфигураций в одном каталоге сборки. Начиная с CMake 3.17, также доступен генератор Ninja Multi-Config. Эти многоконфигурационные генераторы игнорируют переменную кэша CMAKE_BUILD_TYPE и вместо этого требуют от разработчика выбрать тип сборки в IDE или с помощью опции командной строки во время сборки. Конфигурирование и сборка таких проектов обычно выглядит следующим образом:

-4

При сборке в среде Xcode IDE тип сборки определяется схемой сборки, а в среде Visual Studio тип сборки определяется текущей конфигурацией решения. Обе среды хранят отдельные каталоги для разных типов сборки, поэтому переключение между сборками не приводит к постоянным пересборкам. По сути, делается то же самое, что и при использовании нескольких директорий для сборки, описанном выше для генераторов с одной конфигурацией, просто IDE управляет структурой директорий от имени разработчика.

При сборке с помощью командной строки генератор Ninja Multi-Config обладает немного большей гибкостью по сравнению с другими многоконфигурационными генераторами. Кэш-переменная CMAKE_DEFAULT_BUILD_TYPE может быть использована для изменения конфигурации по умолчанию, которая будет использоваться, если в командной строке сборки не указана конфигурация. Генераторы Xcode и Visual Studio имеют свою собственную фиксированную логику для определения конфигурации по умолчанию в этом сценарии. Генератор Ninja Multi-Config также поддерживает расширенные возможности, позволяющие выполнять пользовательские команды в одной конфигурации, а другие цели собирать с одной или несколькими другими конфигурациями. В большинстве проектов эти расширенные возможности обычно не нужны, но документация CMake по генератору Ninja Multi-Config содержит подробную информацию и примеры.

15.2. Распространенные ошибки

Обратите внимание, что для одноконфигурационных генераторов тип сборки задается во время конфигурирования, в то время как для многоконфигурационных генераторов тип сборки задается во время сборки. Это различие очень важно, поскольку оно означает, что тип сборки не всегда известен, когда CMake обрабатывает файл CMakeLists.txt проекта. Рассмотрим следующий фрагмент кода CMake, который, к сожалению, встречается довольно часто, но демонстрирует неправильное применение

-5

Вышеописанное будет отлично работать для генераторов на основе Makefile и Ninja, но не для Xcode, Visual Studio или Ninja Multi-Config. На практике любая логика, основанная на CMAKE_BUILD_TYPE в проекте, вызывает сомнения, если только она не защищена проверкой, подтверждающей, что используется одноконфигурационный генератор. Для многоконфигурационных генераторов эта переменная, скорее всего, будет пустой, но даже если это не так, ее значение следует считать ненадежным, поскольку сборка будет его игнорировать. Вместо того чтобы ссылаться на CMAKE_BUILD_TYPE в файле CMakeLists.txt, проекты должны использовать другие более надежные альтернативные методы, например генераторные выражения, основанные на $<CONFIG:...>.

При написании скриптов сборки распространенной ошибкой является предположение об использовании определенного генератора или неправильный учет различий между генераторами. В идеале разработчики должны иметь возможность изменить генератор в одном месте, а остальная часть скрипта должна по-прежнему работать корректно. Удобно, что одноконфигурационные генераторы будут игнорировать любые спецификации времени сборки, а многоконфигурационные - переменную CMAKE_BUILD_TYPE, поэтому, указав оба параметра, скрипт может учесть оба случая. Например:

-6

В приведенном выше примере разработчик может просто изменить имя генератора, указанное в параметре -G, и остальная часть сценария будет работать без изменений.

Отсутствие явного задания CMAKE_BUILD_TYPE для одноконфигурационных генераторов также часто встречается, но обычно это не то, что задумал разработчик. Поведение, уникальное для одноконфигурационных генераторов, заключается в том, что если CMAKE_BUILD_TYPE не задан, то тип сборки обычно будет пустым. Это может привести к ошибочному пониманию того, что пустой тип сборки эквивалентен Debug, но это не так. Пустой тип сборки - это собственный уникальный, безымянный тип сборки. В таких случаях не используются специфические для конфигурации флаги компилятора и компоновщика, что часто приводит к вызову компилятора и компоновщика с минимальными флагами. Поведение компилятора и компоновщика определяется их собственными значениями по умолчанию. Хотя это может быть похоже на поведение типа сборки Debug, это ни в коем случае не гарантировано.

Использование компиляторов Visual Studio с одноконфигурационным генератором - это несколько особый случай. Для этого набора инструментов существуют разные библиотеки времени выполнения для отладочных и неотладочных сборок. Пустой тип сборки сделает неясным, какая среда выполнения должна использоваться. Чтобы избежать этой двусмысленности, для данной комбинации тип сборки по умолчанию будет равен Debug.

15.3. Пользовательские типы сборки

Иногда разработчик может захотеть ограничить набор типов сборки подмножеством типов по умолчанию или добавить другие пользовательские типы сборки с особыми флагами компилятора и компоновщика. Хорошим примером последнего является добавление типа сборки для профилирования или покрытия кода, оба из которых требуют особых настроек компилятора и компоновщика.

Существует два основных места, где разработчик может увидеть набор типов сборки. При использовании сред IDE для многоконфигурационных генераторов, таких как Xcode и Visual Studio, IDE предоставляет выпадающий список или что-то подобное, из которого разработчик выбирает конфигурацию, которую он хочет собрать. Для одноконфигурационных генераторов, таких как Makefiles или Ninja, тип сборки вводится непосредственно в кэш-переменную CMAKE_BUILD_TYPE, но графический интерфейс CMake можно сделать таким, чтобы вместо простого текстового поля для редактирования он представлял комбинированное поле с вариантами выбора. Механизмы, лежащие в основе этих двух случаев, различны, поэтому их следует обрабатывать отдельно.

Набор типов сборки, известных многоконфигурационным генераторам, контролируется кэш-переменной CMAKE_CONFIGURATION_TYPES, а точнее, значением этой переменной в конце обработки файла CMakeLists.txt верхнего уровня. Первая встретившаяся команда project() заполняет кэш-переменную, если она еще не была определена. В CMake 3.22 и более поздних версиях переменная окружения CMAKE_CONFIGURATION_TYPES может задавать значение по умолчанию. Если эта переменная окружения не задана или используется более ранняя версия CMake, значением по умолчанию будет (возможно, подмножество) четырех стандартных конфигураций, упомянутых в разделе 15.1, «Основы типов сборки» (Debug, Release, RelWithDebInfo и MinSizeRel).

Проекты могут изменять переменную CMAKE_CONFIGURATION_TYPES после первой команды project(), но только в файле CMakeLists.txt верхнего уровня. Некоторые генераторы CMake полагаются на то, что эта переменная будет иметь постоянное значение во всем проекте. Пользовательские типы сборки можно определить, добавив их в CMAKE_CONFIGURATION_TYPES, а ненужные типы сборки можно удалить из этого списка. Обратите внимание, что изменять следует только некэшируемую переменную, так как изменение кэшируемой переменной может привести к отмене изменений, сделанных разработчиком.

Необходимо следить за тем, чтобы не устанавливать CMAKE_CONFIGURATION_TYPES, если он еще не определен. До CMake 3.9 очень распространенным подходом к определению того, используется ли многоконфигурационный генератор, была проверка того, что CMAKE_CONFIGURATION_TYPES не пуст. До версии 3.11 так поступали даже части самого CMake. Хотя этот метод обычно точен, нередки случаи, когда проекты в одностороннем порядке устанавливают CMAKE_CONFIGURATION_TYPES даже при использовании генератора с одной конфигурацией. Это может привести к принятию неверных решений относительно типа используемого генератора. Для решения этой проблемы в CMake 3.9 было добавлено новое глобальное свойство GENERATOR_IS_MULTI_CONFIG, которое устанавливается в true, если используется генератор с несколькими конфигурациями, обеспечивая точный способ получения этой информации. Несмотря на это, проверка CMAKE_CONFIGURATION_TYPES все еще распространена, так что новые проекты должны изменять ее на проверку GENERATOR_IS_MULTI_CONFIG. Следует также отметить, что до CMake 3.11 добавление пользовательских типов сборки в CMAKE_CONFIGURATION_TYPES было небезопасным. Некоторые части CMake учитывали только типы сборки по умолчанию, но даже в этом случае проекты могут с пользой определять пользовательские типы сборки в более ранних версиях CMake, в зависимости от того, как они будут использоваться. Тем не менее, для большей надежности рекомендуется использовать по крайней мере CMake 3.11, если предполагается определять пользовательские типы сборки.

Другой аспект этой проблемы заключается в том, что разработчики могут добавлять свои собственные типы в кэш-переменную CMAKE_CONFIGURATION_TYPES и/или удалять те, которые их не интересуют. Поэтому проекты не должны полагаться на предположения о том, какие типы конфигурации определены, а какие нет.

Учитывая все вышесказанное, следующий шаблон показывает предпочтительный способ добавления проектами собственных типов сборки для генераторов с поддержкой мультиконфигурации:

-7

Для генераторов с одной конфигурацией существует только один тип сборки. Он задается кэш-переменной CMAKE_BUILD_TYPE, которая представляет собой строку. В графическом интерфейсе CMake она обычно представлена в виде текстового поля для редактирования, поэтому разработчик может задать в ней произвольное содержимое. Как уже говорилось в разделе 10.6, «Свойства кэш-переменных», переменные кэша могут иметь свойство STRINGS, в котором задается набор допустимых значений. Тогда приложение CMake GUI будет представлять эту переменную не в виде текстового поля для редактирования, а в виде комбобокса, содержащего допустимые значения.

-8

Свойства могут быть изменены только из файлов CMakeLists.txt проекта, поэтому можно смело устанавливать свойство STRINGS, не беспокоясь о сохранении изменений разработчиков. Обратите внимание, однако, что установка свойства STRINGS для переменной кэша не гарантирует, что переменная кэша будет содержать одно из представленных значений, а лишь управляет тем, как переменная будет отображена в приложении CMake GUI. Разработчики по-прежнему могут установить CMAKE_BUILD_TYPE на любое значение в командной строке cmake или отредактировать файл CMakeCache.txt вручную. Для жесткого требования, чтобы переменная имела одно из определенных значений, проект должен сам явно выполнить эту проверку.

-9

Значение по умолчанию для CMAKE_BUILD_TYPE - пустая строка, поэтому описанное выше будет приводить к фатальной ошибке как для одноконфигурационных, так и для многоконфигурационных генераторов, если разработчик не задаст его явно. Это нежелательно, особенно для многоконфигурационных генераторов, которые даже не используют значение переменной CMAKE_BUILD_TYPE. С этим можно справиться, предоставив проекту значение по умолчанию, если CMAKE_BUILD_TYPE не была задана. Более того, техники для генераторов с несколькими и одной конфигурацией можно и нужно объединить, чтобы обеспечить надежное поведение для всех типов генераторов. Конечный пример будет выглядеть так:

-10

Приведенные выше методы позволяют выбрать пользовательский тип сборки, но они ничего не определяют в этом типе сборки. Выбор типа сборки определяет, какие переменные и в какой конфигурации следует использовать. Он также влияет на любые генераторные выражения, логика которых зависит от текущей конфигурации ($<CONFIG> и $<CONFIG:...>). Эти переменные и генераторные выражения рассматриваются в разделе 16.6, «Переменные компилятора и компоновщика», и в разделе 24.3, «Выбор инструмента». На данный момент основной интерес представляют следующие семейства переменных.

  • CMAKE_<LANG>_FLAGS_<CONFIG>
  • CMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>

Флаги, указанные в этих переменных, добавляются к набору по умолчанию, предоставляемому одноименными переменными без суффикса _<CONFIG>. Пользовательский тип сборки Profile может быть определен следующим образом:

-11

Для простоты примера выше предполагается наличие GCC-совместимого компилятора. Пример включает профилирование, а также отладочные символы и большинство оптимизаций. Альтернативный вариант - взять за основу флаги компилятора и компоновщика одного из типов сборки по умолчанию и добавить к ним дополнительные флаги. Это можно сделать после команды project(), поскольку эта команда заполняет переменные флагов компилятора и компоновщика по умолчанию. Для примера с профилированием в качестве базовой конфигурации хорошо подходит тип сборки по умолчанию RelWithDebInfo, поскольку он позволяет выполнять как отладку, так и большинство оптимизаций:

-12

Для каждой пользовательской конфигурации должны быть определены соответствующие переменные-флаги компилятора и компоновщика. Для некоторых типов генераторов с несколькими конфигурациями CMake проверит наличие требуемых переменных и выдаст ошибку, если они не заданы.

Определение пользовательского типа сборки в файле CMakeLists.txt проекта имеет свои недостатки. При жестком кодировании флагов компилятора и компоновщика разработчик не может изменить их без модификации проекта. Использование кэш-переменных вместо обычных переменных может показаться хорошим решением, но проекты не могут реализовать это надежно для всех сценариев. Лучшей альтернативой было бы определение кэш-переменных в предустановках CMake или файлах цепочки инструментов, над которыми разработчик имеет полный контроль. Обсуждение этих возможностей см. в главе 42, «Предустановки» и разделе 24.1, «Файлы цепочки инструментов».

Еще одна переменная, которая иногда может быть определена для пользовательского типа сборки, - CMAKE_<CONFIG>_POSTFIX. Она используется для инициализации свойства <CONFIG>_POSTFIX каждой цели-библиотеки, значение которого будет добавлено к имени выходного файла при сборке для указанной конфигурации. Это позволяет помещать библиотеки из нескольких типов сборки в один каталог, не перезаписывая друг друга. CMAKE_DEBUG_POSTFIX часто устанавливается в значения типа d или _debug, особенно для сборок Visual Studio, где для сборок Debug и не-Debug должны использоваться разные DLL, поэтому в пакете могут потребоваться библиотеки для обоих типов сборок. В случае с пользовательским типом сборки Profile, определенным выше, пример может быть таким:

-13

При создании пакетов, содержащих несколько типов сборок, настоятельно рекомендуется задавать CMAKE_<CONFIG>_POSTFIX для каждого типа сборки. По соглашению, постфикс для сборок типа Release обычно пуст. Обратите внимание, что свойство цели <CONFIG>_POSTFIX игнорируется на платформах Apple.

По историческим причинам элементы, передаваемые команде target_link_libraries(), могут быть снабжены ключевыми словами debug или optimized, чтобы указать, что именованный элемент должен быть подключен только для отладочных или неотладочных сборок соответственно. Тип сборки считается отладочным, если он указан в глобальном свойстве DEBUG_CONFIGURATIONS, в противном случае он считается оптимизированным (неотладочным). Для пользовательских типов сборки следует добавить их имя в это глобальное свойство, если они должны рассматриваться как отладочные сборки в данном сценарии. Например, если проект определяет свой собственный тип сборки под названием StrictChecker, и этот тип сборки должен считаться отладочным типом сборки, он может (и должен) указать это следующим образом:

-14

Новые проекты обычно предпочитают использовать генераторные выражения вместо ключевых слов debug и optimized в команде target_link_libraries(). Более подробно эта область рассматривается в следующей главе.

15.4. Рекомендуемые практики

Разработчики не должны предполагать, что для сборки их проекта используется определенный генератор CMake. Другой разработчик того же проекта может предпочесть использовать другой генератор, потому что он лучше интегрируется с его средством IDE. Или же в будущей версии CMake может быть добавлена поддержка нового типа генератора, который может принести другие преимущества. Некоторые инструменты сборки могут содержать ошибки, которые впоследствии могут повлиять на проект, поэтому может быть полезно иметь альтернативные генераторы, на которые можно опереться, пока эти ошибки не будут исправлены. Расширение набора поддерживаемых проектом платформ также может быть затруднено, если предполагается использование определенного генератора CMake.

При использовании генераторов с одной конфигурацией, таких как Makefiles или Ninja, подумайте об использовании нескольких каталогов сборки, по одной для каждого типа сборки. Это позволит переключаться между типами сборки без необходимости полной перекомпиляции каждый раз. Это обеспечивает поведение, схожее с тем, что предлагают многоконфигурационные генераторы, и может быть полезным способом позволить таким инструментам IDE, как Qt Creator, имитировать многоконфигурационную функциональность.

Для одноконфигурационных генераторов рассмотрите возможность установки CMAKE_BUILD_TYPE на лучшее значение по умолчанию, если оно пустое. Хотя пустой тип сборки технически допустим, разработчики часто неправильно понимают, что он означает сборку Debug, а не отдельный тип сборки. Кроме того, не создавайте логику, основанную на CMAKE_BUILD_TYPE, пока не будет подтверждено, что используется генератор с одной конфигурацией. Даже в этом случае такая логика, скорее всего, будет хрупкой и, вероятно, может быть лучше выражена с большей общностью и надежностью с помощью генераторных выражений.

Модифицируйте переменную CMAKE_CONFIGURATION_TYPES, только если известно, что используется многоконфигурационный генератор, или если эта переменная уже существует. Если вы добавляете пользовательский тип сборки или удаляете один из типов сборки по умолчанию, не изменяйте переменную кэша. Вместо этого измените одноименную обычную переменную, которая будет иметь приоритет над кэш-переменной. Кроме того, при изменении одного из типов сборки по умолчанию предпочитайте добавлять и удалять отдельные элементы, а не полностью заменять список. Эти меры помогут избежать вмешательства в изменения, вносимые разработчиком в переменную кэша. Вносите такие изменения только в файл CMakeLists.txt верхнего уровня.

Если требуется CMake 3.9 или более поздняя версия, используйте глобальное свойство GENERATOR_IS_MULTI_CONFIG для окончательного запроса типа генератора вместо того, чтобы полагаться на существование CMAKE_CONFIGURATION_TYPES для выполнения менее надежной проверки.

Распространенной, но неверной практикой является запрос свойства цели LOCATION для выяснения имени выходного файла цели. Связанная с этим ошибка заключается в предположении определенной структуры каталогов сборки в пользовательских командах (см. главу 20, Пользовательские задачи). Эти методы работают не для всех типов сборки, поскольку LOCATION не известно на стадии конфигурирования для генераторов с несколькими конфигурациями, а структура выходного каталога сборки обычно отличается для различных типов генераторов CMake. Вместо них следует использовать генераторные выражения типа $<TARGET_FILE:...>, поскольку они надежно указывают необходимый путь для всех генераторов, как одноконфигурационных, так и многоконфигурационных.

Это был ознакомительный фрагмент книги Professional CMake: A Practical Guide by Craig Scott