Хабрахабр

Полное руководство по CMake. Часть вторая: Система сборки

Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи. В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++.

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

Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает. Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make).

Система сборки CMake принимает на вход файл CMakeLists.txt с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.

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

Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла: Команда cmake_minimum_required проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой.

# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)

В начале любого CMakeLists.txt следует задать характеристики проекта командой project для лучшего оформления интегрированными средами и прочими инструментами разработки.

# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)

Вы также можете отключить указание любых языков путём написания ключевого слова NONE в качестве списка языков или просто оставить пустой список. Стоит отметить, что если ключевое слово LANGUAGES опущено, то по умолчанию задаются языки C CXX.

Этот пример запускает скриптовый файл MyCMakeScript.cmake описанной командой: Команда include заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include языков C/C++.

message("'TEST_VARIABLE' is equal to [$]") # Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake) message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")

Таким образом, скриптовый файл, включаемый командой include, не создаёт собственной области видимости, о чём метко упомянули в комментариях к предыдущей статье. В данном примере, первое сообщение уведомит том, что переменная TEST_VARIABLE ещё не определена, однако если скрипт MyCMakeScript.cmake определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной.

Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe или просто <ExecutableName>). Команда add_executable компилирует исполняемый файл с заданным именем из списка исходников. Типичный пример вызова данной команды:

# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)

Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a или <LibraryName>.lib). Команда add_library компилирует библиотеку с указанным видом и именем из исходников. Типичный пример вызова данной команды:

# Скомпилировать статическую библиотеку "MyLibrary" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageConsumer.c":
add_library(MyLibrary STATIC ObjectHandler.c TimeManager.c MessageConsumer.c)

  • Статические библиотеки задаются ключевым словом STATIC вторым аргументом и представляют из себя архивы объектных файлов, связываемых с исполняемыми файлами и другими библиотеками во время компиляции.
  • Динамические библиотеки задаются ключевым словом SHARED вторым аргументом и представляют из себя двоичные библиотеки, загружаемые операционной системой во время выполнения программы.
  • Модульные библиотеки задаются ключевым словом MODULE вторым аргументом и представляют из себя двоичные библиотеки, загружаемые посредством техник выполнения самим исполняемым файлом.
  • Объектные библиотеки задаются ключевым словом OBJECT вторым аргументом и представляют из себя набор объектных файлов, связываемых с исполняемыми файлами и другими библиотеками во время компиляции.

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

Первым аргументом команда target_sources принимает название цели, ранее указанной с помощью команд add_library или add_executable, а последующие аргументы являются списком добавляемых исходных файлов.

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

# Задать исполняемый файл "MyExecutable" из исходников
# "ObjectPrinter.c" и "SystemEvaluator.c":
add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c) # Добавить к цели "MyExecutable" исходник "MessageConsumer.c":
target_sources(MyExecutable MessageConsumer.c)
# Добавить к цели "MyExecutable" исходник "ResultHandler.c":
target_sources(MyExecutable ResultHandler.c)

# Задать исполняемый файл "MyExecutable" из исходников
# "ObjectPrinter.c", "SystemEvaluator.c", "MessageConsumer.c" и "ResultHandler.c":
add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c MessageConsumer.c
ResultHandler.c)

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

Для "не-DLL" платформ динамические библиотеки рассматриваются библиотечными целями, а для "DLL-платформ" — целями выполнения. Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles.

Важно подметить, что "DLL-платформами" считаются все платформы, основанные на Windows, в том числе и Cygwin.

Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable или add_library, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Команда target_link_libraries компанует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Пример:

# Скомпановать исполняемый файл "MyExecutable" с
# библиотеками "JsonParser", "SocketFactory" и "BrowserInvoker":
target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker)

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

Пример ниже демонстрирует применение описанного механизма: Команда add_subdirectory побуждает CMake к незамедлительной обработке указанного файла подпроекта.

# Добавить каталог "subLibrary" в сборку основного проекта,
# а генерируемые файлы расположить в каталоге "subLibrary/build":
add_subdirectory(subLibrary subLibrary/build)

В данном примере первым аргументом команды add_subdirectory выступает подпроект subLibrary, а второй аргумент необязателен и информирует CMake о папке, предназначенной для генерируемых файлов включаемого подпроекта (например, CMakeCache.txt и cmake_install.cmake).

Данную особенность упомянули в комментариях к предыдущей статье. Стоит отметить, что все переменные из родительской области видимости унаследуются добавленным каталогом, а все переменные, определённые и переопределённые в данном каталоге, будут видимы лишь ему (если ключевое слово PARENT_SCOPE не было определено аргументом команды set).

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

# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED) # Уведомить компилятор о каталоге заголовков "GSL":
include_directories(${GSL_INCLUDE_DIRS}) # Скомпановать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)

Опция REQUIRED требует печати фатальной ошибки и завершении работы CMake если требуемый пакет не найден. В приведённом выше примере команда find_package первым аргументом принимает наименование пакета, а затем требуемую версию. Противоположность — это опция QUIET, требующая CMake продолжать свою работу, даже если пакет не был найден.

Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета). Далее вызывается команда include_directories, информирующая компилятора о расположении заголовочных файлов библиотеки GSL.

В конечном итоге, исполняемый файл MyExecutable линкуется с библиотекой GSL командой target_link_libraries с помощью переменной GSL::gsl, инкапсулирующей расположение самой GSL.

Это можно сделать путём проверки переменной <PackageName>_FOUND, автоматически определяемой после завершения команды find_package. Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET. Например, в случае успешного импортирования настроек GSL в Ваш проект, переменная GSL_FOUND обратится в истину.

Пример выше применял модульную форму. В общем случае, команда find_package имеет две разновидности запуска: модульную и конфигурационную. Это означает, что во время вызова команды CMake ищет скриптовый файл вида Find<PackageName>.cmake в директории CMAKE_MODULE_PATH, а затем запускает его и импортирует все необходимые настройки (в данном случае CMake запустила стандартный файл FindGSL.cmake).

Данная команда способна работать с целями, файлами, папками и многим другим. Команда install генерирует установочные правила для Вашего проекта. Сперва рассмотрим установку целей.

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

# Установить цели "TimePrinter" и "DataScanner" в директорию "bin":
install(TARGETS TimePrinter DataScanner DESTINATION bin)

Пример, демонстрирующий установку файлов: Процесс описания установки файлов аналогичен, за тем исключением, что вместо ключевого слова TARGETS следует указать FILES.

# Установить файлы "DataCache.txt" и "MessageLog.txt" в директорию "~/":
install(FILES DataCache.txt MessageLog.txt DESTINATION ~/)

Важно подметить, что при установке будет копироваться всё содержимое папки, а не только её название. Процесс описания установки папок также аналогичен, за тем исключением, что вместо ключевого слова FILES следует указать DIRECTORY. Пример установки папок выглядит следующим образом:

# Установить каталоги "MessageCollection" и "CoreFiles" в директорию "~/":
install(DIRECTORY MessageCollection CoreFiles DESTINATION ~/)

После завершения обработки CMake всех Ваших файлов Вы можете выполнить установку всех описанных объектов командой sudo make install (если CMake генерирует Makefile), или же выполнить данное действие интегрированной средой разработки, поддерживающей CMake.

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

+ MyProject - CMakeLists.txt - Defines.h - StartProgram.c + core - CMakeLists.txt - Core.h - ProcessInvoker.c - SystemManager.c

Главный файл сборки CMakeLists.txt описывает компиляцию всей программы: сперва происходит вызов команды add_executable, компилирующей исполняемый файл, затем вызывается команда add_subdirectory, побуждающая обработку подпроекта, и наконец, исполняемый файл линкуется с собранной библиотекой:

# Задать минимальную версию CMake:
cmake_minimum_required(VERSION 3.0) # Указать характеристики проекта:
project(MyProgram VERSION 1.0.0 LANGUAGES C) # Добавить в сборку исполняемый файл "MyProgram":
add_executable(MyProgram StartProgram.c) # Требовать обработку файла "core/CMakeFiles.txt":
add_subdirectory(core) # Скомпановать исполняемый файл "MyProgram" со
# скомпилированной статической библиотекой "MyProgramCore":
target_link_libraries(MyProgram MyProgramCore) # Установить исполняемый файл "MyProgram" в директорию "bin":
install(TARGETS MyProgram DESTINATION bin)

Файл core/CMakeLists.txt вызывается главным файлом сборки и компилирует статическую библиотеку MyProgramCore, предназначенную для линковки с исполняемым файлом:

# Задать минимальную версию CMake:
cmake_minimum_required(VERSION 3.0) # Указать характеристики проекта:
project(MyProgramCore VERSION 1.0.0 LANGUAGES C) # Добавить в сборку статическую библиотеку "MyProgramCore":
add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c)

&& make && sudo make install работа системы сборки CMake завершается успешно. После череды команд cmake . Первая команда побуждает обработку файла CMakeLists.txt в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпанованный исполняемый файл MyProgram в систему.

Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.

До скорых встреч!
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»