При запуске CMake разработчики обычно думают, что это один шаг, который включает чтение файла CMakeLists.txt и создание соответствующего набора проектных файлов, специфичных для генератора (например, файлов решений и проектов Visual Studio, проектов Xcode, файлов Unix Makefiles или входных файлов Ninja). Однако здесь есть два совершенно разных этапа. При запуске CMake конец вывода в консоль обычно выглядит примерно так:
Когда вызывается CMake, он сначала считывает и обрабатывает файл CMakeLists.txt, расположенный в верхней части иерархии файлов исходного кода, включая все остальные файлы, к которым он обращается. По мере выполнения команд, функций и т. д. создается внутреннее представление проекта. Это называется шагом конфигурирования. На этом этапе создается большая часть вывода в консоль, включая содержимое команд message(). В конце этапа конфигурирования в консоль выводится сообщение -- Configuring done.
После того как CMake закончил чтение и обработку файла CMakeLists.txt, он выполняет этап генерации. На этом этапе создаются файлы проекта инструмента сборки с использованием внутреннего представления, созданного на этапе конфигурирования. По большей части разработчики склонны игнорировать этап генерации. В консоли почти всегда появляется сообщение -- Generating done сразу после завершения шага конфигурирования, так что это вполне объяснимо. Но есть ситуации, когда понимание разделения на два разных этапа особенно важно.
Рассмотрим проект, созданный для генератора CMake с несколькими конфигурациями, например Xcode, Visual Studio или Ninja Multi-Config. Когда читаются файлы CMakeLists.txt, CMake не знает, для какой конфигурации будет собираться цель. Это многоконфигурационная настройка, поэтому есть несколько вариантов (например, Debug, Release и т. д.). Разработчик выбирает конфигурацию во время сборки, уже после завершения работы CMake. Это может создать проблему, если файл CMakeLists.txt хочет сделать что-то вроде копирования файла в тот же каталог, что и конечный исполняемый файл для данной цели, поскольку расположение этого каталога зависит от того, какая конфигурация собирается. Необходимо указать CMake «Для той конфигурации, которая собирается, используйте каталог конечного исполняемого файла».
Это яркий пример функциональности, предоставляемой генераторными выражениями. Они позволяют закодировать некоторую логику, которая не оценивается во время конфигурирования, а откладывается до этапа генерации, когда происходит запись файлов проекта. Они могут использоваться для выполнения условной логики, получения информации о различных аспектах сборки, таких как каталоги, имена вещей, детали платформы и многое другое. Они даже могут использоваться для предоставления различного содержимого в зависимости от того, выполняется сборка или установка.
Генераторные выражения можно использовать не везде, но они поддерживаются во многих местах. В справочной документации CMake, если определенная команда или свойство поддерживает генераторные выражения, об этом будет явно указано. Набор поддерживаемых генераторных выражений и свойств, их поддерживающих, со временем расширялся. Проектам следует убедиться, что для минимальной версии CMake, которая им требуется, изменяемые свойства действительно поддерживают используемые генераторные выражения.
11.1. Простая булева логика
Генераторное выражение задается с помощью синтаксиса $<...>, где содержимое между угловыми скобками может принимать несколько различных форм. Как станет ясно вскоре, важной особенностью является условное включение содержимого. Самыми основными генераторными выражениями для этого являются следующие:
Для $<1:...> результатом выражения будет часть ..., в то время как для $<0:...> часть ... игнорируется, и выражение приводит к пустой строке. По сути, это условные выражения true и false, но, в отличие от переменных, понятие true и false допускает только эти два конкретных значения. Любые значения, отличные от 0 или 1 для условного выражения, CMake отвергает с фатальной ошибкой. Чтобы сделать оценку булевых выражений более гибкой и гарантировать, что содержимое оценивается в 0 или 1, можно использовать другое генераторное выражение:
Оно оценивает содержимое ... так же, как команда if() оценивает булеву константу, поэтому она понимает все обычные специальные строки, такие как OFF, NO, FALSE и так далее. Очень часто его используют для обертывания переменной, которая должна содержать булево значение, но которое может быть отличным от 0 или 1 (примеры см. в таблице чуть ниже).
Также поддерживаются логические операции:
Выражения AND и OR могут принимать любое количество аргументов, разделенных запятыми, и выдавать соответствующий логический результат, в то время как NOT принимает только одно выражение и выдает отрицание своего аргумента. Поскольку AND, OR и NOT требуют, чтобы их выражения разрешались только в 0 или 1, подумайте о том, чтобы обернуть эти выражения в $<BOOL:...>, чтобы гарантировать правильную оценку истинности или ложности результата.
В CMake 3.8 и более поздних версиях логику if-then-else также можно очень удобно выразить с помощью специального выражения $<IF:...>:
Как обычно, значение expr должно равняться 1 или 0. Результатом будет val1, если значение expr равно 1, и val0, если значение expr равно 0. До CMake 3.8 эквивалентная логика должна была быть выражена следующим более длинным способом, требующим дважды указывать выражение:
Генераторные выражения могут быть вложенными, что позволяет строить выражения произвольной сложности. В приведенном выше примере показано вложенное условие, но вложенной может быть любая часть генераторного выражения. Следующие примеры демонстрируют рассмотренные до сих пор возможности:
Как и в случае с командой if(), CMake также поддерживает проверку строк, чисел и версий в генераторных выражениях, хотя синтаксис немного отличается. Все приведенные ниже выражения оцениваются в 1, если соответствующее условие выполнено, или в 0 в противном случае.
Еще одно очень полезное условное выражение - проверка типа сборки:
Это значение будет равно 1, если arg соответствует фактически собираемому типу сборки, и 0 для всех остальных типов сборки. Обычно это используется для предоставления флагов компилятора только для отладочных сборок или для выбора различных реализаций разных типов сборок. Например:
Вышеописанное скомпонует исполняемый файл с библиотекой CheckedAlgo для сборок Debug и с библиотекой FastAlgo для всех остальных типов сборок. Генераторное выражение $<CONFIG:...> - единственный способ надежно обеспечить такую функциональность, которая работает во всех типах генераторов CMake, включая генераторы с поддержкой мультиконфигурации, такие как Xcode, Visual Studio или Ninja Multi-Config. Более подробно эта тема рассматривается в разделе 15.2, «Распространенные ошибки».
CMake предлагает еще больше условных тестов, основанных на таких параметрах, как информация о платформе и компиляторе, настройки политики CMake и т. д. Разработчикам следует обратиться к справочной документации CMake для получения полного набора поддерживаемых условных выражений.
11.2. Сведения о цели
Еще одно распространенное использование генераторных выражений - предоставление информации о целях. Любое свойство цели можно получить с помощью одной из следующих двух форм:
Первая форма предоставляет значение определенного свойства для указанной цели, в то время как вторая форма извлекает свойство из цели, для которой генераторное выражение было применено.
Хотя TARGET_PROPERTY - это очень гибкий тип выражения, он не всегда является лучшим способом получения информации о цели. Например, CMake также предоставляет другие выражения, которые дают подробную информацию о директории и имени собранного бинарного файла цели. Эти более строгие выражения позволяют извлекать части некоторых свойств или вычислять значения на основе необработанных свойств. Наиболее общим из них является набор генераторных выражений TARGET_FILE:
- TARGET_FILE Здесь указывается абсолютный путь и имя собранного бинарного файла цели, включая префикс и суффикс файла, если это необходимо для данной платформы (например, .exe, .dylib). Для платформ на базе Unix, где разделяемые библиотеки обычно имеют сведения о версии в имени файла, они также будут включены.
- TARGET_FILE_NAME То же самое, что и TARGET_FILE, но без пути (т. е. предоставляется только часть с именем файла).
- TARGET_FILE_DIR То же, что и TARGET_FILE, но без имени файла. Это самый надежный способ получить директорию, в которую будет собран конечный исполняемый файл или библиотека. Его значение отличается для разных конфигураций сборки при использовании генератора мультиконфигурации, например Xcode, Visual Studio или Ninja Multi-Config.
Приведенные выше три выражения TARGET_FILE особенно полезны при определении пользовательских правил для копирования файлов на этапах после сборки (см. раздел 20.2, «Добавление этапов сборки в существующую цель»). В дополнение к выражениям TARGET_FILE CMake также предоставляет несколько специфических для библиотек выражений, которые выполняют аналогичные функции, за исключением того, что они немного по-другому обрабатывают префикс и/или суффикс имени файла. Эти выражения имеют имена, начинающиеся с TARGET_LINKER_FILE и TARGET_SONAME_FILE, и, как правило, используются не так часто, как выражения TARGET_FILE.
В CMake 3.15 добавлена поддержка дополнительных генераторных выражений, извлекающих базовое имя, префикс и суффикс имен файлов, связанных с целью. Проектам, нуждающимся в этих более тонких деталях, следует обратиться к документации CMake, но такая необходимость должна возникать редко.
Проекты, поддерживающие платформу Windows, также могут получить подробную информацию о PDB-файлах для заданной цели. Выражения, начинающиеся с TARGET_PDB_FILE, работают по аналогии с TARGET_PROPERTY, предоставляя сведения о пути и имени файла PDB, используемого для цели, для которой используется генераторное выражение.
Еще одно генераторное выражение, связанное с целями, заслуживает отдельного упоминания. CMake позволяет определить цель как объектную библиотеку, то есть это не библиотека в обычном смысле, а просто набор объектных файлов, которые CMake связывает с целью, но не приводит к созданию конечного файла библиотеки. При использовании CMake 3.11 или более ранних версий невозможно ссылаться на такую библиотеку. Вместо этого объектные библиотеки должны быть добавлены к целям тем же способом, что и файлы исходного кода. Затем CMake включает эти объектные файлы на этапе компоновки так же, как и объектные файлы, созданные при компиляции исходных текстов цели. Для этого используется генераторное выражение $<TARGET_OBJECTS:...>, которое перечисляет объектные файлы в форме, удобной для использования add_executable() или add_library(), как показано в следующем примере:
В приведенном выше примере отдельная библиотека для ObjLib не создается, но исходные файлы src1.cpp и src2.cpp все равно компилируются только один раз. Это может быть удобнее для некоторых сборок, поскольку позволяет избежать затрат времени на создание статической библиотеки или затрат времени выполнения на разрешение символов для динамической библиотеки, но при этом избежать необходимости компилировать одни и те же исходные тексты несколько раз.
Начиная с CMake 3.12, можно ссылаться непосредственно на объектую библиотеку, а не использовать $<TARGET_OBJECTS:...>, как описано выше. На такое связывание накладываются ограничения, подробности которых обсуждаются в разделе 19.2, «Библиотеки».
11.3. Общая информация
Генераторные выражения могут предоставлять информацию не только о целях. Можно получить информацию об используемом компиляторе, платформе, для которой собирается цель, имени конфигурации сборки и многое другое. Подобные выражения обычно используются в более сложных ситуациях, например, для работы с пользовательским компилятором или для решения проблем, характерных для конкретного компилятора или набора инструментов. Эти выражения также могут использоваться не по назначению, поскольку может показаться, что они предоставляют возможность создавать пути к объектам, которые в противном случае можно было бы получить более надежными методами, например, с помощью выражений TARGET_FILE или других функций CMake. Разработчикам следует хорошо подумать, прежде чем полагаться на более общие генераторные выражения как на способ решения проблемы. Тем не менее, некоторые из этих выражений действительно можно использовать. Некоторые из наиболее распространенных приведены здесь в качестве отправной точки для дальнейшего изучения:
- $<CONFIG> Оценивает тип сборки. Используйте это выражение вместо переменной CMAKE_BUILD_TYPE, поскольку эта переменная не используется в генераторах проектов с несколькими конфигурациями, таких как Xcode, Visual Studio или Ninja Multi-Config. В предыдущих версиях CMake для этого использовалось устаревшее выражение $<CONFIGURATION>, но теперь проекты должны использовать только $<CONFIG>.
- $<PLATFORM_ID> Идентифицирует платформу, для которой собирается цель. Это может быть полезно в ситуациях кросс-компиляции, особенно когда сборка может поддерживать несколько платформ (например, сборки для умных устройств и симуляторов). Это генераторное выражение тесно связано с переменной CMAKE_SYSTEM_NAME, и проектам следует подумать, не будет ли использование этой переменной более простым в их конкретной ситуации.
- $<C_COMPILER_VERSION>, $<CXX_COMPILER_VERSION> В некоторых ситуациях бывает полезно добавлять содержимое только в том случае, если версия компилятора старше или новее некоторой определенной версии. Этого можно добиться с помощью генераторных выражений $<VERSION_xxx:...>. Например, чтобы вывести строку OLDCXX, если версия компилятора C++ меньше 4.2.0, можно использовать следующее выражение:
Такие выражения обычно используются только в ситуациях, когда тип компилятора известен, а специфическое поведение компилятора должно быть обработано проектом особым образом. Это может быть полезным приемом в определенных ситуациях, но и может снизить переносимость проекта, если он слишком сильно полагается на такие выражения.
11.4. Выражения обработки путей
В CMake 3.24 добавлена поддержка двух выражений для работы с путями:
- $<PATH_EQUAL:path1,path2> Это эквивалент пути $<STREQUAL:string1,string2>. Когда ожидается, что сравниваемые объекты будут путями, а не произвольными строками, $<PATH_EQUAL:...> более четко выражает это ожидание. Преимущество этого метода в том, что он сравнивает каждую часть пути по отдельности, эффективно сворачивая несколько последовательных разделителей каталогов в один (ожидается, что они будут использовать прямые косые черты). Пути можно обернуть с помощью $<PATH:CMAKE_PATH,...>, чтобы убедиться, что они имеют требуемую форму:
- $<PATH:subcommand,...> По сути, это эквивалент команды стадии конфигурирования cmake_path() (подробно рассматривается в разделе 21.1.1, «cmake_path()»). Поддерживается тот же набор операций, хотя синтаксис имеет некоторые отличия. Полный список поддерживаемых подкоманд можно найти в руководстве по генераторным выражениям официальной документации CMake. Ниже приведены некоторые примеры, чтобы дать представление о том, что возможно.
В CMake 3.27 и более поздних версиях различные выражения $<PATH:subcommand,...> также принимают список путей. При передаче списка путей подкоманда применяет операцию к каждому пути и выдает список в качестве результата.
11.5. Выражения обработки списков
В CMake 3.27 добавлено семейство выражений $<LIST:...> которые предоставляют возможности аналогичные возможностям команды стадии конфигурирования list(), но на стадии генерации. Генераторные выражения требуют некоторого дополнительного внимания к кавычкам, точкам с запятой и запятым, чтобы аргументы выражения правильно разбирались CMake. Поддерживаемые операции со списками см. в разделе 6.7, «Списки», или в руководстве по генераторным выражениям в документации CMake.
В более ранних версиях CMake поддерживается несколько других выражений обработки списков. Выражения $<LIST:...> следует использовать, если это позволяет минимальная версия CMake, но в более ранних версиях вместо них можно использовать следующие.
- $<JOIN:list,...> Эффект этого генераторного выражения заключается в замене каждой точки с запятой в списке на содержимое ..., эффективно соединяя элементы списка с ... между каждым. Выражение также удалит все пустые элементы из списка. Если сохранение пустых элементов важно, используйте вместо этого $<LIST:JOIN,...>. Обратите внимание, что, как и во многих других генераторных выражениях, $<JOIN:...> никогда не следует использовать без кавычек. Это не позволит точкам с запятой в списке действовать как разделители аргументов для команды, в которой используется генераторное выражение (более подробное обсуждение этой темы см. в разделе 9.8, «Проблемы с обработкой аргументов»). Кавычки также необходимы для того, чтобы любые пробелы в составе ... не выступали в качестве разделителей аргументов. Ниже показан очень распространенный пример неправильного использования этого генераторного выражения:
- $<REMOVE_DUPLICATES:list> Это выражение удаляет все дубликаты, оставляя только первый экземпляр. Обратите внимание, что оно также применяется к пустым элементам списка. Это выражение поддерживается только с CMake 3.15 или более поздней версией. Если проект использует CMake 3.27 или более позднюю версию в качестве минимальной, предпочтите использовать $<LIST:REMOVE_DUPLICATES,list> вместо этого, так как $<REMOVE_DUPLICATES:...> может быть устаревшим в будущей версии CMake.
- $<FILTER:list,INCLUDE,regex>, $<FILTER:list,EXCLUDE,regex> Фильтрует список, сохраняя только те элементы, которые соответствуют или не соответствуют указанному регулярному выражению regex. Это то же самое, что и $<LIST:FILTER,list,...>, который следует использовать вместо этого, если проект требует CMake 3.27 или более поздней версии в качестве минимальной. $<FILTER:...> поддерживается в CMake 3.15 или более поздней версии, но может быть устаревшим в будущей версии CMake.
11.6. Вспомогательные выражения
Некоторые генераторные выражения изменяют содержимое или заменяют специальные символы. Ниже приведены некоторые из них, которые чаще всего используются или легко понимаются неправильно.
- $<COMMA> Бывают ситуации, когда запятую необходимо включить в генераторное выражение, но это нарушает синтаксис самого генераторного выражения. Чтобы обойти такие случаи, вместо запятой можно использовать $<COMMA>, чтобы предотвратить ее разбор как части синтаксиса.
- $<SEMICOLON> Как и в предыдущем случае, точка с запятой, встроенная в генераторное выражение, может быть разобрана CMake как разделитель аргументов команды. При использовании $<SEMICOLON> разбор аргументов не будет видеть необработанный символ точки с запятой, поэтому такого разделения аргументов не произойдет.
- $<LOWER_CASE:...>, $<UPPER_CASE:...> Любое содержимое может быть преобразовано в нижний или верхний регистр с помощью этих выражений. Это может быть особенно полезно в качестве шага перед выполнением сравнения строк. Например:
- $<GENEX_EVAL:...>, $<TARGET_GENEX_EVAL:target,...> В некоторых более сложных сценариях может возникнуть ситуация, когда оценка генераторного выражения приводит к содержимому, которое само содержит генераторные выражения. Например, при оценке свойства цели с помощью $<TARGET_PROPERTY:...> значение полученного свойства содержит другое генераторное выражение. Обычно полученное свойство не обрабатывает далее содержащихся в нем генераторных выражений, но оно может быть принудительным с помощью $<GENEX_EVAL:...> или $<TARGET_GENEX_EVAL:...>. Эти два генераторных выражения были введены в CMake 3.13. Проекты редко должны использовать эти два генераторных выражения. Приведенный ниже пример демонстрирует основную мотивацию, по которой они были добавлены в CMake, но этот сценарий не должен возникать в большинстве случаев.
11.7. Рекомендуемые практики
По сравнению с другими функциональными возможностями, генераторные выражения - это недавно добавленная функция CMake. Из-за этого в большинстве материалов в Интернете и других источниках, посвященных CMake, они, как правило, не используются. Это печально, поскольку генераторные выражения обычно более надежны и обеспечивают большую обобщенность, чем старые методы. Есть разные ситуации, когда руководство из лучших побуждений, ограничивает кол-во поддерживаемых генераторов или платформ для проекта, но использование подходящих генераторных выражений не привело бы к таким ограничениям. Это особенно верно в отношении реализации логики, которая пытается делать разные вещи для разных типов сборки. Поэтому разработчикам следует ознакомиться с возможностями, которые предоставляют генераторные выражения. Упомянутые выше выражения - это лишь подмножество того, что поддерживает CMake, но они составляют хорошую основу для покрытия большинства ситуаций, с которыми, скорее всего, столкнется большинство разработчиков.
При разумном использовании генераторные выражения могут привести к созданию более лаконичных файлов CMakeLists.txt. Например, условное включение исходного файла в зависимости от типа сборки может быть выполнено относительно лаконично, как показал пример, приведенный ранее для $<CONFIG:...>. Такое использование позволяет сократить количество вветвлений «if-then-else», что приводит к улучшению читабельности, если генераторные выражения не слишком сложны. Генераторные выражения также отлично подходят для работы с содержимым, которое меняется в зависимости от цели или типа сборки. Ни один другой механизм в CMake не предлагает такой гибкости и универсальности для работы с множеством факторов, которые необходимы для вычисления значения конкретного свойства цели.
И наоборот, легко переборщить и попытаться сделать все генераторным выражением. Это может привести к появлению слишком сложных выражений, которые в конечном итоге затуманивают логику и могут быть сложны для отладки (в разделе 14.5, «Отладка генераторных выражений», приведены некоторые приемы, помогающие справиться с этой проблемой). Как всегда, разработчики должны отдавать предпочтение ясности, а не высокоумию, и это особенно верно в отношении генераторных выражений. Сначала подумайте, нет ли в CMake уже специального средства для достижения того же результата. Различные модули CMake предоставляют более узкую функциональность, направленную на конкретный пакет стороннего разработчика или на выполнение определенных специфических задач. Существует также множество переменных и свойств, которые могут упростить или вовсе заменить необходимость в генераторных выражениях. Несколько минут на изучение справочной документации по CMake могут избавить вас от многих часов ненужной работы по созданию сложных генераторных выражений, которые на самом деле были не нужны.
Если в проекте в качестве минимальной версии указана CMake 3.27 или более поздняя, предпочитайте использовать более общие выражения $<LIST:...>, а не старые и более специфические $<JOIN:...>, $<REMOVE_DUPLICATES:...> или $<FILTER:...>. Эти выражения со временем могут стать устаревшими. Проекты будут долговечнее, если вместо них использовать выражения $<LIST:...>.
Это был ознакомительный фрагмент книги Professional CMake: A Practical Guide by Craig Scott