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

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

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

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

5.1. Тестирование

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

Следующий минимальный пример показывает, как определить проект с парой простых тестов:

-2

Вызов enable_testing() необходим для того, чтобы указать CMake на создание входного файла для ctest. Обычно его следует вызывать сразу после project().

Команда add_test() - это способ определения теста в проекте. Она поддерживает несколько различных форм, но рекомендуется та, что показана выше, с ключевыми словами NAME и COMMAND.

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

COMMAND может быть любой произвольной командой, которую можно запустить из оболочки или командной строки. В качестве особого случая это также может быть имя исполняемой цели, определенной проектом. Затем CMake преобразует имя цели в местоположение бинарного файла, созданного для этой цели. В приведенном выше примере тест SomethingWorks запустит исполняемый файл, собранный для CMake-цели testSomething. Проекту не нужно заботиться о том, где сборка создаст двоичный файл в файловой системе, CMake предоставит эту информацию ctest автоматически.

По умолчанию тест считается пройденным, если он возвращает код выхода 0. Можно определить гораздо более подробные и гибкие критерии, которые рассматриваются в разделе 27.3, «Критерии прохождения / отказа и другие типы результатов», но часто достаточно простой проверки кода выхода.

Следующая последовательность шагов позволит сконфигурировать, собрать и протестировать проект. Можно использовать любой генератор CMake, но в данном примере используется Ninja. Он настраивает сборку на использование конфигурации Debug, и ctest обнаружит это автоматически.

-3

Некоторые генераторы являются многоконфигурационными, например Xcode, Visual Studio и Ninja Multi-Config. При использовании таких генераторов конфигурация для сборки и тестирования должна быть предоставлена во время сборки и тестирования:

-4

Для удобства вместо более длинного --build-config в команде ctest можно использовать -C.

Если тестов много и их выполнение занимает нетривиальное время, их можно выполнять параллельно:

-5

Вместо опции --parallel можно также использовать более короткую опцию -j. Число после ключевого слова определяет, сколько тестов может выполняться одновременно. В более продвинутых сценариях тесту может быть выделено более одного процессора, что обсуждается в разделе 28.3, «Параллельное выполнение».

Вывод ctest по умолчанию довольно лаконичен. Вывод результатов пройденных и неудачных тестов будет скрыт, будут показаны только результаты. Полный вывод можно получить с помощью опции -V или --verbose, или только вывод неудачных тестов с помощью --output-on-failure.

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

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

5.2. Установка

Хотя вещи, собранные в рамках проекта, часто можно использовать непосредственно из каталога сборки, это зачастую лишь ступень на пути к развертыванию проекта. Развертывание может принимать различные формы, но общим элементом большинства из них является этап установки. Во время установки файлы копируются из каталога сборки (и, возможно, исходного каталога) в место установки. Файлы могут быть преобразованы каким-либо образом до или после копирования.

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

-6

В приведенном выше примере используются возможности, добавленные в CMake 3.14. Если не указано, куда устанавливать цели, CMake будет использовать расположение по умолчанию, соответствующее схеме, используемой в большинстве Unix-систем. Эта же схема обычно хорошо работает и в Windows, поэтому ее можно использовать везде, за исключением пакетов приложений на платформах Apple, которые имеют свою собственную уникальную структуру каталогов (подробно рассматривается в разделе 25.2.1, «Структура пакетов» и разделе 25.3.1, «Структура фреймворка»). По умолчанию CMake устанавливает исполняемые файлы в подкаталог bin ниже места базовой установки, библиотеки - в подкаталог lib, а заголовки - в подкаталог include. В Windows библиотеки DLL будут устанавливаться в bin, а не в lib.

Если используется CMake 3.13 или более ранняя версия, то никаких пунктов назначения по умолчанию не предусмотрено, и проект должен указать их явно. Команда install(), эквивалентная предыдущему примеру, будет выглядеть следующим образом:

-7

В разделе 35.2, «Установка целей проекта», подробно рассматривается, что означают RUNTIME, LIBRARY и ARCHIVE, а также ряд других типов устанавливаемых объектов, связанных с целью. В нем также обсуждается работа с символическими ссылками, созданными для разделяемых библиотек, когда они имеют сведения о версии, что является отдельной темой, рассматриваемой в разделе 23.3, «Версионирование разделяемых библиотек».

