Полное руководство по CMake. Часть вторая: Система сборки +37
C++, Системы сборки, C
Рекомендация: подборка платных и бесплатных курсов таргетированной рекламе — https://katalog-kursov.ru/
Введение
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Ниже приведены примеры использования языка CMake, по которым Вам следует попрактиковаться. Экспериментируйте с исходным кодом, меняя существующие команды и добавляя новые. Чтобы запустить данные примеры, установите CMake с официального сайта.
Принцип работы
Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make). Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает.
Система сборки CMake принимает на вход файл CMakeLists.txt
с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.
Сгенерированные файлы будут содержать конкретные названия системных утилит, директорий и компиляторов, в то время как команды CMake орудуют лишь абстрактным понятием компилятора и не привязаны к платформенно зависимым инструментам, сильно различающихся на разных операционных системах.
Проверка версии CMake
Команда cmake_minimum_required
проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой. Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла:
# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
Как подметили в комментариях, команда cmake_minimum_required
выставляет все флаги совместимости (смотреть cmake_policy
). Некоторые разработчики намеренно выставляют низкую версию CMake, а затем корректируют функционал вручную. Это позволяет одновременно поддерживать древние версии CMake и местами использовать новые возможности.
Оформление проекта
В начале любого CMakeLists.txt
следует задать характеристики проекта командой project
для лучшего оформления интегрированными средами и прочими инструментами разработки.
# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
Стоит отметить, что если ключевое слово LANGUAGES
опущено, то по умолчанию задаются языки C CXX
. Вы также можете отключить указание любых языков путём написания ключевого слова NONE
в качестве списка языков или просто оставить пустой список.
Запуск скриптовых файлов
Команда include
заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include
языков C/C++. Этот пример запускает скриптовый файл MyCMakeScript.cmake
описанной командой:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
# Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake)
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
В данном примере, первое сообщение уведомит о том, что переменная TEST_VARIABLE
ещё не определена, однако если скрипт MyCMakeScript.cmake
определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной. Таким образом, скриптовый файл, включаемый командой include
, не создаёт собственной области видимости, о чём упомянули в комментариях к предыдущей статье.
Компиляция исполняемых файлов
Команда add_executable
компилирует исполняемый файл с заданным именем из списка исходников. Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe
или просто <ExecutableName>
). Типичный пример вызова данной команды:
# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
Компиляция библиотек
Команда add_library
компилирует библиотеку с указанным видом и именем из исходников. Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a
или <LibraryName>.lib
). Типичный пример вызова данной команды:
# Скомпилировать статическую библиотеку "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
, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:
- Переменные
RUNTIME_OUTPUT_DIRECTORY
иRUNTIME_OUTPUT_NAME
определяют местоположение целей выполнения. - Переменные
LIBRARY_OUTPUT_DIRECTORY
иLIBRARY_OUTPUT_NAME
определяют местоположение библиотек. - Переменные
ARCHIVE_OUTPUT_DIRECTORY
иARCHIVE_OUTPUT_NAME
определяют местоположение архивов.
Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для «не-DLL» платформ динамические библиотеки рассматриваются библиотечными целями, а для «DLL-платформ» — целями выполнения. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles
.
Важно подметить, что «DLL-платформами» считаются все платформы, основанные на Windows, в том числе и Cygwin.
Компоновка с библиотеками
Команда target_link_libraries
компонует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable
или add_library
, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Пример:
# Скомпоновать исполняемый файл "MyExecutable" с
# библиотеками "JsonParser", "SocketFactory" и "BrowserInvoker":
target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker)
Стоит отметить, что модульные библиотеки не подлежат компоновке с исполняемыми файлами или другими библиотеками, так как они предназначены исключительно для загрузки техниками выполнения.
Работа с целями
Как упомянули в комментариях, цели в CMake тоже подвержены ручному манипулированию, однако весьма ограниченному.
Имеется возможность управления свойствами целей, предназначенных для задания процесса сборки проекта. Команда get_target_property
присваивает предоставленной переменной значение свойства цели. Данный пример выводит значение свойства C_STANDARD
цели MyTarget
на экран:
# Присвоить переменной "VALUE" значение свойства "C_STANDARD":
get_target_property(VALUE MyTarget C_STANDARD)
# Вывести значение полученного свойства на экран:
message("'C_STANDARD' property is equal to [${VALUE}]")
Команда set_target_properties
устанавливает указанные свойства целей заданными значениями. Данная команда принимает список целей, для которых будут установлены значения свойств, а затем ключевое слово PROPERTIES
, после которого следует список вида «<название свойства> <новое значение>»:
# Установить свойству 'C_STANDARD' значение "11",
# а свойству 'C_STANDARD_REQUIRED' значение "ON":
set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
Пример выше задал цели MyTarget
свойства, влияющие на процесс компиляции, а именно: при компиляции цели MyTarget
CMake затребует компилятора о использовании стандарта C11. Все известные именования свойств целей перечисляются на этой странице.
Также имеется возможность проверки ранее определённых целей с помощью конструкции if(TARGET <TargetName>)
:
# Выведет "The target was defined!" если цель "MyTarget" уже определена,
# а иначе выведет "The target was not defined!":
if(TARGET MyTarget)
message("The target was defined!")
else()
message("The target was not defined!")
endif()
Добавление подпроектов
Команда add_subdirectory
побуждает CMake к незамедлительной обработке указанного файла подпроекта. Пример ниже демонстрирует применение описанного механизма:
# Добавить каталог "subLibrary" в сборку основного проекта,
# а генерируемые файлы расположить в каталоге "subLibrary/build":
add_subdirectory(subLibrary subLibrary/build)
В данном примере первым аргументом команды add_subdirectory
выступает подпроект subLibrary
, а второй аргумент необязателен и информирует CMake о папке, предназначенной для генерируемых файлов включаемого подпроекта (например, CMakeCache.txt
и cmake_install.cmake
).
Стоит отметить, что все переменные из родительской области видимости унаследуются добавленным каталогом, а все переменные, определённые и переопределённые в данном каталоге, будут видимы лишь ему (если ключевое слово PARENT_SCOPE
не было определено аргументом команды set
). Данную особенность упомянули в комментариях к предыдущей статье.
Поиск пакетов
Команда find_package
находит и загружает настройки внешнего проекта. В большинстве случаев она применяется для последующей линковки внешних библиотек, таких как Boost и GSL. Данный пример вызывает описанную команду для поиска библиотеки GSL и последующей линковки:
# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED)
# Скомпоновать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)
# Уведомить компилятор о каталоге заголовков "GSL":
target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS})
В приведённом выше примере команда find_package
первым аргументом принимает наименование пакета, а затем требуемую версию. Опция REQUIRED
требует печати фатальной ошибки и завершении работы CMake, если требуемый пакет не найден. Противоположность — это опция QUIET
, требующая CMake продолжать свою работу, даже если пакет не был найден.
Далее исполняемый файл MyExecutable
линкуется с библиотекой GSL командой target_link_libraries
с помощью переменной GSL::gsl
, инкапсулирующей расположение уже скомпилированной GSL.
В конце вызывается команда target_include_directories
, информирующая компилятора о расположении заголовочных файлов библиотеки GSL. Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS
, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета).
Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET
. Это можно сделать путём проверки переменной <PackageName>_FOUND
, автоматически определяемой после завершения команды find_package
. Например, в случае успешного импортирования настроек GSL в Ваш проект, переменная GSL_FOUND
обратится в истину.
В общем случае, команда find_package
имеет две разновидности запуска: модульную и конфигурационную. Пример выше применял модульную форму. Это означает, что во время вызова команды CMake ищет скриптовый файл вида Find<PackageName>.cmake
в директории CMAKE_MODULE_PATH
, а затем запускает его и импортирует все необходимые настройки (в данном случае CMake запустила стандартный файл FindGSL.cmake
).
Способы включения заголовков
Информировать компилятора о располжении включаемых заголовков можно посредством двух команд: include_directories
и target_include_directories
. Вы решаете, какую из них использовать, однако стоит учесть некоторые различия между ними (идея предложена в комментариях).
Команда include_directories
влияет на область каталога. Это означает, что все директории заголовков, указанные данной командой, будут применяться для всех целей текущего CMakeLists.txt
, а также для обрабатываемых подпроектов (смотреть add_subdirectory
).
Команда target_include_directories
влияет лишь на указанную первым аргументом цель, а на другие цели никакого воздействия не оказывается. Пример ниже демонстрирует разницу между этими двумя командами:
add_executable(RequestGenerator RequestGenerator.c)
add_executable(ResponseGenerator ResponseGenerator.c)
# Применяется лишь для цели "RequestGenerator":
target_include_directories(RequestGenerator headers/specific)
# Применяется для целей "RequestGenerator" и "ResponseGenerator":
include_directories(headers)
В комментариях упомянуто, что в современных проектах применение команд include_directories
и link_libraries
является нежелательным. Альтернатива — это команды target_include_directories
и target_link_libraries
, действующие лишь на конкретные цели, а не на всю текущую область видимости.
Установка проекта
Команда 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 checkinstall
(если 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)
# Добавить в сборку статическую библиотеку "MyProgramCore":
add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c)
После череды команд cmake . && make && sudo checkinstall
работа системы сборки CMake завершается успешно. Первая команда запускает обработку файла CMakeLists.txt
в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпонованный исполняемый файл MyProgram
в систему.
Заключение
Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю. До скорых встреч!
Введение
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Запуск CMake
Ниже приведены примеры использования языка CMake, по которым Вам следует попрактиковаться. Экспериментируйте с исходным кодом, меняя существующие команды и добавляя новые. Чтобы запустить данные примеры, установите CMake с официального сайта.
Принцип работы
Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make). Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает.
Система сборки CMake принимает на вход файл CMakeLists.txt
с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.
Сгенерированные файлы будут содержать конкретные названия системных утилит, директорий и компиляторов, в то время как команды CMake орудуют лишь абстрактным понятием компилятора и не привязаны к платформенно зависимым инструментам, сильно различающихся на разных операционных системах.
Проверка версии CMake
Команда cmake_minimum_required
проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой. Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла:
# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
Оформление проекта
В начале любого CMakeLists.txt
следует задать характеристики проекта командой project
для лучшего оформления интегрированными средами и прочими инструментами разработки.
# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
Стоит отметить, что если ключевое слово LANGUAGES
опущено, то по умолчанию задаются языки C CXX
. Вы также можете отключить указание любых языков путём написания ключевого слова NONE
в качестве списка языков или просто оставить пустой список.
Запуск скриптовых файлов
Команда include
заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include
языков C/C++. Этот пример запускает скриптовый файл MyCMakeScript.cmake
описанной командой:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
# Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake)
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
В данном примере, первое сообщение уведомит том, что переменная TEST_VARIABLE
ещё не определена, однако если скрипт MyCMakeScript.cmake
определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной. Таким образом, скриптовый файл, включаемый командой include
, не создаёт собственной области видимости, о чём метко упомянули в комментариях к предыдущей статье.
Компиляция исполняемых файлов
Команда add_executable
компилирует исполняемый файл с заданным именем из списка исходников. Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe
или просто <ExecutableName>
). Типичный пример вызова данной команды:
# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
Компиляция библиотек
Команда add_library
компилирует библиотеку с указанным видом и именем из исходников. Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a
или <LibraryName>.lib
). Типичный пример вызова данной команды:
# Скомпилировать статическую библиотеку "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
, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:
- Переменные
RUNTIME_OUTPUT_DIRECTORY
иRUNTIME_OUTPUT_NAME
определяют местоположение целей выполнения. - Переменные
LIBRARY_OUTPUT_DIRECTORY
иLIBRARY_OUTPUT_NAME
определяют местоположение библиотек. - Переменные
ARCHIVE_OUTPUT_DIRECTORY
иARCHIVE_OUTPUT_NAME
определяют местоположение архивов.
Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для «не-DLL» платформ динамические библиотеки рассматриваются библиотечными целями, а для «DLL-платформ» — целями выполнения. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles
.
Важно подметить, что «DLL-платформами» считаются все платформы, основанные на Windows, в том числе и Cygwin.
Компановка с библиотеками
Команда target_link_libraries
компанует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable
или add_library
, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Пример:
# Скомпановать исполняемый файл "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
). Данную особенность упомянули в комментариях к предыдущей статье.
Поиск пакетов
Команда find_package
находит и загружает настройки внешнего проекта. В большинстве случаев она применяется для последующей линковки внешних библиотек, таких как Boost и GSL. Данный пример вызывает описанную команду для поиска библиотеки GSL и последующей линковки:
# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED)
# Уведомить компилятор о каталоге заголовков "GSL":
include_directories(${GSL_INCLUDE_DIRS})
# Скомпановать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)
В приведённом выше примере команда find_package
первым аргументом принимает наименование пакета, а затем требуемую версию. Опция REQUIRED
требует печати фатальной ошибки и завершении работы CMake если требуемый пакет не найден. Противоположность — это опция QUIET
, требующая CMake продолжать свою работу, даже если пакет не был найден.
Далее вызывается команда include_directories
, информирующая компилятора о расположении заголовочных файлов библиотеки GSL. Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS
, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета).
В конечном итоге, исполняемый файл MyExecutable
линкуется с библиотекой GSL командой target_link_libraries
с помощью переменной GSL::gsl
, инкапсулирующей расположение самой GSL.
Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET
. Это можно сделать путём проверки переменной <PackageName>_FOUND
, автоматически определяемой после завершения команды find_package
. Например, в случае успешного импортирования настроек 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)
После череды команд cmake . && make && sudo make install
работа системы сборки CMake завершается успешно. Первая команда побуждает обработку файла CMakeLists.txt
в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпанованный исполняемый файл MyProgram
в систему.
Заключение
Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю. До скорых встреч!
Введение
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Запуск CMake
Ниже приведены примеры использования языка CMake, по которым Вам следует попрактиковаться. Экспериментируйте с исходным кодом, меняя существующие команды и добавляя новые. Чтобы запустить данные примеры, установите CMake с официального сайта.
Принцип работы
Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make). Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает.
Система сборки CMake принимает на вход файл CMakeLists.txt
с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.
Сгенерированные файлы будут содержать конкретные названия системных утилит, директорий и компиляторов, в то время как команды CMake орудуют лишь абстрактным понятием компилятора и не привязаны к платформенно зависимым инструментам, сильно различающихся на разных операционных системах.
Проверка версии CMake
Команда cmake_minimum_required
проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой. Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла:
# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
Оформление проекта
В начале любого CMakeLists.txt
следует задать характеристики проекта командой project
для лучшего оформления интегрированными средами и прочими инструментами разработки.
# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
Стоит отметить, что если ключевое слово LANGUAGES
опущено, то по умолчанию задаются языки C CXX
. Вы также можете отключить указание любых языков путём написания ключевого слова NONE
в качестве списка языков или просто оставить пустой список.
Запуск скриптовых файлов
Команда include
заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include
языков C/C++. Этот пример запускает скриптовый файл MyCMakeScript.cmake
описанной командой:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
# Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake)
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
В данном примере, первое сообщение уведомит том, что переменная TEST_VARIABLE
ещё не определена, однако если скрипт MyCMakeScript.cmake
определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной. Таким образом, скриптовый файл, включаемый командой include
, не создаёт собственной области видимости, о чём метко упомянули в комментариях к предыдущей статье.
Компиляция исполняемых файлов
Команда add_executable
компилирует исполняемый файл с заданным именем из списка исходников. Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe
или просто <ExecutableName>
). Типичный пример вызова данной команды:
# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
Компиляция библиотек
Команда add_library
компилирует библиотеку с указанным видом и именем из исходников. Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a
или <LibraryName>.lib
). Типичный пример вызова данной команды:
# Скомпилировать статическую библиотеку "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
, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:
- Переменные
RUNTIME_OUTPUT_DIRECTORY
иRUNTIME_OUTPUT_NAME
определяют местоположение целей выполнения. - Переменные
LIBRARY_OUTPUT_DIRECTORY
иLIBRARY_OUTPUT_NAME
определяют местоположение библиотек. - Переменные
ARCHIVE_OUTPUT_DIRECTORY
иARCHIVE_OUTPUT_NAME
определяют местоположение архивов.
Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для «не-DLL» платформ динамические библиотеки рассматриваются библиотечными целями, а для «DLL-платформ» — целями выполнения. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles
.
Важно подметить, что «DLL-платформами» считаются все платформы, основанные на Windows, в том числе и Cygwin.
Компановка с библиотеками
Команда target_link_libraries
компанует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable
или add_library
, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Пример:
# Скомпановать исполняемый файл "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
). Данную особенность упомянули в комментариях к предыдущей статье.
Поиск пакетов
Команда find_package
находит и загружает настройки внешнего проекта. В большинстве случаев она применяется для последующей линковки внешних библиотек, таких как Boost и GSL. Данный пример вызывает описанную команду для поиска библиотеки GSL и последующей линковки:
# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED)
# Уведомить компилятор о каталоге заголовков "GSL":
include_directories(${GSL_INCLUDE_DIRS})
# Скомпановать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)
В приведённом выше примере команда find_package
первым аргументом принимает наименование пакета, а затем требуемую версию. Опция REQUIRED
требует печати фатальной ошибки и завершении работы CMake если требуемый пакет не найден. Противоположность — это опция QUIET
, требующая CMake продолжать свою работу, даже если пакет не был найден.
Далее вызывается команда include_directories
, информирующая компилятора о расположении заголовочных файлов библиотеки GSL. Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS
, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета).
В конечном итоге, исполняемый файл MyExecutable
линкуется с библиотекой GSL командой target_link_libraries
с помощью переменной GSL::gsl
, инкапсулирующей расположение самой GSL.
Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET
. Это можно сделать путём проверки переменной <PackageName>_FOUND
, автоматически определяемой после завершения команды find_package
. Например, в случае успешного импортирования настроек 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)
После череды команд cmake . && make && sudo make install
работа системы сборки CMake завершается успешно. Первая команда побуждает обработку файла CMakeLists.txt
в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпанованный исполняемый файл MyProgram
в систему.
Заключение
Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю. До скорых встреч!
Автор: Gymmasssorla
Источник
Введение
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Читать дальше →
Полное руководство по CMake. Часть вторая: Система сборки
Source: habrahabr
Введение
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Запуск CMake
Ниже приведены примеры использования языка CMake, по которым Вам следует попрактиковаться. Экспериментируйте с исходным кодом, меняя существующие команды и добавляя новые. Чтобы запустить данные примеры, установите CMake с официального сайта.
Принцип работы
Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make). Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает.
Система сборки CMake принимает на вход файл CMakeLists.txt
с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.
Сгенерированные файлы будут содержать конкретные названия системных утилит, директорий и компиляторов, в то время как команды CMake орудуют лишь абстрактным понятием компилятора и не привязаны к платформенно зависимым инструментам, сильно различающихся на разных операционных системах.
Проверка версии CMake
Команда cmake_minimum_required
проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой. Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла:
# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
Как подметили в комментариях, команда cmake_minimum_required
выставляет все флаги совместимости (смотреть cmake_policy
). Некоторые разработчики намеренно выставляют низкую версию CMake, а затем корректируют функционал вручную. Это позволяет одновременно поддерживать древние версии CMake и местами использовать новые возможности.
Оформление проекта
В начале любого CMakeLists.txt
следует задать характеристики проекта командой project
для лучшего оформления интегрированными средами и прочими инструментами разработки.
# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
Стоит отметить, что если ключевое слово LANGUAGES
опущено, то по умолчанию задаются языки C CXX
. Вы также можете отключить указание любых языков путём написания ключевого слова NONE
в качестве списка языков или просто оставить пустой список.
Запуск скриптовых файлов
Команда include
заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include
языков C/C++. Этот пример запускает скриптовый файл MyCMakeScript.cmake
описанной командой:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
# Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake)
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
В данном примере, первое сообщение уведомит о том, что переменная TEST_VARIABLE
ещё не определена, однако если скрипт MyCMakeScript.cmake
определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной. Таким образом, скриптовый файл, включаемый командой include
, не создаёт собственной области видимости, о чём упомянули в комментариях к предыдущей статье.
Компиляция исполняемых файлов
Команда add_executable
компилирует исполняемый файл с заданным именем из списка исходников. Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe
или просто <ExecutableName>
). Типичный пример вызова данной команды:
# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
Компиляция библиотек
Команда add_library
компилирует библиотеку с указанным видом и именем из исходников. Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a
или <LibraryName>.lib
). Типичный пример вызова данной команды:
# Скомпилировать статическую библиотеку "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
, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:
- Переменные
RUNTIME_OUTPUT_DIRECTORY
иRUNTIME_OUTPUT_NAME
определяют местоположение целей выполнения; - Переменные
LIBRARY_OUTPUT_DIRECTORY
иLIBRARY_OUTPUT_NAME
определяют местоположение библиотек; - Переменные
ARCHIVE_OUTPUT_DIRECTORY
иARCHIVE_OUTPUT_NAME
определяют местоположение архивов.
Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для «не-DLL» платформ динамические библиотеки рассматриваются библиотечными целями, а для «DLL-платформ» — целями выполнения. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles
.
Важно подметить, что «DLL-платформами» считаются все платформы, основанные на Windows, в том числе и Cygwin.
Компоновка с библиотеками
Команда target_link_libraries
компонует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable
или add_library
, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Пример:
# Скомпоновать исполняемый файл "MyExecutable" с
# библиотеками "JsonParser", "SocketFactory" и "BrowserInvoker":
target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker)
Стоит отметить, что модульные библиотеки не подлежат компоновке с исполняемыми файлами или другими библиотеками, так как они предназначены исключительно для загрузки техниками выполнения.
Работа с целями
Как упомянули в комментариях, цели в CMake тоже подвержены ручному манипулированию, однако весьма ограниченному.
Имеется возможность управления свойствами целей, предназначенных для задания процесса сборки проекта. Команда get_target_property
присваивает предоставленной переменной значение свойства цели. Данный пример выводит значение свойства C_STANDARD
цели MyTarget
на экран:
# Присвоить переменной "VALUE" значение свойства "C_STANDARD":
get_target_property(VALUE MyTarget C_STANDARD)
# Вывести значение полученного свойства на экран:
message("'C_STANDARD' property is equal to [${VALUE}]")
Команда set_target_properties
устанавливает указанные свойства целей заданными значениями. Данная команда принимает список целей, для которых будут установлены значения свойств, а затем ключевое слово PROPERTIES
, после которого следует список вида <название свойства> <новое значение>
:
# Установить свойству 'C_STANDARD' значение "11",
# а свойству 'C_STANDARD_REQUIRED' значение "ON":
set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
Пример выше задал цели MyTarget
свойства, влияющие на процесс компиляции, а именно: при компиляции цели MyTarget
CMake затребует компилятора о использовании стандарта C11. Все известные именования свойств целей перечисляются на этой странице.
Также имеется возможность проверки ранее определённых целей с помощью конструкции if(TARGET <TargetName>)
:
# Выведет "The target was defined!" если цель "MyTarget" уже определена,
# а иначе выведет "The target was not defined!":
if(TARGET MyTarget)
message("The target was defined!")
else()
message("The target was not defined!")
endif()
Добавление подпроектов
Команда add_subdirectory
побуждает CMake к незамедлительной обработке указанного файла подпроекта. Пример ниже демонстрирует применение описанного механизма:
# Добавить каталог "subLibrary" в сборку основного проекта,
# а генерируемые файлы расположить в каталоге "subLibrary/build":
add_subdirectory(subLibrary subLibrary/build)
В данном примере первым аргументом команды add_subdirectory
выступает подпроект subLibrary
, а второй аргумент необязателен и информирует CMake о папке, предназначенной для генерируемых файлов включаемого подпроекта (например, CMakeCache.txt
и cmake_install.cmake
).
Стоит отметить, что все переменные из родительской области видимости унаследуются добавленным каталогом, а все переменные, определённые и переопределённые в данном каталоге, будут видимы лишь ему (если ключевое слово PARENT_SCOPE
не было определено аргументом команды set
). Данную особенность упомянули в комментариях к предыдущей статье.
Поиск пакетов
Команда find_package
находит и загружает настройки внешнего проекта. В большинстве случаев она применяется для последующей линковки внешних библиотек, таких как Boost и GSL. Данный пример вызывает описанную команду для поиска библиотеки GSL и последующей линковки:
# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED)
# Скомпоновать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)
# Уведомить компилятор о каталоге заголовков "GSL":
target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS})
В приведённом выше примере команда find_package
первым аргументом принимает наименование пакета, а затем требуемую версию. Опция REQUIRED
требует печати фатальной ошибки и завершении работы CMake, если требуемый пакет не найден. Противоположность — это опция QUIET
, требующая CMake продолжать свою работу, даже если пакет не был найден.
Далее исполняемый файл MyExecutable
линкуется с библиотекой GSL командой target_link_libraries
с помощью переменной GSL::gsl
, инкапсулирующей расположение уже скомпилированной GSL.
В конце вызывается команда target_include_directories
, информирующая компилятора о расположении заголовочных файлов библиотеки GSL. Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS
, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета).
Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET
. Это можно сделать путём проверки переменной <PackageName>_FOUND
, автоматически определяемой после завершения команды find_package
. Например, в случае успешного импортирования настроек GSL в Ваш проект, переменная GSL_FOUND
обратится в истину.
В общем случае, команда find_package
имеет две разновидности запуска: модульную и конфигурационную. Пример выше применял модульную форму. Это означает, что во время вызова команды CMake ищет скриптовый файл вида Find<PackageName>.cmake
в директории CMAKE_MODULE_PATH
, а затем запускает его и импортирует все необходимые настройки (в данном случае CMake запустила стандартный файл FindGSL.cmake
).
Способы включения заголовков
Информировать компилятора о располжении включаемых заголовков можно посредством двух команд: include_directories
и target_include_directories
. Вы решаете, какую из них использовать, однако стоит учесть некоторые различия между ними (идея предложена в комментариях).
Команда include_directories
влияет на область каталога. Это означает, что все директории заголовков, указанные данной командой, будут применяться для всех целей текущего CMakeLists.txt
, а также для обрабатываемых подпроектов (смотреть add_subdirectory
).
Команда target_include_directories
влияет лишь на указанную первым аргументом цель, а на другие цели никакого воздействия не оказывается. Пример ниже демонстрирует разницу между этими двумя командами:
add_executable(RequestGenerator RequestGenerator.c)
add_executable(ResponseGenerator ResponseGenerator.c)
# Применяется лишь для цели "RequestGenerator":
target_include_directories(RequestGenerator headers/specific)
# Применяется для целей "RequestGenerator" и "ResponseGenerator":
include_directories(headers)
В комментариях упомянуто, что в современных проектах применение команд include_directories
и link_libraries
является нежелательным. Альтернатива — это команды target_include_directories
и target_link_libraries
, действующие лишь на конкретные цели, а не на всю текущую область видимости.
Установка проекта
Команда 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 checkinstall
(если 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)
# Добавить в сборку статическую библиотеку "MyProgramCore":
add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c)
После череды команд cmake . && make && sudo checkinstall
работа системы сборки CMake завершается успешно. Первая команда запускает обработку файла CMakeLists.txt
в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпонованный исполняемый файл MyProgram
в систему.
Заключение
Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю. До скорых встреч!
В данной статье рассмотрено использование системы сборки CMake, применяемой в колоссальном количестве проектов на C/C++. Строго рекомендуется прочитать первую часть руководства во избежание непонимания синтаксиса языка CMake, явным образом фигурирующего на протяжении всей статьи.
Ниже приведены примеры использования языка CMake, по которым Вам следует попрактиковаться. Экспериментируйте с исходным кодом, меняя существующие команды и добавляя новые. Чтобы запустить данные примеры, установите CMake с официального сайта.
Система сборки CMake представляет из себя оболочку над другими платформенно зависимыми утилитами (например, Ninja или Make). Таким образом, в самом процессе сборки, как бы парадоксально это ни звучало, она непосредственного участия не принимает.
Система сборки CMake принимает на вход файл CMakeLists.txt
с описанием правил сборки на формальном языке CMake, а затем генерирует промежуточные и нативные файлы сборки в том же каталоге, принятых на Вашей платформе.
Сгенерированные файлы будут содержать конкретные названия системных утилит, директорий и компиляторов, в то время как команды CMake орудуют лишь абстрактным понятием компилятора и не привязаны к платформенно зависимым инструментам, сильно различающихся на разных операционных системах.
Команда cmake_minimum_required
проверяет запущенную версию CMake: если она меньше указанного минимума, то CMake завершает свою работу фатальной ошибкой. Пример, демонстрирующий типичное использование данной команды в начале любого CMake-файла:
# Задать третью минимальную версию CMake:
cmake_minimum_required(VERSION 3.0)
В начале любого CMakeLists.txt
следует задать характеристики проекта командой project
для лучшего оформления интегрированными средами и прочими инструментами разработки.
# Задать характеристики проекта "MyProject":
project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX)
Стоит отметить, что если ключевое слово LANGUAGES
опущено, то по умолчанию задаются языки C CXX
. Вы также можете отключить указание любых языков путём написания ключевого слова NONE
в качестве списка языков или просто оставить пустой список.
Команда include
заменяет строку своего вызова кодом заданного файла, действуя аналогично препроцессорной команде include
языков C/C++. Этот пример запускает скриптовый файл MyCMakeScript.cmake
описанной командой:
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
# Запустить скрипт `MyCMakeScript.cmake` на выполнение:
include(MyCMakeScript.cmake)
message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]")
В данном примере, первое сообщение уведомит том, что переменная TEST_VARIABLE
ещё не определена, однако если скрипт MyCMakeScript.cmake
определит данную переменную, то второе сообщение уже будет информировать о новом значении тестовой переменной. Таким образом, скриптовый файл, включаемый командой include
, не создаёт собственной области видимости, о чём метко упомянули в комментариях к предыдущей статье.
Команда add_executable
компилирует исполняемый файл с заданным именем из списка исходников. Важно отметить, что окончательное имя файла зависит от целевой платформы (например, <ExecutableName>.exe
или просто <ExecutableName>
). Типичный пример вызова данной команды:
# Скомпилировать исполняемый файл "MyExecutable" из
# исходников "ObjectHandler.c", "TimeManager.c" и "MessageGenerator.c":
add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c)
Команда add_library
компилирует библиотеку с указанным видом и именем из исходников. Важно отметить, что окончательное имя библиотеки зависит от целевой платформы (например, lib<LibraryName>.a
или <LibraryName>.lib
). Типичный пример вызова данной команды:
# Скомпилировать статическую библиотеку "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
, определяется только на стадии генерации, однако данное правило можно изменить несколькими переменными, определяющими конечное местоположение двоичных файлов:
- Переменные
RUNTIME_OUTPUT_DIRECTORY
иRUNTIME_OUTPUT_NAME
определяют местоположение целей выполнения. - Переменные
LIBRARY_OUTPUT_DIRECTORY
иLIBRARY_OUTPUT_NAME
определяют местоположение библиотек. - Переменные
ARCHIVE_OUTPUT_DIRECTORY
иARCHIVE_OUTPUT_NAME
определяют местоположение архивов.
Исполняемые файлы всегда рассматриваются целями выполнения, статические библиотеки — архивными целями, а модульные библиотеки — библиотечными целями. Для «не-DLL» платформ динамические библиотеки рассматриваются библиотечными целями, а для «DLL-платформ» — целями выполнения. Для объектных библиотек таких переменных не предусмотрено, поскольку такой вид библиотек генерируется в недрах каталога CMakeFiles
.
Важно подметить, что «DLL-платформами» считаются все платформы, основанные на Windows, в том числе и Cygwin.
Команда target_link_libraries
компанует библиотеку или исполняемый файл с другими предоставляемыми библиотеками. Первым аргументом данная команда принимает название цели, сгенерированной с помощью команд add_executable
или add_library
, а последующие аргументы представляют собой названия целей библиотек или полные пути к библиотекам. Пример:
# Скомпановать исполняемый файл "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
). Данную особенность упомянули в комментариях к предыдущей статье.
Команда find_package
находит и загружает настройки внешнего проекта. В большинстве случаев она применяется для последующей линковки внешних библиотек, таких как Boost и GSL. Данный пример вызывает описанную команду для поиска библиотеки GSL и последующей линковки:
# Загрузить настройки пакета библиотеки "GSL":
find_package(GSL 2.5 REQUIRED)
# Уведомить компилятор о каталоге заголовков "GSL":
include_directories(${GSL_INCLUDE_DIRS})
# Скомпановать исполняемый файл с библиотекой "GSL":
target_link_libraries(MyExecutable GSL::gsl)
В приведённом выше примере команда find_package
первым аргументом принимает наименование пакета, а затем требуемую версию. Опция REQUIRED
требует печати фатальной ошибки и завершении работы CMake если требуемый пакет не найден. Противоположность — это опция QUIET
, требующая CMake продолжать свою работу, даже если пакет не был найден.
Далее вызывается команда include_directories
, информирующая компилятора о расположении заголовочных файлов библиотеки GSL. Обратите внимание на то, что используется переменная GSL_INCLUDE_DIRS
, хранящая местоположение описанных мною заголовков (это пример импортированных настроек пакета).
В конечном итоге, исполняемый файл MyExecutable
линкуется с библиотекой GSL командой target_link_libraries
с помощью переменной GSL::gsl
, инкапсулирующей расположение самой GSL.
Вам, вероятно, захочеться проверить результат поиска пакета, если Вы указали опцию QUIET
. Это можно сделать путём проверки переменной <PackageName>_FOUND
, автоматически определяемой после завершения команды find_package
. Например, в случае успешного импортирования настроек 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)
После череды команд cmake . && make && sudo make install
работа системы сборки CMake завершается успешно. Первая команда побуждает обработку файла CMakeLists.txt
в корневом каталоге проекта, вторая команда окончательно компилирует необходимые двоичные файлы, а третья команда устанавливает скомпанованный исполняемый файл MyProgram
в систему.
Теперь Вы способны писать свои и понимать чужие CMake-файлы, а подробно прочитать про остальные механизмы Вы можете на официальном сайте.
Следующая статья данного руководства будет посвящена тестированию и созданию пакетов с помощью CMake и выйдет через неделю. До скорых встреч!
Let’s block ads! (Why?)
Добавлено 5 апреля 2023 в 21:41
На данный момент мы рассмотрели, как с помощью CMake создать простейший проект. В этом разделе мы узнаем, как создать и использовать библиотеку в нашем проекте. Мы также увидим, как сделать использование нашей библиотеки необязательным.
Упражнение 1. Создание библиотеки
Чтобы добавить библиотеку в CMake, используйте команду add_library()
и укажите, какие исходные файлы должны составлять эту библиотеку.
Вместо того, чтобы размещать все исходные файлы в одном каталоге, мы можем организовать наш проект с одним или несколькими подкаталогами. В данном случае мы создадим подкаталог специально для нашей библиотеки. В нем мы можем добавить новый файл CMakeLists.txt и один или несколько исходных файлов. В файле CMakeLists.txt верхнего уровня мы будем использовать команду add_subdirectory()
, чтобы добавить подкаталог в сборку.
Как только библиотека создана, она подключается к нашей исполняемой цели с помощью команд target_include_directories()
и target_link_libraries()
.
Цель
Добавить и использовать библиотеку.
Полезные ресурсы
add_library()
add_subdirectory()
target_include_directories()
target_link_libraries()
PROJECT_SOURCE_DIR
Редактируемые файлы
- CMakeLists.txt
- tutorial.cxx
- MathFunctions/CMakeLists.txt
С чего начать
В этом упражнении мы добавим в наш проект библиотеку, содержащую нашу собственную реализацию вычисления квадратного корня из числа. Затем исполняемый файл может использовать эту библиотеку вместо стандартной функции квадратного корня, предоставляемой компилятором.
Для этого урока мы поместим библиотеку в подкаталог MathFunctions. Этот каталог уже содержит заголовочный файл MathFunctions.h и исходный файл mysqrt.cxx. Нам не нужно будет изменять ни один из этих файлов. В исходном файле есть одна функция, mysqrt
, которая обеспечивает функционал, аналогичный функции sqrt
компилятора.
В каталоге Help/guide/tutorial/Step2 начните с TODO 1 и завершите TODO 6.
Сначала добавьте одну строку CMakeLists.txt в подкаталоге MathFunctions.
Затем отредактируйте CMakeLists.txt верхнего уровня.
И в конце используйте недавно созданную библиотеку MathFunctions
в tutorial.cxx.
Сборка и запуск
Запустите исполняемый файл cmake
или cmake-gui
, чтобы настроить проект, а затем собрать его с помощью выбранного инструмента сборки.
Ниже показано, как это выглядит из командной строки:
mkdir Step2_build
cd Step2_build
cmake ../Step2
cmake --build .
Попробуйте использовать только что собранное приложение Tutorial
и убедитесь, что оно по-прежнему выдает точные значения квадратного корня.
Решение
В файле CMakeLists.txt в каталоге MathFunctions мы создаем целевую библиотеку с именем MathFunctions
с помощью add_library()
. Исходный файл библиотеки передается в качестве аргумента функции add_library()
. Это выглядит как следующая строка:
TODO 1
MathFunctions/CMakeLists.txt
add_library(MathFunctions mysqrt.cxx)
Чтобы использовать новую библиотеку, добавим вызов add_subdirectory()
в файл CMakeLists.txt верхнего уровня, чтобы библиотека была собрана.
TODO 2
CMakeLists.txt
add_subdirectory(MathFunctions)
Затем новая цель библиотеки связывается с исполняемой целью с помощью target_link_libraries()
.
TODO 3
CMakeLists.txt
target_link_libraries(Tutorial PUBLIC MathFunctions)
Наконец, нам нужно указать расположение заголовочного файла библиотеки. Измените target_include_directories()
, чтобы добавить подкаталог MathFunctions в качестве включаемого каталога, чтобы можно было найти заголовочный файл MathFunctions.h.
TODO 4
CMakeLists.txt
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
"${PROJECT_SOURCE_DIR}/MathFunctions"
)
Теперь воспользуемся нашей библиотекой. В tutorial.cxx включите MathFunctions.h:
TODO 5
tutorial.cxx
#include "MathFunctions.h"
Наконец, замените вызов sqrt
нашей библиотечной функцией mysqrt
.
TODO 6
tutorial.cxx
const double outputValue = mysqrt(inputValue);
Упражнение 2. Делаем нашу библиотеку необязательной
Теперь давайте сделаем библиотеку MathFunctions
необязательной. Хотя для данного руководства в этом нет необходимости, для больших проектов это обычное дело.
CMake может сделать это с помощью команды option()
. Она дает пользователям переменную, которую они могут изменить при конфигурировании своей сборки cmake. Этот параметр будет храниться в кеше, поэтому пользователю не нужно устанавливать значение каждый раз, когда он запускает CMake в каталоге сборки.
Цель
Добавить возможность сборки без MathFunctions
.
Полезные ресурсы
if()
list()
option()
cmakedefine
Редактируемые файлы
- CMakeLists.txt
- tutorial.cxx
- TutorialConfig.h.in
С чего начать
Начните с файлов, полученных в упражнении 1. Выполните TODO 7–TODO 13.
Сначала создайте переменную USE_MYMATH
с помощью команды option()
в файле CMakeLists.txt верхнего уровня. В том же файле используйте этот параметр, чтобы определить, следует ли создавать и использовать библиотеку MathFunctions
.
Затем обновите tutorial.cxx и TutorialConfig.h.in, чтобы использовать USE_MYMATH
.
Сборка и запуск
Поскольку наш каталог сборки уже был настроен в упражнении 1, мы можем пересобрать его, просто вызвав следующие команды:
cd ../Step2_build
cmake --build .
Затем запустите исполняемый файл Tutorial
для нескольких чисел, чтобы убедиться, что он все еще правильно работает.
Теперь давайте изменим значение USE_MYMATH
на OFF
. Самый простой способ – использовать cmake-gui или ccmake, если вы находитесь в терминале. Или, наоборот, если вы хотите изменить параметр из командной строки, попробуйте:
cmake ../Step2 -DUSE_MYMATH=OFF
Теперь пересоберите код следующей командой:
cmake --build .
Затем снова запустите исполняемый файл, чтобы убедиться, что он всё еще работает с параметром USE_MYMATH
, установленным на OFF
. Какая функция дает лучшие результаты, sqrt
или mysqrt
?
Решение
Первый шаг – добавить параметр в файл CMakeLists.txt верхнего уровня. Этот параметр будет отображаться в cmake-gui и ccmake со значением по умолчанию ON
, которое может быть изменено пользователем.
TODO 7
CMakeLists.txt
option(USE_MYMATH "Use tutorial provided math implementation" ON)
Затем сделайте сборку и компоновку библиотеки MathFunctions
условной.
Начните с создания списка необязательных целевых библиотек для нашего проекта с помощью list()
. На данный момент это только MathFunctions
. Назовем наш список EXTRA_LIBS
.
Точно так же нам нужно создать list()
для необязательных включений, которые мы назовем EXTRA_INCLUDES
. В этом списке мы добавим путь к заголовочному файлу, необходимому для нашей библиотеки.
Затем создайте оператор if()
, который проверяет значение USE_MYMATH
. Внутри блока if()
поместите команду add_subdirectory()
из упражнения 1 с дополнительными командами list()
.
Когда USE_MYMATH
установлена в ON
, списки будут сгенерированы и добавлены в наш проект. Когда USE_MYMATH
установлена в OFF
, списки остаются пустыми. С помощью этой стратегии мы позволяем пользователям переключать USE_MYMATH
, чтобы управлять библиотекой, используемой в сборке.
Файл CMakeLists.txt верхнего уровня теперь будет выглядеть следующим образом:
TODO 8
CMakeLists.txt
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()
Теперь, когда у нас есть эти два списка, нам нужно обновить target_link_libraries()
и target_include_directories()
. Изменить их достаточно просто.
Для target_link_libraries()
мы заменяем записанные имена библиотек на EXTRA_LIBS
. Это выглядит следующим образом:
TODO 9
CMakeLists.txt
target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})
Затем делаем то же самое с target_include_directories()
и EXTRA_INCLUDES
.
TODO 10
CMakeLists.txt
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
${EXTRA_INCLUDES}
)
Обратите внимание, что это классический подход при работе со многими компонентами. Современный подход мы рассмотрим в шаге 3 данного руководства.
Соответствующие изменения в исходном коде довольно просты. Во-первых, в tutorial.cxx мы включаем заголовок MathFunctions.h, если определена USE_MYMATH
.
TODO 11
tutorial.cxx
#ifdef USE_MYMATH
# include "MathFunctions.h"
#endif
Затем в том же файле мы делаем так, чтобы USE_MYMATH
контролировала, какая функция квадратного корня используется:
TODO 12
tutorial.cxx
#ifdef USE_MYMATH
const double outputValue = mysqrt(inputValue);
#else
const double outputValue = sqrt(inputValue);
#endif
Поскольку исходный код теперь требует USE_MYMATH
, мы можем добавить её в TutorialConfig.h.in с помощью следующей строки:
TODO 13
TutorialConfig.h.in
#cmakedefine USE_MYMATH
С этими изменениями наша библиотека теперь совершенно необязательна для тех, кто ее создает и использует.
Бонусный вопрос
Почему важно сконфигурировать TutorialConfig.h.in после добавления параметра USE_MYMATH
? Что произойдет, если мы перевернем их местами?
Показать/скрыть ответ
Мы конфигурируем его после, потому что TutorialConfig.h.in использует значение
USE_MYMATH
. Если мы сконфигурируем этот файл перед вызовомoption()
, мы не будем использовать ожидаемое значениеUSE_MYMATH
.
Теги
C++ / CppCMakeАвтоматизация сборкиПрограммированиеСистема сборкиСтатическая библиотека
Introduction
CMake is a cross-platform software for building projects written in C, C++, Fortran, CUDA and so on. CMake utilizes build-systems such as Ninja, Linux make, Visual Studio, and Xcode. It compiles projects with compilers like GCC, Clang, Intel, MS Visual C++.
CMake is frequently used in compiling open-source and commercial projects. It has comprehensive but daunting manual instruction. In this post, instead of throwing instructions for some random commands, I aim to explain how to employ modern CMake step by step to build executables (applications) and static/shared/header-only libraries from C++ projects.
Prerequisits
I am assuming
- you had a look at my post on CMake programming,
- you have CMake v3.23 on your machine,
- you have a compiler like GCC, Clang, Intel, or MS Visual C++ installed on your operating system.
Compile examples
Examples are on GitHub here and their links are mentioned in each section as well. To build an example, go to its directory in a terminal and run
Usual build configurations are Debug, Release, RelWithDebInfo and MinSizeRel.
For single configuration generators like make
and Ninja run:
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
For multi-configuration generators like Visual Studio or Xcode, the configuration can be set at the building stage:
cmake ..
cmake --build . --config Release
To install targets, after building the project, run CMake like
cmake --install . --prefix "/home/myuser/installdir " --config Release
Note that --config Release
only works for multi-configuration generators. For a single configuration generator, you already set the CMAKE_BUILD_TYPE before building the project.
Basic executable
The code for this example is here on GitHub. It is a simple executable (or application). The project folder is like this:
---example1
|
----- app.cpp
|
----- shape.cpp
|
----- shape.h
|
----- CMakeLists.txt
app.cpp
includes shape.h
. The CMakeLists.txt
is
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
add_executable(app)
target_sources(app PRIVATE app.cpp shape.cpp shape.h)
install(TARGETS app)
add_executable()
: used to declare the executable target,app
. You can choose any other name instead ofapp
.target_sources()
: we add source files necessary to compileapp
target.PRIVATE
: to say that sources are just for creatingapp
target and nothing else.install()
: will copy the compiled executable intopath/to/install/directory/bin
when you runcmake --install blah blah
Executable with subdirectories
The code for this example is here on GitHub.
In this example, we have source files and headers distributed in subdirectories:
--- geometry
|
---- shape
| |
| --- shape.h
| |
| --- shape.cpp
| |
| --- CMakeLists.txt
|
---- square
| |
| --- square.h
| |
| --- square.cpp
| |
| --- CMakeLists.txt
|
---- app.cpp
|
---- CMakeLists.txt
Where geometries/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.20)
project(geometries LANGUAGES CXX)
add_executable(app)
target_sources(app PRIVATE "app.cpp")
target_include_directories(app PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")
install(TARGETS app)
-
add_executable()
: is to defineapp
target. -
target_sources()
: to add the source in the currrent directory,app.cpp
, toapp
target. -
target_include_directories()
: To tell CMake that the project directory tree contains headers. In this way, we can have headers from different directories added to each other with a relative path to the project directory. For example,square.h
can have#include "shape/shape.h"
. -
PRIVATE
: fortarget_*
means the added files and directories are just for creating targets, not for linking to them. -
add_subdirectory()
: to tell CMake to go into those subdirectories as there are more logics there in theirCMakeLists.txt
files.
shape/CMakeLists.txt
is just
target_sources(app PRIVATE shape.cpp shape.h)
and square/CMakeLists.txt
is
target_sources(app PRIVATE square.cpp square.h)
So we added sources in the subdirectories to app
target.
Executable with namespace
The code for this example is here on GitHub.
This example is similar to a big project with namespaces. Namespaces are used to avoid name conflicts in a project, read more on them in this post. The CMake script is very similar to the previous example.
The project folder is like
--- geometry
|
---- shape
| |
| --- base.h, base.cpp
| |
| --- CMakeLists.txt
|
---- rectangle
| |
| --- base.h, base.cpp
| |
| --- CMakeLists.txt
|
---- square
| |
| --- base.h, base.cpp
| |
| --- CMakeLists.txt
|
---- app.cpp
|
---- CMakeLists.txt
app.cpp
is
#include "square/base.h"
#include "rectangle/base.h"
using namespace Geometry;
int main() {
Square::Base s;
Rectangle::Base r;
Shape::Print(s);
Shape::Print(r);
return 0;
}
base
files represent base class for different shapes. It is assumed the developer will derived more classes from them. We have files and classes with the same name, but they are elegently resolved with namespaces and CMake.
The app/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.20)
project(geometry LANGUAGES CXX)
add_executable(app)
target_sources(app PRIVATE "app.cpp")
target_compile_features(app PRIVATE cxx_std_20)
target_include_directories(app PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")
add_subdirectory("rectangle")
install(TARGETS app)
target_compile_features()
: to tell CMake that we need C++20 for compiling this project. There are haigh-level features likecxx_std_11
,cxx_std_14
and low-level ones likecxx_constexpr
andcxx_auto_type
. Adding low-level ones, CMake automatically figures out which standard to use. See more features here.
CMakeLists.txt
in shape, rectangle and square are the same:
target_sources(app PRIVATE base.cpp base.h)
The code for this example is here on GitHub.
In this example, we compile a library and link it to an executable
--- geometry
|
---- shape
| |
| --- shape.h, shape.cpp
| |
| --- CMakeLists.txt
|
---- square
| |
| --- square.h, square.cpp, info.h
| |
| --- CMakeLists.txt
|
---- example
| |
| --- app.cpp
|
---- CMakeLists.txt
The geometries/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
if (MSVC)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
add_library(geo SHARED)
target_include_directories(geo PRIVATE "${PROJECT_SOURCE_DIR}")
add_subdirectory("shape")
add_subdirectory("square")
add_executable(app)
target_sources(app PRIVATE "example/app.cpp")
target_link_libraries(app PRIVATE geo)
install(TARGETS geo FILE_SET HEADERS)
-
if (MSVC)
: checking CMake is employing MS Visual C++. -
CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
: This is necessary for MSVC to create a symbol file,.lib
, besides a shared library,.dll
. -
add_library()
: to define a library target,geo
.SHARED
means a shared library, you can also make a static library withSTATIC
keyword, or an object file withOBJECT
keyword. -
target_include_directories()
: is for making source files aware of the location of private headers relative to the project directory. -
target_link_libraries()
: to tell CMake thatapp
is dependent ongeo
library. So first compilegeo
then link it toapp
executable. -
install(TARGETS)
: to install compiled libraries and their headers in the assigned install directory you set when runningcmake --install blah blah
. Executables and windows dll files go intobin
directory, libraries go intolib
directory, and public headers go intoinclude
directory at the destination.Install(TARGETS)
has keywords to fine-tune the destination directories, see this example:
install(TARGETS myTarget
# for executables and dll on Win
RUNTIME DESTINATION bin
# shared libraries
LIBRARY DESTINATION lib
# for static libraries
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include)
For more info see CMake manual here.
shape/CMakeLists.txt
is
target_sources(geo
PRIVATE shape.cpp
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES shape.h)
square/CMakeLists.txt
is
target_sources(geo
PRIVATE square.cpp info.h
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES square.h)
-
PRIVATE
:info.h
is a private header of this library, a user doesn’t need to know about it. The same for*.cpp
files as they are only needed to compile the library, but a user doesn’t need them. -
PUBLIC
: Any files added afterPUBLIC
is used for compiling the library and included for any other target that linking to this library. -
FILE_SET HEADERS BASE_DIRS ${PROJECT_SOURCE_DIR} FILES square.h
: is a CMake 3.23 feature. We know to link to a library, we need its public headers. This line makes sure any other target linking togeo
, gets aware of the header location. The base directory is droped from header file path so it will be accessible with a relative path. For example/path/to/geometry/square/square.h
will be included assquare/square.h
.
Therefore, in the geometry/CMakeLists.txt
, this line
install(TARGETS geo FILE_SET HEADERS)
means that CMake installs the public headers in the include
directory with their relative path, like install/path/include/square/square.h
.
The code for this example is here on GitHub.
A header-only library has all the implementations defined in headers. There are .h
/.hpp
files and but no .cpp
files except for tests. This type of library is not compiled standalone. But other projects link to them. Therefore, when a header-only library is installed, only header files are copied at the destination.
The example for this case has the below structure:
--- geometry
|
---- shape
| |
| --- shape.h
| |
| --- CMakeLists.txt
|
---- square
| |
| --- square.h
| |
| --- CMakeLists.txt
|
---- examples
| |
| --- example1.cpp
|
--- geometry.h
|
---- CMakeLists.txt
The example1.cpp
is like this
#include "geometry.h"
int main() {
Square s;
PrintShape(s);
return 0;
}
The geometry/CMakeLists.txt
is
cmake_minimum_required(VERSION 3.23)
project(geometry LANGUAGES CXX)
add_library(geo INTERFACE)
target_sources(geo INTERFACE
FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES "geometry.h")
add_subdirectory("shape")
add_subdirectory("square")
add_executable(example1)
target_sources(example1 PRIVATE "examples/example1.cpp")
target_link_libraries(example1 PRIVATE geo)
install(TARGETS geo FILE_SET HEADERS)
-
add_library(geo INTERFACE)
: Here INTERFACE means it is a header-only library, i.e. this library is not compiled. -
target_sources()
: heregeometry.h
is declared and mentioned to be an interface header. Any other project linking to this library should be aware of this and its location. The subdirectories using the same technique.
shape/CMakeLists.txt
is
target_sources(geo
INTERFACE FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES shape.h)
and square/CMakeLists.txt
is
target_sources(geo
INTERFACE FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES square.h)
More on CMake
My next post is on creating config files in CMake to find package. And in case you haven’t seen it, my previous post was on programming in CMake.
Even more
Designing a big project needs a good understanding of namespaces, see my post on how namespaces are used in big projects.
Did you know CMake is supported by Visual Studio code? have a look at my essential list of VS code extensions for C++.
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks