Предыдущие главы были посвящены основным аспектам CMake. Переменные, свойства, условные конструкции, генераторные выражения, функции и т. д. - все это часть того, что можно считать языком CMake. В отличие от этого, модули - это готовые фрагменты кода CMake, построенные поверх основных возможностей языка. Они предоставляют богатый набор функциональных возможностей, которые проекты могут использовать для достижения самых разных целей. Будучи написанными и упакованными как обычный код CMake и, следовательно, читаемыми человеком, модули также могут быть полезным ресурсом для получения дополнительной информации о том, как сделать что-то в CMake.
Модули собираются вместе и предоставляются в одном каталоге как часть релиза CMake. Проекты используют модули одним из двух способов: либо напрямую, либо как сценарий поиска для внешнего пакета. Первый метод использует команду include(), чтобы по сути внедрить код модуля в текущую область видимости. Это работает так же, как было описано в разделе 8.2, «include()», за исключением того, что команде include() нужно передать только базовое имя модуля, а не полный путь и расширение файла. Все опции include() работают точно так же, как было описано раньше.
При задании имени модуля команда include() будет искать в четко определенном наборе мест файл, имя которого равно имени модуля (с учетом регистра) с добавлением .cmake. Например, include(FooBar) приведет к тому, что CMake будет искать файл с именем FooBar.cmake, а в системах с чувствительностью к регистру, таких как Linux, имена файлов типа foobar.cmake не будут совпадать.
При поиске файла модуля CMake сначала обращается к переменной CMAKE_MODULE_PATH. Предполагается, что это список каталогов, и CMake будет искать в каждом из них по порядку. Первый подходящий файл будет использован, а если подходящий файл не найден или CMAKE_MODULE_PATH пуст или не определен, CMake будет искать в своем собственном внутреннем каталоге модулей. Такой порядок поиска позволяет проектам беспрепятственно добавлять свои собственные модули, добавляя каталоги в CMAKE_MODULE_PATH. Полезная схема - собрать все файлы модулей проекта в один каталог и добавить его в CMAKE_MODULE_PATH где-то в начале файла CMakeLists.txt верхнего уровня. Такая структура каталогов показана ниже:
Затем в соответствующем файле CMakeLists.txt нужно только добавить каталог cmake в CMAKE_MODULE_PATH, после чего можно вызывать include(), используя только имя базового файла при загрузке каждого из модулей.
CMakeLists.txt:
Существует одно исключение из порядка поиска, используемого CMake для нахождения модуля. Если файл, вызывающий include(), находится в собственном внутреннем каталоге модулей CMake, то перед обращением к CMAKE_MODULE_PATH сначала будет выполнен поиск во внутреннем каталоге модулей. Это предотвращает случайную (или намеренную) замену официального модуля своим собственным и изменение задокументированного поведения кода.
Второй способ использования модулей - это команда find_package(). Подробно об этом говорится в разделе 34.5, «Поиск пакетов», но пока что упрощенная форма этой команды без дополнительных ключевых слов демонстрирует ее базовое использование:
При таком использовании поведение очень похоже на include(), за исключением того, что CMake будет искать файл с именем FindPackageName.cmake, а не PackageName.cmake. Это метод, с помощью которого в сборку часто попадают сведения о внешнем пакете, включая такие вещи, как импортируемые цели, переменные, определяющие расположение соответствующих файлов, библиотек или программ, информация о необязательных компонентах, сведения о версии и так далее. Набор опций и возможностей, связанных с find_package(), значительно богаче, чем у include(), и глава 34 посвящена подробному рассмотрению этой темы.
В оставшейся части этой главы мы познакомимся с рядом интересных модулей, которые входят в состав релиза CMake. Это далеко не полный набор, но они дают представление о том, какая функциональность доступна. Другие модули будут представлены в последующих главах, где их функциональность будет тесно связана с обсуждаемой темой. В документации CMake приведен полный список всех доступных модулей, каждый из которых имеет свой раздел справки, объясняющий, что предоставляет модуль и как его можно использовать. Однако имейте в виду, что качество документации варьируется от модуля к модулю.
12.1. Проверка наличия и поддержки
Одной из наиболее обширных областей, охватываемых модулями CMake, является проверка наличия или поддержки различных вещей. Все модули этого семейства работают принципиально одинаково: принимают небольшой тестовый код, а затем пытаются скомпилировать и, возможно, скомпоновать и запустить полученный исполняемый файл, чтобы подтвердить, поддерживается ли то, что тестируется в коде. Все эти модули имеют имя, начинающееся с Check.
Одними из наиболее фундаментальных модулей Check... являются те, которые компилируют и компонуют короткий тестовый файл в исполняемый файл и возвращают результат успеха/неудачи. В CMake 3.19 или более поздней версии модуль CheckSourceCompiles предоставляет такую возможность. Он определяет команду check_source_compiles():
В качестве lang может выступать один из языков, поддерживаемых CMake, например C, CXX, CUDA и так далее. В более ранних версиях CMake те же возможности предоставляют отдельные модули для каждого языка, но набор поддерживаемых языков гораздо меньше. Эти модули имеют имена вида Check<LANG>SourceCompiles, и каждый из них предоставляет связанную с ним команду, выполняющую проверку:
Для всех этих команд аргумент code должен представлять собой строку, содержащую исходный код, который должен создать исполняемый файл для соответствующего языка. Результат попытки компиляции и компоновки кода хранится в resultVar как кэш-переменная, причем true означает успех. Ложным значением может быть пустая строка, сообщение об ошибке и т. д. в зависимости от ситуации. После того как тест был выполнен один раз, последующие запуски CMake будут использовать кэшированный результат, а не выполнять тест заново. Это происходит даже в том случае, если тестируемый код был изменен. Для принудительной повторной оценки необходимо вручную удалить переменную из кэша.
Если указан параметр FAIL_REGEX, то применяется дополнительный критерий: если вывод тестовой компиляции и линковки совпадает с любым из указанных regex (список регулярных выражений), проверка будет считаться неудачной, даже если код успешно компилируется и линкуется.
В случае Фортрана расширение файла может повлиять на то, как компиляторы обрабатывают исходные файлы, поэтому расширение файла может быть явно указано с помощью опции SRC_EXT, чтобы получить ожидаемое поведение. При использовании старых модулей Check<LANG>SourceCompiles для случаев C или C++ эквивалентной опции нет, но новый модуль CheckSourceCompiles поддерживает ее для всех языков.
Перед вызовом любой из команд проверки компиляции можно задать ряд переменных вида CMAKE_REQUIRED_..., чтобы повлиять на то, как они будут компилировать код:
- CMAKE_REQUIRED_FLAGS Дополнительные флаги для передачи в командную строку компилятора после содержимого соответствующих переменных CMAKE_<LANG>_FLAGS и CMAKE_<LANG>_FLAGS_<CONFIG> (см. раздел 16.6, «Переменные компилятора и компоновщика»). Это должна быть одна строка с несколькими флагами, разделенными пробелами, в отличие от всех остальных переменных ниже, которые являются списками CMake.
- CMAKE_REQUIRED_DEFINITIONS Список определений компилятора в CMake, каждое из которых задано в форме -DFOO или -DFOO=bar.
- CMAKE_REQUIRED_INCLUDES Указывает каталоги для поиска заголовков. Несколько путей должны быть указаны в виде списка CMake, при этом пробелы рассматриваются как часть пути.
- CMAKE_REQUIRED_LIBRARIES Список библиотек CMake для добавления на этапе компоновки. Не добавляйте к именам библиотек префикс -l или подобную опцию, указывайте только имя библиотеки или имя импортируемой CMake цели (обсуждается в главе 19, Типы целей).
- CMAKE_REQUIRED_LINK_OPTIONS Список опций CMake, которые должны быть переданы компоновщику при сборке исполняемого файла или архиватору при сборке статической библиотеки. Поддержка этой переменной доступна только в CMake 3.14 или более поздней версии.
- CMAKE_REQUIRED_QUIET Если эта опция присутствует, команда не будет печатать никаких сообщений о состоянии.
Эти переменные используются для построения аргументов вызова try_compile(), выполняемого внутри модуля для проведения проверки. В документации CMake по try_compile() обсуждаются дополнительные переменные, которые могут влиять на проверки, а другие аспекты поведения try_compile(), связанные с выбором цепочки инструментов и типом цели для сборки, рассматриваются в разделе 24.5, «Проверки компилятора».
Помимо проверки возможности сборки кода, CMake также предоставляет модули, которые проверяют, может ли собранный исполняемый файл быть успешно запущен. Успех определяется кодом возврата исполняемого файла, созданного из предоставленного исходного кода, при этом 0 считается успехом, а все остальные значения означают неудачу. В CMake 3.19 и более поздних версиях один модуль предоставляет команду для всех языков:
Опять же, в более ранних версиях CMake отдельные модули для каждого языка предоставляют те же возможности, но с меньшим количеством поддерживаемых языков:
Для этих команд нет опции FAIL_REGEX, так как успех или неудача определяются исключительно кодом возврата тестового исполняемого файла. Если экзешник не может быть собран, это также рассматривается как неудача. Все те же переменные, которые влияют на сборку кода для команд check_source_compiles() или check_<lang>_source_compiles(), также влияют и на команды этих модулей.
Для сборок, которые кросс-компилируются на другую целевую платформу, команды check_source_runs() и check_<lang>_source_runs() ведут себя совершенно по-другому. Они могут запустить код под симулятором, если были предоставлены необходимые данные, что, вероятно, значительно замедлит выполнение этапа CMake. Если же данные о симуляторе не были предоставлены, команды будут ожидать заранее определенного результата через набор переменных и не будут пытаться ничего запускать. Эта довольно сложная тема рассматривается в документации CMake по команде try_run(), которую модуль использует для внутренних проверок.
Некоторые категории проверок настолько распространены, что CMake предоставляет для них специальные модули. Они покрывают большую часть ситуаций, связанных с написанием кода проверки, и позволяют проектам указывать минимальное количество информации. Как правило, это просто обертки вокруг команд, предоставляемых одним из вышеупомянутых модулей, поэтому используется тот же набор переменных. Эти более специализированные модули проверяют флаги компилятора, символы препроцессора, функции, переменные, заголовочные файлы и многое другое. Как и в случае с вышеупомянутыми модулями, CMake 3.19 и более поздние версии предоставляют единый модуль и команду для всех поддерживаемых языков. Для более ранних версий CMake необходимо использовать набор модулей для каждого языка.
Команды проверки флагов обновляют внутреннюю переменную CMAKE_REQUIRED_DEFINITIONS, чтобы включить флаг в вызов check_source_compiles() с тривиальным тестовым файлом. В качестве опции FAIL_REGEX также передается внутренний набор регулярных выражений отказа, проверяющий, приводит ли флаг к выдаче диагностического сообщения или нет. Результатом вызова будет значение true, если диагностическое сообщение не было выдано. Обратите внимание, что это означает, что любой флаг, который приводит к предупреждению компилятора, но успешно компилируется, все равно будет считаться не прошедшим проверку. Также следует помнить, что эти команды предполагают, что любые флаги, уже присутствующие в соответствующих переменных CMAKE_<LANG>_FLAGS (см. раздел 16.6, «Переменные компилятора и компоновщика»), сами по себе не генерируют предупреждений компилятора. Если это так, то логика каждой из этих команд проверки флагов будет нарушена, и результатом всех таких проверок будет неудача.
В CMake 3.18 также появился модуль CheckLinkerFlag. Он предоставляет команду check_linker_flag(), которая в основном является просто удобной оберткой для check_source_compiles(). Как таковая, она поддерживает все те же переменные, что и рассмотренные ранее, за исключением того, что она берет на себя обработку переменной CMAKE_REQUIRED_LINK_OPTIONS.
Указанный флаг не передается непосредственно компоновщику. Компоновщик вызывается через компилятор, который внутренне добавляет дополнительные флаги, библиотеки и т. д., необходимые для успешной компоновки для указанного языка. Прямой флаг для компоновщика обычно не работает, так как ему обычно требуется какой-то префикс, например -Wl,... или -Xlinker, чтобы указать компилятору передать его компоновщику. Этот префикс зависит от конкретного компилятора, но можно использовать специальный префикс LINKER:, и CMake автоматически подставит правильный префикс, специфичный для компилятора. См. раздел 16.1.2, «Флаги компоновщика», и команду target_link_options() в разделе 16.2, «Команды свойств цели» для обсуждения соответствующих вопросов.
Еще два примечательных модуля CheckSymbolExists и CheckCXXSymbolExists. Первый предоставляет команду, которая создает тестовый исполняемый файл на языке C, а второй делает то же самое с исполняемым файлом на C++. Оба модуля проверяют, существует ли определенный символ как символ препроцессора (т. е. то, что можно проверить с помощью оператора #ifdef), функция или переменная.
Для каждого из элементов, указанных в заголовках (список CMake, если необходимо указать более одного заголовка), в исходный код теста будет добавлен соответствующий #include. В большинстве случаев проверяемый символ будет определен одним из этих заголовков. Результат проверки сохраняется в кэш-переменной resultVar обычным способом.
В случае с функциями и переменными символ должен разрешаться в то, что является частью исполняемого файла теста. Если функция или переменная предоставляется библиотекой, эта библиотека должна быть скомпонована как часть теста, что можно сделать с помощью переменной CMAKE_REQUIRED_LIBRARIES.
Существуют ограничения на тип функций и переменных, которые могут быть проверены этими командами. Можно использовать только те символы, которые удовлетворяют требованиям к именованию символа препроцессора. Последствия этого для check_cxx_symbol_exists() более негативны, поскольку это означает, что проверять можно только нешаблонные функции или переменные в глобальном пространстве имен, потому что любые двоеточия (::) или шаблонные маркеры (<>) будут недопустимы для символа препроцессора. Также невозможно отличить различные перегрузки одной и той же функции, поэтому их также нельзя проверить.
Существуют и другие модули, целью которых является предоставление функциональности, схожей с CheckSymbolExists или являющейся ее подмножеством. Эти модули либо взяты из более ранних версий CMake, либо предназначены для языков, отличных от C или C++. Модуль CheckFunctionExists уже документирован как устаревший, а модуль CheckVariableExists не предлагает ничего такого, чего бы уже не предоставлял CheckSymbolExists. Модуль CheckFortranFunctionExists может быть полезен для проектов, работающих с Fortran, но обратите внимание, что модуля CheckFortranVariableExists не существует. Проекты, работающие с Фортраном, могут захотеть использовать CheckFortranSourceCompiles для обеспечения согласованности.
Более детальные проверки предоставляются другими модулями. Например, члены struct могут быть проверены с помощью CheckStructHasMember, специфические прототипы функций C или C++ могут быть проверены с помощью CheckPrototypeDefinition, а размер непользовательских типов может быть проверен с помощью CheckTypeSize. Возможны и другие проверки более высокого уровня, обеспечиваемые модулями CheckLanguage, CheckLibraryExists и различными модулями CheckIncludeFile.... Дополнительные модули проверки продолжают добавляться в CMake по мере его развития, поэтому обратитесь к документации по модулям CMake, чтобы узнать полный набор доступных функций.
В ситуациях, когда выполняется несколько проверок или когда эффекты выполнения проверок должны быть изолированы друг от друга или от остальной части текущей области видимости, сохранение и восстановление состояния до и после проверок вручную может быть обременительным. В частности, часто требуется сохранять и восстанавливать различные переменные CMAKE_REQUIRED_.... Чтобы помочь в этом, CMake предоставляет модуль CMakePushCheckState, который определяет следующие три команды:
Эти команды позволяют рассматривать различные переменные CMAKE_REQUIRED_... как набор и выталкивать их состояние в/из виртуального стека. Каждый раз, когда вызывается cmake_push_check_state(), она фактически начинает новую область виртуальных переменных только для переменных CMAKE_REQUIRED_... (а также переменной CMAKE_EXTRA_INCLUDE_FILES, которая используется только модулем CheckTypeSize). cmake_pop_check_state() действует наоборот, она сбрасывает текущие значения переменных CMAKE_REQUIRED_... и восстанавливает их значения на предыдущем уровне стека. Команда cmake_reset_check_state() удобна для очистки всех переменных CMAKE_REQUIRED_..., а опция RESET в cmake_push_check_state() также удобна для очистки переменных перед завершением функции. Однако обратите внимание, что до версии CMake 3.10 существовала ошибка, в результате которой опция RESET игнорировалась, поэтому для проектов, которым необходимо работать с версиями до 3.10, лучше использовать отдельный вызов cmake_reset_check_state().
12.2. Другие модули
CMake имеет отличную встроенную поддержку некоторых языков, особенно C и C++. Он также включает ряд модулей, которые обеспечивают поддержку языков в более расширяемом и настраиваемом виде. Эти модули позволяют сделать аспекты некоторых языков или связанных с ними пакетов доступными для проектов, определив соответствующие команды, переменные и свойства. Многие из этих модулей предоставляются как часть поддержки вызовов find_package() (см. раздел 34.5, «Поиск пакетов»), а другие предназначены для более прямого использования через include(), чтобы работать в текущей область видимости. Приведенный ниже список модулей должен дать представление о том, какая языковая поддержка доступна:
- CSharpUtilities
- FindJava, FindJNI, UseJava
- FindLua
- FindMatlab
- FindPerl, FindPerlLibs
- FindPython
- FindPHP4
- FindRuby
- FindSWIG, UseSWIG
- FindTCL
- FortranCInterface
Кроме того, предусмотрены модули для взаимодействия с внешними данными и проектами (см. главу 38, ExternalProject и главу 39, FetchContent). Также предусмотрен ряд модулей для облегчения различных аспектов тестирования и упаковки. Они тесно связаны с инструментами CTest и CPack, распространяемыми в составе пакета CMake, и подробно рассматриваются в главе 27 «Тестирование» и главе 36 «Упаковка». Помощь в отладке также оказывает модуль CMakePrintHelpers (см. раздел 14.3, «Помощники печати»).
12.3. Рекомендуемые практики
Коллекция модулей CMake предоставляет широкие функциональные возможности, построенные поверх основного языка CMake. Проект может легко расширить набор доступных функций, добавив свои собственные модули в определенную директорию, а затем добавив путь к ним в переменную CMAKE_MODULE_PATH. Использование CMAKE_MODULE_PATH должно быть предпочтительнее, чем жесткое кодирование абсолютных или относительных путей в сложных структурах каталогов в вызовах include(), поскольку это способствует отделению общей логики CMake от мест, где эта логика может быть применена. Это, в свою очередь, облегчает перемещение модулей CMake в разные каталоги по мере развития проекта или повторное использование логики в разных проектах. Действительно, нередко организация создает свою собственную коллекцию модулей, возможно, даже храня их в отдельном репозитории. Установив CMAKE_MODULE_PATH соответствующим образом в каждом проекте, можно сделать эти многократно используемые строительные блоки CMake доступными для широкого использования по мере необходимости.
Со временем разработчик, как правило, сталкивается со все большим количеством интересных сценариев, для которых модуль CMake может предоставить полезные сокращения или готовые решения. Иногда беглый просмотр доступных модулей может привести к неожиданному открытию скрытой жемчужины, или новый модуль может предложить лучшую реализацию того, что проект до этого момента реализовывал не лучшим образом. Преимуществом модулей CMake является потенциально большой пул разработчиков и проектов, использующих их на различных платформах и в различных ситуациях, поэтому во многих случаях они могут быть более убедительной альтернативой проектам, реализующим свою собственную логику вручную. Качество модулей, однако, варьируется от одного к другому. Некоторые модули начали свою жизнь довольно рано в CMake, и иногда они могут стать менее полезными, если не следить за изменениями в CMake или в областях, к которым эти модули относятся. Особенно это касается модулей Find..., которые могут не так тщательно отслеживать новые версии пакетов, которые они находят, как хотелось бы. С другой стороны, модули - это обычный код CMake, поэтому любой может изучать их, учиться на их примере, улучшать или обновлять их, не изучая ничего сверх того, что необходимо для базового использования CMake в проекте. Фактически, они являются отличной отправной точкой для разработчиков, желающих приступить к работе над самим CMake.
Обилие различных модулей Check..., предоставляемых CMake, может быть неоднозначным преимуществом. У разработчиков может возникнуть соблазн слишком усердствовать с проверкой всевозможных вещей, что может привести к замедлению этапа конфигурирования ради иногда сомнительной выгоды. Часто бывает, что команды, связанные с этими проверками, доминируют в результатах профилирования при выполнении CMake (см. раздел 14.6, «Профилирование вызовов CMake»). Подумайте, доминируют ли преимущества над затратами времени на внедрение и поддержку проверок, а также сложность проекта. Иногда достаточно нескольких разумных проверок, чтобы охватить наиболее полезные случаи или выявить тонкую проблему, которая в противном случае может привести к трудноотслеживаемым проблемам в дальнейшем. Кроме того, при использовании любого из модулей Check... старайтесь изолировать логику проверки от области, в которой она может быть вызвана. Настоятельно рекомендуется использовать модуль CMakePushCheckState, но избегайте использования опции RESET в cmake_push_check_state(), если важна поддержка версий CMake до 3.10.
Если минимальная версия CMake может быть установлена на 3.20 или более позднюю, избегайте использования относительно популярного, но уже устаревшего модуля TestBigEndian. Этот модуль был упразднен в CMake 3.20 в пользу новой переменной CMAKE_<LANG>_BYTE_ORDER, которая была введена в том же выпуске CMake. Проекты, использующие TestBigEndian, должны по возможности перейти на использование новой переменной.
Это был ознакомительный фрагмент книги Professional CMake: A Practical Guide by Craig Scott