Файлы и каталоги также могут быть установлены. Ниже показано, как традиционно устанавливались заголовочные файлы до последних версий CMake:

-8

Форма install(FILES) требует, чтобы каждый файл был перечислен отдельно. Это удобно, когда нужно установить только некоторые файлы в каталоге. Форма install(DIRECTORY) рекурсивно копирует указанный каталог в место назначения. Чтобы скопировать содержимое директории, а не саму директорию, добавьте к имени директории завершающий символ /. Например, если в каталоге headers находится только подкаталог myproj, то следующая команда будет эквивалентна приведенной выше:

-9

В CMake 3.23 и более поздних версиях более мощным и удобным способом работы с заголовочными файлами является использование наборов файлов. Набор файлов может ассоциировать заголовки с целью, и заголовки могут быть установлены вместе с целью в вызове install(TARGETS). Отдельного вызова install(FILES) или install(DIRECTORY) не требуется. Кроме того, набор файлов предоставляет информацию о том, является ли заголовок частным или публичным, а также о путях поиска заголовков, которые должен использовать потребитель цели.

Наборы файлов определяются командой target_sources(). В разделе 35.5.1, «Наборы файлов», эта тема рассматривается более подробно, а пока в следующем примере показано, как можно определить и установить набор заголовочных файлов:

-10

Как упоминалось ранее, CMake 3.14 и более поздние версии будут использовать include в качестве места назначения по умолчанию для заголовочных файлов, если место назначения не указано. А когда наборы файлов устанавливаются как часть цели, относительная структура под BASE_DIRS набора файлов будет сохранена. Таким образом, структура каталогов установленных заголовков в результате приведенного выше примера будет следующей:

-11

При установке библиотек и заголовочных файлов которые будут использоваться в других проектах CMake, рекомендуется также предоставлять специфический конфигурационный cmake-файл пакета. Этот файл дает потребляющему проекту цель CMake, с которой он может скомпоноваться, и эта цель будет включать пути поиска заголовков, которые будут применяться к потребителю. Подкоманда install(EXPORT) обычно используется для создания некоторых из этих конфигурационных cmake-файлов пакетов. Эта более сложная тема обсуждается в разделе 35.3, «Команда install(EXPORT)» и разделе 35.8, «Конфигурирование команды find_package()». Потребитель импортирует пакет с помощью команды find_package(), которая подробно рассматривается в разделе 34.5, «Поиск пакетов».

Следующая последовательность команд показывает, как сконфигурировать, собрать и установить проект:

-12

Эти шаги очень похожи на те, что были приведены ранее для примера в разделе 5.1, «Тестирование», за исключением того, что последняя команда отличается. Форма cmake --install <buildDir> доступна в CMake 3.15 или более поздней версии. Опция --prefix <installDir> указывает базовое место установки, в которую будет устанавливаться проект. Если --prefix не указан, то место установки берется из значения, зависящего от платформы и заданного на этапе configure. Подробнее об этом см. обсуждение CMAKE_INSTALL_PREFIX в разделе 35.1.2, «Базовый каталог установки».

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

-13

CMake также предоставляет цель сборки install, которая может быть использована для установки проекта с параметрами по умолчанию. Эта команда не столь гибкая, как cmake --install, но она поддерживается во всех выпусках CMake, а не только в CMake 3.15 и более поздних. Команда cmake --install принимает несколько других опций, не показанных выше (см. раздел 35.9, «Запуск процесса установки»), в то время как цель сборки install имеет ограниченные возможности настройки.

Когда проект выходит за рамки одного приложения или библиотеки, установка всего в одном пакете может быть уже нецелесообразной. Проект может захотеть разделить установку на отдельные компоненты. CMake имеет широкую поддержку для этого, но это не простая тема. Разделение проекта на несколько компонентов влияет не только на то, что будет установлено, но и поднимает такие вопросы, как «Какие зависимости существуют между компонентами?» и «Как компоненты должны быть сопоставлены с отдельными пакетами или устанавливаемыми единицами внутри упакованного продукта?». Компоненты обсуждаются во всей главе 35 «Установка», а раздел 36.2 «Компоненты» посвящен именно аспектам упаковки.

5.3. Упаковка

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

CMake предоставляет инструмент cpack, который может создавать бинарные пакеты в различных форматах. Это и простые архивы, такие как .zip, .tar.gz и .7z, и пакеты для таких специфических для конкретной платформы систем упаковки, как RPM, DEB и MSI, и даже автономные графические инсталляторы. Все они основаны на установке проекта с использованием информации, предоставляемой командами install() и другими. Внутри cpack эффективно выполняет одну или несколько команд cmake --install с параметром --prefix, установленным на временную область хранения. Содержимое этой области хранения затем используется для создания пакета в соответствующем формате.

Базовая упаковка реализуется путем установки некоторых соответствующих переменных CMake, а затем включения модуля CMake под названием CPack, который создает входной файл для инструмента cpack. Модули CMake рассмотрены в главе 12, переменные CMake - в главе 6, а в главе 36, подробно рассматривается упаковка. Пока же в следующем минимальном примере показано, как довольно просто собрать все эти вещи вместе:

-14

Различные строки set(...) в приведенном выше примере демонстрируют установку переменных CMake. Значения нужно заключать в кавычки только в том случае, если они содержат пробелы. В реальном проекте будет задано больше переменных, чем эти, но вышеприведенного достаточно, чтобы начать создавать рабочий пакет. Раздел 36.1, «Основы упаковки», содержит более полное руководство по минимальному набору переменных, которые должен установить проект для создания пакетов производственного уровня.

Обратите внимание на ключевое слово VERSION в вызове project(). Это удобный способ указать значение по умолчанию для версии пакета, если используется CMake 3.12 или более поздняя версия. Как отмечалось в разделе 3.2, «Команда project()», оно может использоваться и другими способами, например, для вставки номера версии в исходные файлы, скомпилированные для проекта (рассматривается в главе 22, «Указание сведений о версии»).

Следующий набор шагов демонстрирует, как настроить, собрать и упаковать проект:

-15

И снова только последняя строка существенно отличается от предыдущих примеров (выше также настройка на Release, а не на Debug, поскольку это более типично для предварительно собранных пакетов). Опция cpack -G задает форматы пакетов для генерации. Если указано более одного формата, они должны быть разделены точкой с запятой. Список поддерживаемых форматов зависит от платформы и может быть получен при выполнении команды cpack --help.

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

-16

Продолжая знакомую схему, CMake предоставляет цель для сборки пакета, которая запускает cpack с опциями, указанными в проекте. Опять же, возможности настройки очень ограничены, если собирать пакет через цель, а не запускать cpack напрямую. При использовании цели сборки пакета будет использоваться набор генераторов по умолчанию, но этот набор вряд ли будет подходящим. Проект может переопределить набор по умолчанию, установив переменную CPACK_GENERATORS перед вызовом include(CPack). Список генераторов, как правило, будет отличаться для каждой платформы, поэтому, скорее всего, потребуется некоторая условная логика (см. главу 7, Управление потоком). Следующий пример, взятый из раздела 36.1, «Основы упаковки», является хорошей отправной точкой.

-17

Опция cpack -G переопределяет любой список, заданный проектом с помощью переменной CPACK_GENERATOR, поэтому командная строка по-прежнему имеет более полный контроль.

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

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

При разработке проекта, изменяйте команду install(), выполняя тестовые установки во временную область хранения. Используйте такие команды, как cmake --install с опцией --prefix, указывающей место установки, чтобы убедиться, что содержимое установленного файла соответствует ожидаемому набору файлов. Временную область установки следует сначала очистить, чтобы убедиться, что в ней не осталось содержимого предыдущей тестовой установки.

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

Если проект может установить минимальную версию CMake 3.23 или выше, потратьте немного времени на изучение наборов файлов CMake (см. раздел 35.5.1, «Наборы файлов»). Поместите все заголовочные файлы проекта в наборы файлов и используйте отдельные наборы файлов PRIVATE и PUBLIC, чтобы четко определить, какие из них должны быть установлены, а какие нет. Это также упростит работу с путями поиска заголовочных файлов, как при сборке проекта, так и при его установке.

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