Содержание
См. также статью Современный CMake: 10 советов по улучшению скриптов сборки.
Терминология
- Файл
CMakeLists.txt
служит скриптом (рецептом, сценарием) сборки проекта. Обычно один такой файл собирает все исходники в своём каталоге и в подкаталогах, при этом подкаталоги могут содержать, а могут не содержать дочерние файлыCMakeLists.txt
. С точки зрения IDE, таких как CLion или Visual Studio, файлCMakeLists.txt
также служит проектом, с которым работает программист внутри IDE. - В cmake есть “цель” (“target”) — компонент, который следует собрать. Компонент может быть исполняемым файлом, так и статической либо динамической библиотекой.
- В cmake есть “проект” (“project”) — это набор компонентов, по смыслу похожий на Solution в Visual Studio.
- В cmake есть “флаги” (flags) — это аргументы командной строки для компилятора, компоновщика и других утилит, вызываемых при сборке.
- В cmake есть переменные, и в процессе интерпретации файла
CMakeLists.txt
система сборки cmake вычисляет ряд встроенных переменных для каждой цели, тем самым получая флаги. Затем cmake создаёт вторичный скрипт сборки, который будет напрямую вызывать компилятор, компоновщик и другие утилиты с вычисленными флагами.
Сборка проекта из командной строки (Linux)
На первом шаге проект нужно сконфигурировать, то есть создать финальный скрипт сборки, запустив cmake <параметры> <путь-к-каталогу>
в будущем каталоге сборки.
# Сейчас мы в каталоге `myapp` с файлом CMakeLists.txt
# Создадим каталог `myapp-release` и перейдём в него.
mkdir --parents ../myapp-release
cd ../myapp-release
# Сконфигурируем проект для сборки в Release.
# Флаг установит опцию CMAKE_BUILD_TYPE в значение "Release",
# интерпретатор cmake считает это переключением на Release конфигурацию.
cmake -DCMAKE_BUILD_TYPE=Release ../myapp
На втором шаге нужно запустить финальный скрипт. Не вызывайте make
! Утилита cmake сделает это сама:
# Просим CMake запустить сборку в каталоге `myapp-release`
# Можно добавить флаг `--target mylibrary` для сборки только mylibrary
# Можно добавить флаг `--clean-first`, чтобы в начале новой сборки
# стирать остатки предыдущей.
cmake --build .
# Аналогичный способ для GNU/Linux. Его по привычке советуют в сети, хотя
# make доступен не всегда, а cmake --build работает на всех платформах.
make
Структура CMakeLists.txt
В начале главного файла CMakeLists.txt
ставят метаинформацию о минимальной версии CMake и названии проекта:
# Указывайте последнюю доступную вам версию CMake.
cmake_minimum_required(VERSION 3.8)
# Синтаксис: project(<имя> VERSION <версия> LANGUAGES CXX),
# теги VERSION и LANGUAGES необязательные.
project(libmodel3d)
Затем следует список инструкций, служащих для вычисления различных переменных, создания целей сборки,
подключения проектов из подкаталогов и так далее. Например, подключить дополнительный CMakeLists.txt
из подкаталога можно так:
# Простая версия: подключает скрипт по пути <подкаталог>/CMakeLists.txt
add_subdirectory(<подкаталог>)
# Расширенная версия: дополнительно установит подкаталог сборки подпроекта
add_subdirectory(<подкаталог> <подкаталог сборки>)
Целью может стать исполняемый файл, собираемый из исходного кода
# Синтаксис: add_executable(<имя> <список исходников...>)
# Добавлять `.h` необязательно, но лучше для работы из IDE:
# - IDE определит заголовок как часть проекта
# - cmake будет отслеживать изменения в заголовке и пересобирать
# проект при изменениях.
add_executable(pngconverter main.cpp PngReader.h PngReader.cpp)
Целью также может быть библиотека, статическая или динамическая.
# Синтаксис: add_library(<имя> [STATIC|SHARED|INTERFACE] <список исходников...>)
# Тип библиотеки (staic или shared) зависит от параметров сборки
add_library(libpngutils PngUtils.h PngUtils.cpp)
# Тип библиотеки: static
add_library(libpngtools STATIC PngTools.h PngTools.cpp)
Автогенерация проекта для Visual Studio (Windows)
Если используется Visual C++, то путь немного другой: на шаге конфигурирования создаётся проект для Visual Studio, который затем можно собрать из IDE либо так же из командной строки.
Созданный проект Visual Studio нельзя изменять и использовать постоянно, потому что при генерации проекта используются абсолютные пути и другие неприемлемые для постоянной работы вещи.
# Сейчас мы в каталоге `myapp` с файлом CMakeLists.txt
# Сгенерируем проект Visual Studio для сборки.
mkdir --parents ../myapp-build
cd ../myapp-build
# Конфигурируем для сборки с Visual Studio 2017, версия тулчейна v140
cmake -G "Visual Studio 2017"
Если проект был сконфигурирован успешно, то в каталоге ../myapp-build
появятся автоматически сгенерированный BUILD_ALL.sln и проекты для Visual Studio. Их можно открыть к IDE, либо собрать из командной строки с помощью cmake. Названия опций говорят сами за себя:
cmake --build .
--target myapp
--config Release
--clean-first
Зависимости между библиотеками и приложениями
Не используйте директивы include_directories
, add_definitions
, add_compile_options
!
Они меняют глобальные настройки для всех целей, это создаёт проблемы при масштабировании.
- Используйте target_link_libraries для добавления статических и динамических библиотек, от которых зависит цель
- Используйте target_include_directories вместо include_directories для добавления путей поиска заголовков, от которых зависит цель
- Используйте target_compile_definitions вместо add_definitions для добавления макросов, с которыми собирается цель
- Используйте target_compile_options для добавления специфичных флагов компилятора, с которыми собирается цель
Пример:
# Добавляем цель - статическую библиотеку
add_library(mylibrary STATIC
ColorDialog.h ColorDialog.cpp
ColorPanel.h ColorPanel.cpp)
# ! Осторожно - непереносимый код !
# Добавляем к цели путь поиска заголовков /usr/include/wx-3.0
# Лучше использовать find_package для получения пути к заголовкам.
target_include_directories(mylibrary /usr/include/wx-3.0)
Вы можете выбирать область видимости настройки:
PUBLIC
делает настройку видимой для текущей цели и для всех зависящих от неё целейPRIVATE
делает настройку видимой только для текущей целиINTERFACE
делает настройку видимой только для всех зависящих от неё целей
Пример использования областей видимости:
# Каталог include будет добавлен к путям поиска заголовков в текущей цели и во всех зависимых целях
target_include_directories(myTarget PUBLIC ./include)
# Каталог src будет добавлен к путям поиска заголовков только в текущей цели
target_include_directories(myTarget PUBLIC ./src)
Схема зависимостей условного проекта:
Выбор стандарта и диалекта C++
Для настройки стандарта и флагов языка C++ не добавляйте флаги напрямую!
# ! Устаревший метод - прямое указание флага !
target_compile_options(hello PRIVATE -std=c++11)
В CMake версии 3.8+ вы можете прямо потребовать включить нужный стандарт:
# Источник: https://cmake.org/cmake/help/latest/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html
# Включаем C++ 2017
target_compile_features(myapp cxx_std_17)
# Альтернатива: включаем C++ 2014
target_compile_features(myapp cxx_std_14)
# Альтернатива: включаем C++ 2011
target_compile_features(myapp cxx_std_11)
В CMake версии до 3.7 включительно можно использовать set_target_properties
(если не работает, то у вас слишком старый CMake):
# Стандарт: C++ 2014, расширения языка от производителя компилятора отключены
set_target_properties(myapp PROPERTIES
CXX_STANDARD 14
CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO
)
Для разработчиков библиотек есть более тонкий контроль над возможностями языка:
# API библиотеки (т.е. заголовки) требуют лямбда-функций и override,
# реализация библиотеки требует ещё и range-based for.
target_compile_features(mylibrary PUBLIC cxx_override cxx_lambdas PRIVATE cxx_range_for)
Функции в CMake
CMake позволяет объявлять функции командами function(name) / endfunction()
и макросы командами macro(name) / endmacro()
. Предпочитайте функции, а не макросы, т.к. у функций есть своя область видимости переменных, а у макросов — нет.
function(hello_get_something var_name)
...
# Установить переменную в области видимости вызывающей стороны
# можно с помощью тега PARENT_SCOPE
set(${var_name} ${ret} PARENT_SCOPE)
endfunction()
Добавление исходников к цели с target_sources
Лучше добавлять специфичные исходники с помощью target_sources, а не с помощью дополнительных переменных.
add_library(hello hello.cxx)
if(WIN32)
target_sources(hello PRIVATE system_win.cxx)
elseif(UNIX)
target_sources(hello PRIVATE system_posix.cxx)
else()
target_sources(hello PRIVATE system_generic.cxx)
endif()
Интерфейс к утилитам командной строки
Подробнее см. Command-Line Tool Mode
# Создать каталог debug-build
cmake -E make_directory debug-build
# Перейти в каталог debug-build
cmake -E chdir debug-build
Функция find_package
Функция find_package принимает имя библиотеки как аргумент и обращается к CMake, чтобы найти скрипт для настройки переменных данной библиотеки. В итоге при сборке либо возникает ошибка из-за того что пакет не найден, либо добавляются переменные, хранящие пути поиска заголовков, имена библиотек для компоновщика и другие параметры.
Пример подключения Boost, вызывающего встроенный в CMake скрипт FindBoost:
# Весь Boost без указания конкретных компонентов
find_package(Boost REQUIRED)
# Теперь доступны переменные
# - Boost_INCLUDE_DIRS: пути к заголовочным файлам
# - Boost_LIBRARY_DIRS: пути к статическим/динамическим библиотекам
# - Boost_LIBRARIES: список библиотек для компоновщика
# - Boost_<C>_LIBRARY: библиотека для компоновки с компонентом <C> библиотек Boost
Пример подключения библиотеки Bullet с помощью встроенного скрипта FindBullet и компоновки с приложением my_app:
# Вызываем встроенный скрипт FindBullet.cmake
find_package(Bullet REQUIRED)
# Добавляем пути поиска заголовков к цели my_app
target_include_directories(my_app ${BULLET_INCLUDE_DIRS})
# Добавляем список библиотек для компоновки с целью my_app
target_link_libraries(my_app ${BULLET_LIBRARIES})
Полное руководство по 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 и выйдет через неделю. До скорых встреч!
Эта статья находится в разработке!
Содержание
- 1 Что это и зачем нужно
- 1.1 Краткое описание
- 2 Старт
- 3 Подробное описание
- 3.1 Указание необходимой версии cmake
- 3.2 Название проекта
- 3.3 Переменные
- 3.4 Устанавливаем команды компилятору
- 3.5 Папка с хедерами
- 3.6 Самое важное — подключение библиотек
- 3.7 Пример хорошего CMakeLists.txt и где он будет лежать
- 3.8 Как создать библиотеку в поддиректории и слинковать ее с основной программой
- 4 Как использовать CMake в связке с QtCreator
- 4.1 Как добавить header в проект, чтобы его было видно в списке файлов
Что это и зачем нужно
CMake — кроссплатформенная автоматизированная система сборки проектов.
Непосредственно сборкой она не занимается, а только генерирует Makefile, который потом будет выполнен утилитой make.
CMake может проверять наличие необходимых библиотек и подключать их, собирать проекты под разными компиляторами и операционными системами. Т.е. у вас есть куча кода и файлик, содержащий информацию для cmake, и чтобы скомпилить
это дело где-нибудь еще, вам нужно просто запустить там cmake, который сделает всё сам. Удобно, полезно, просто.
Краткое описание
Если нет желания/времени/сил читать весь туториал и Вы используете какой-нибудь QtCreator (или любая другая IDE, умеющая работать с cmake), то:
- Создайте в IDE проект под cmake
- Найдите в папке с проектом CMakeFiles.txt
- Пробегитесь глазами по туториалу, соотнося его с вашим CMakeFiles.txt
Про подключение библиотек рекомендуется все-таки прочитать целиком.
Старт
Предполагается, что найти и скачать сам cmake ты, %username%, в состоянии. //а если нет?
Предположим, у Вас есть исходничек «test.cpp» (// а если нет?)(А если нет, то CMake тебе трогать рано).
Для начала нужно создать файлик для cmake, который обычно называют «CMakeLists.txt», и написать туда вот это:
add_executable(test test.cpp)
Теперь запускаем (из консоли) в этой папке команду «cmake CMakeLists.txt» (аргументом можно передавать не только файл, но и директорию, в которой он лежит, тогда cmake найдет его сам).
cmake будет использовать переданный (или найденный) файл проекта (тот самый CMakeLists.txt), и в текущей директории будет создавать проект.
Проект — это много-много файлов и директорий (примечание: поэтому лучше запускать cmake из другой директории, чтобы можно было, например, быстро удалить все бинарники), из которых нас больше всего интересует Makefile.
Makefile — это файл, нужный для утилиты make.
Именно она запускает компиляторы, линковщики и прочие радости. Запускаем make в каталоге сборки (т.е. там же, где Вы запускали cmake).
В консоли вылезет примерно такой текст:
Scanning dependencies of target test [100%] Building CXX object CMakeFiles/test.dir/test.cpp.o Linking CXX executable test [100%] Built target test
А у Вас в папочке появится исполняемый файл «test». Запустите, убедитесь, что это действительно то, что ожидается от компиляции файла «test.cpp».
Подробное описание
Поразбираемся с различными возможностями cmake.
Указание необходимой версии cmake
cmake_minimum_required(VERSION 2.6)
Указывайте высокую минимальную версию CMake.
Если используемая версия cmake меньше 2.6, он не захочет работать. Писать эту команду всегда — хороший стиль (cmake будет пыхтеть и обижаться, если вы не укажете версию, но собирать всё равно всё будет).
Название проекта
project(visualization)
Указывает, что этот cmake-файл является корневым для некоторого проекта. С проектами связаны определенные переменные и поведение cmake (читайте документацию).
Переменные
В cmake можно создавать текстовые переменные. Команда
set(VARIABLE The variable's value)
запишет в переменную «VARIABLE» значение «The variable’s value».
Чтобы где-либо использовать значение этой переменной, нужно написать ${VARIABLE}.
Чтобы добавить к переменной некий текст, можно сделать так:
set(VARIABLE "${VARIABLE} new text")
Как видите, использовать значение можно и внутри кавычек.
Переменные активно используются различными библиотеками — для установки флагов, параметров сборки/линковки и прочих вкусностей, об этом чуть-чуть попозже.
Пример коше’гного проекта со списком сорцов в отдельной переменной:
cmake_minimum_required(VERSION 2.6) set(SOURCES test.cpp lib1.cpp lib2.cpp) add_executable(test ${SOURCES})
Устанавливаем команды компилятору
add_definitions(-DSOME_IMPORTANT_DEFINITION)
Эта команда используется для установки дефайнов, которыe можно проверить в коде через, например, #ifdef SOME_IMPORTANT_DEFINITION.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall")
Эта команда добавит к флагам, используемым при сборке c++-кода, флаги -std=c++11 и -Wall.
Кто не знает: «-std=c++11» включает в gcc поддержку стандарта c++11, «-Wall» говорит gcc выводить все предупреждения (очень советую, помогает отловить много глупых багов и писать аккуратный код).
Если ваша версия GCC меньше, чем 4.7.0, вместо -std=c++11 нужно использовать -std=c++0x.
В GCC 4.8.0 появился флаг -std=c++1y, в котором начинают реализовывать фичи следующего стандарта.
Папка с хедерами
Допустим, Вы хотите, чтобы хедеры (файлики, подключаемые через #include) искались еще и в каталогах «headers/» и «more_headers/»:
include_directories("headers/" "more_headers/")
Надеюсь, и это понятно.
Самое важное — подключение библиотек
Научимся искать и подключать библиотеки при помощи cmake на примере Boost.
Для начала установим переменные для буста:
set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_MULTITHREADED ON)
Первое — мы не хотим, чтобы буст подключался к нам статически (т.е. хотим динамическую линковку). Если ты, %username%, не знаешь, что это, пока просто забей и используй этот флаг так, как написано. Но в ближайшее время узнай, о чем речь.
Второй флаг разрешает бусту внутри своих магических реализаций использовать треды для распараллеливания и прочих радостей.
Итак, мы установили флаги. Давайте найдем буст!
Допустим, нам нужны компоненты буста под названием chrono (библиотека для работы со временем) и filesystem (библиотека для работы с файловой системой):
find_package(Boost COMPONENTS chrono filesystem REQUIRED)
Win, будут искаться только нужные библиотеки, и их расположение будет записано в переменную Boost_LIBRARIES.
Опция «REQUIRED» говорит о том, что библиотека необходима проекту.
Без нее cmake решит, что отсутствие данной библиотеки — не так уж и страшно, и будет собирать дальше.
Добавим директории с хедерами буста для поиска в них хедеров:
include_directories(${Boost_INCLUDE_DIRS})
Итак, осталось найденные библиотеки подключить к исполняемому файлу.
target_link_libraries(test ${Boost_LIBRARIES})
В качестве библиотек нужно указать пути к необходимым собранным библиотекам. cmake нашел указанные нами библиотеки и записал в переменную, чем мы и пользуемся.
Заметим, что эту команду нужно вызывать после того, как создан target сборки (через add_executable).
Пример хорошего CMakeLists.txt и где он будет лежать
Итак, полный пример использования всего этого. У нас есть некая директория (отныне считаем ее «/sources»), и в ней лежат исходники
/sources/lib1/main.cpp /sources/lib2/main.cpp /sources/main.cpp
В корне «/» лежит файл «/CMakeLists.txt»:
cmake_minimum_required(VERSION 2.8) project(cmake-example) set(Boost_USE_STATIC_LIBS OFF) set(Boost_USE_MULTITHREADED ON) find_package(Boost COMPONENTS chrono filesystem REQUIRED) set(CMAKE_CXX_FLAGS "$$${CMAKE_CXX_FLAGS} -std=c++11 -Wall") set(SRC_LIST lib1/main.cpp lib2/main.cpp main.cpp) add_executable($$${PROJECT_NAME} $$${SRC_LIST}) target_link_libraries($$${PROJECT_NAME} $$${Boost_LIBRARIES})
Если Вам что-то в нём не понятно — перечитайте соответствующую информацию выше.
Создаем директорию «/build» (не «/sources/build»), переходим в нее, запускаем в ней «cmake ..».
«..» — метка родительской директории.
cmake возьмет из нее наш CMakeLists.txt и по нему создаст проект в папке «/build».
Чтобы проект собрать, запускаем «make» в той же папке «/build».
Таким образом, в корне у нас есть:
- CMakeLists.txt
- директория с исходниками
- каталог сборки
Все разделено, автоматизировано и удобно.
Как создать библиотеку в поддиректории и слинковать ее с основной программой
Пусть в ./ лежит основной проект, а в ./subdir мы хотим сделать либу, а в ./build построить проект.
./subdir/CMakeLists.txt
project(MegaLibrary) set(SOURCES lib.cpp) set(HEADERS lib.h) add_library(lib $$${SOURCES} $$${HEADERS}) target_include_directories(lib PUBLIC $$${CMAKE_CURRENT_SOURCE_DIR})
./CMakeLists.txt
project(MainProject) set(MAIN_PROJECT_SRC_LIST main) # Other stuff
add_executable(main $$${MAIN_PROJECT_SRC_LIST}) add_subdirectory(subdir) target_link_libraries(main lib)
Теперь можно в файлах основного проекта делать #include «lib.h» (см. документацию по target_include_directories).
В ./build запускаем «cmake .. && make» и получаем собранный проект.
Как использовать CMake в связке с QtCreator
Интеграция с cmake у QtCreator не очень тесная, тем не менее, работать с ним можно.
Создаем новый проект без использования Qt, выбираем «Проект на С++ с использованием CMake». Создастся дефолтный файл сборки, который просто добавляет все исходники в директории проекта и компилирует их в один бинарник.
Если вы создали файл header.h в директорию проекта, просто строчку
add_executable($$${PROJECT_NAME} $$${SRC_LIST})
измените на
add_executable($$${PROJECT_NAME} $$${SRC_LIST} "header.h")
Добавлено 7 января 2023 в 23:23
С чего начать работу с CMake? Этот шаг познакомит вас с базовыми синтаксисом, некоторыми командами и переменными CMake. По мере введения этих концепций мы проработаем три упражнения и создадим простой проект CMake.
Каждое упражнение на этом этапе начинается с некоторой исходной информации. Затем предоставляется цель и список полезных ресурсов. Каждый файл в разделе «Редактируемые файлы» находится в каталоге Step1 и содержит один или несколько комментариев TODO. Каждое TODO представляет собой одну или две строки кода, которые нужно изменить или добавить. TODO предназначены для выполнения в порядке нумерации, сначала выполните TODO 1, затем TODO 2 и т. д. В разделе «С чего начать» вы найдете несколько полезных советов и руководство по выполнению упражнения. Затем в разделе «Сборка и запуск» будет пошагово показано, как создать и протестировать упражнение. Наконец, в конце каждого упражнения обсуждается предполагаемое решение.
Также обратите внимание, что каждый шаг в руководстве основан на следующем. Так, например, исходный код для Шага 2 является полным решением для Шага 1.
Упражнение 1. Создание базового проекта
Самый простой проект CMake – это исполняемый файл, созданный из одного файла исходного кода. Для таких простых проектов достаточно файла CMakeLists.txt с тремя командами.
Примечание. Хотя CMake поддерживает команды, набранные в верхнем, нижнем и смешанном регистре, использование нижнего регистра предпочтительнее и будет использоваться на протяжении всего руководства.
Самый популярный файл CMakeLists.txt любого проекта должен начинаться с указания минимальной версии CMake с помощью команды cmake_minimum_required()
. Она устанавливает параметры политики и гарантирует, что следующие функции CMake выполняются с совместимой версией CMake.
Чтобы начать проект, мы используем команду project()
для установки имени проекта. Этот вызов требуется для каждого проекта и должен вызываться вскоре после cmake_minimum_required()
. Как мы увидим позже, эту команду также можно использовать для указания другой информации на уровне проекта, такой как язык или номер версии.
Наконец, команда add_executable()
указывает CMake создать исполняемый файл, используя указанные файлы исходного кода.
Цель
Узнать, как создать простой проект CMake.
Полезные ресурсы
add_executable()
cmake_minimum_required()
project()
Редактируемые файлы
- CMakeLists.txt
С чего начать
Исходный код tutorial.cxx находится в каталоге Help/guide/tutorial/Step1 и может использоваться для вычисления квадратного корня числа. Этот файл на данном шаге редактировать не нужно.
В том же каталоге находится файл CMakeLists.txt, который вы должны заполнить. Начните с TODO 1 и проработайте до TODO 3.
Сборка и запуск
Как только TODO 1 – TODO 3 будут выполнены, мы будем готовы собрать и запустить наш проект! Сначала запустите исполняемый файл cmake или cmake-gui, чтобы сконфигурировать проект, а затем соберите его с помощью выбранного инструмента сборки.
Например, из командной строки мы можем перейти в каталог Help/guide/tutorial дерева исходного кода CMake и создать каталог сборки:
mkdir Step1_build
Затем перейдите в этот каталог сборки и запустите cmake, чтобы настроить проект и создать файлы нативной системы сборки:
cd Step1_build
cmake ../Step1
Затем вызовите эту систему сборки, чтобы скомпилировать/слинковать проект:
cmake --build .
Наконец, попробуйте использовать только что собранный Tutorial с этими командами:
Tutorial 4294967296
Tutorial 10
Tutorial
Решение
Как упоминалось выше, всё, что нам нужно для запуска, – это трехстрочный файл CMakeLists.txt. Первая строка – используем cmake_minimum_required()
для установки версии CMake следующим образом:
TODO 1
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
Следующим шагом для создания базового проекта является использование команды project()
следующим образом, чтобы установить имя проекта:
TODO 2
CMakeLists.txt
project(Tutorial)
Последней командой для вызова базового проекта является add_executable()
. Мы вызываем его так:
TODO 3
CMakeLists.txt
add_executable(Tutorial tutorial.cxx)
Упражнение 2. Определение стандарта C++
В CMake есть специальные переменные, которые либо создаются за кулисами, либо имеют значение для CMake, если они установлены кодом проекта. Многие из этих переменных начинаются с CMAKE_
. Согласно соглашению об именовании избегайте этого префикса при создании переменных для ваших проектов. Две из этих специальных устанавливаемых пользователем переменных – CMAKE_CXX_STANDARD
и CMAKE_CXX_STANDARD_REQUIRED
. Их можно использовать вместе, чтобы указать стандарт C++, необходимый для сборки проекта.
Цель
Добавить функцию, которая требует C++11.
Полезные ресурсы
CMAKE_CXX_STANDARD
CMAKE_CXX_STANDARD_REQUIRED
set()
Редактируемы файлы
- CMakeLists.txt
- tutorial.cxx
С чего начать
Продолжаем редактировать файлы в каталоге Step1. Начните с TODO 4 и завершите TODO 6.
Во-первых, отредактируйте tutorial.cxx, добавив функцию, для которой требуется C++11. Затем обновите CMakeLists.txt, чтобы он требовал C++11.
Сборка и запуск
Давайте снова соберем наш проект. Поскольку мы уже создали каталог сборки и запустили CMake для упражнения 1, мы можем перейти к этапу сборки:
cd Step1_build
cmake --build .
Теперь мы можем попробовать использовать только что собранное приложение Tutorial с теми же командами, что и раньше:
Tutorial 4294967296
Tutorial 10
Tutorial
Решение
Начнем с добавления в наш проект функций C++11, заменив atof
на std::stod
в tutorial.cxx. Это выглядит следующим образом:
TODO 4
tutorial.cxx
const double inputValue = std::stod(argv[1]);
Чтобы выполнить TODO 5, просто удалите #include <cstdlib>
.
Нам нужно будет явно указать в коде CMake, что он должен использовать правильные флаги. Один из способов включить поддержку определенного стандарта C++ в CMake – использовать переменную CMAKE_CXX_STANDARD
. Для этого руководства установите для переменной CMAKE_CXX_STANDARD
в файле CMakeLists.txt значение 11, а для CMAKE_CXX_STANDARD_REQUIRED
– значение True
. Объявления CMAKE_CXX_STANDARD
обязательно добавьте перед вызовом add_executable()
.
TODO 6
CMakeLists.txt
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
Упражнение 3. Добавление номера версии и сконфигурированного заголовочного файла
Иногда бывает полезно, чтобы переменная, определенная в файле CMakelists.txt
, также была доступна в исходном коде. В этом случае мы хотели бы распечатать версию проекта.
Один из способов добиться этого – использовать сконфигурированный заголовочный файл. Мы создаем входной файл с одной или несколькими переменными для замены. Эти переменные имеют специальный синтаксис, похожий на @VAR@
. Затем мы используем команду configure_file()
, чтобы скопировать входной файл в заданный выходной файл и заменить эти переменные текущим значением VAR
в файле CMakelists.txt.
Хотя мы могли бы редактировать версию непосредственно в исходном коде, использование этой функции предпочтительнее, поскольку она создает единый источник и позволяет избежать дублирования.
Цель
Определить и сообщить номер версии проекта.
Полезные ресурсы
<PROJECT-NAME>_VERSION_MAJOR
<PROJECT-NAME>_VERSION_MINOR
configure_file()
target_include_directories()
Редактируемые файлы
- CMakeLists.txt
- tutorial.cxx
С чего начать
Продолжаем редактировать файлы из Step1. Начните с TODO 7 и завершите TODO 12. В этом упражнении мы начнем с добавления номера версии проекта в CMakeLists.txt. В том же файле используйте configure_file()
, чтобы скопировать заданный входной файл в выходной файл и заменить некоторые значения переменных в содержимом входного файла.
Затем создайте входной заголовочный файл TutorialConfig.h.in, определяющий номера версий, которые будут принимать переменные, переданные из configure_file()
.
Наконец, обновите tutorial.cxx, чтобы распечатать номер его версии.
Сборка и запуск
Давайте снова соберем наш проект. Как и раньше, мы уже создали каталог сборки и запустили CMake, поэтому можем перейти к этапу сборки:
cd Step1_build
cmake --build .
Убедитесь, что теперь при запуске исполняемого файла без каких-либо аргументов выводится номер версии.
Решение
В этом упражнении мы улучшаем наш исполняемый файл с помощью вывода номера версии. Хотя мы могли бы сделать это исключительно в исходном коде, использование CMakeLists.txt позволяет нам поддерживать единый источник данных для номера версии.
Сначала мы модифицируем файл CMakeLists.txt, чтобы использовать команду project()
для установки имени проекта и номера версии. Когда вызывается команда project()
, CMake за кулисами определяет Tutorial_VERSION_MAJOR
и Tutorial_VERSION_MINOR
.
TODO 7
CMakeLists.txt
project(Tutorial VERSION 1.0)
Затем мы используем configure_file()
для копирования входного файла с заменой переменных, указанных CMake:
TODO 8
CMakeLists.txt
configure_file(TutorialConfig.h.in TutorialConfig.h)
Поскольку сконфигурированный файл будет записан в бинарный каталог проекта, мы должны добавить этот каталог в список путей для поиска включаемых файлов.
Примечание. В этом руководстве мы будем ссылаться на сборку проекта и каталог бинарных файлов проекта как синонимы. Они одинаковы и не означают ссылку на каталог bin/.
Используем target_include_directories()
, чтобы указать, где исполняемая цель должна искать включаемые файлы.
TODO 9
CMakeLists.txt
target_include_directories(Tutorial PUBLIC
"${PROJECT_BINARY_DIR}"
)
TutorialConfig.h.in – это входной заголовочный файл, который необходимо сконфигурировать. Когда configure_file()
вызывается из нашего CMakeLists.txt, значения для @Tutorial_VERSION_MAJOR@
и @Tutorial_VERSION_MINOR@
будут заменены в TutorialConfig.h соответствующими номерами версий из проекта.
TODO 10
TutorialConfig.h.in
// сконфигурированные параметры и настройки для Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
Затем нам нужно изменить tutorial.cxx, включив в него сконфигурированный заголовочный файл TutorialConfig.h.
TODO 11
tutorial.cxx
#include "TutorialConfig.h"
Наконец, мы распечатываем имя исполняемого файла и номер версии, обновляя tutorial.cxx следующим образом:
TODO 12
tutorial.cxx
if (argc < 2) {
// сообщить версию
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}
Теги
C++ / CppCMakeАвтоматизация сборкиПрограммированиеСистема сборки
CMake Crash Course — Basics and Scripting
Author: methylDragon
Contains a syntax reference for CMake. We’ll be going through some concepts, the CLI, and scripting with CMake!
Pre-Requisites
Assumed knowledge
- Have a rudimentary understanding of C/C++
- Since CMake is used to build C/C++ projects!
- Understood the linkage and build concepts from this tutorial
- The tutorial is written with Linux users in mind, specifically Ubuntu
- But the scripting section should apply in general
- Recommended: Check out this 15 minute tutorial to familiarise yourself with CMake!
Table Of Contents
- Introduction
- CMake Concepts and Command Line
2.1 Installation
2.2 CMakeLists.txt
2.3 CMake Build Order
2.4 Project Structure
2.5 Invoking CMake
2.6 Configuring CMake
2.7 Ordering - CMake Scripting
3.1 Code Style
3.2 Minimal CMakeLists.txt
3.3 Comments
3.4 Printing
3.5 Variables
3.6 Variable Scope and Directories
3.7 User Input: Cache Variables
3.8 User Input: Options
3.9 Prefixes
3.10 Conditionals
3.11 Loops
3.12 Functions
3.13 Macros
1. Introduction
CMake is Cross-platform Make, where Make
is the utility for building programs from source code.
CMake is an extensible, open-source system that manages the build process in an operating system and compiler-independent manner. Unlike many cross-platform systems, CMake is designed to be used in conjunction with the native build environment. Simple configuration files placed in each source directory (called CMakeLists.txt files) are used to generate standard build files (e.g., makefiles on Unix and projects/workspaces in Windows MSVC) which are used in the usual way.
In other words, CMake is used to generate files that are used with Make to build a project. It’s like a meta-build system. (Though, actually, it can be used with a lot more build pipelines like Visual Studio, XCode, and more!)
Additionally, CMake is more generally used nowadays due to its convenience and simplicity (relative to pure Make
), and so, it is far more useful to learn as a build tool. This is especially true since anything you can do in make
, you can do in CMake.
When comparing CMake with Make, there are several advantages of using CMake:
- Cross platform discovery of system libraries.
- Automatic discovery and configuration of the toolchain.
- Easier to compile your files into a shared library in a platform agnostic way, and in general easier to use than make.
Also note, this tutorial will not serve to be an exhaustive guide on everything CMake, but is designed to be just enough to get you up to speed and able to work somewhat effectively on projects.
Additionally, since CMake is mostly used with C or C++, this tutorial will focus on use of CMake with those languages.
In this section, we’ll go through some concepts, the command line interface, and scripting basics.
2. CMake Concepts and Command Line
Before we can even begin trying to code with CMake, we need to know what it actually does, and how to use it from the command line. So let’s take a look!
2.1 Installation
go to top
Wow! So easy! If this doesn’t work, check your software repositories, or seek out a PPA repository.
2.2 CMakeLists.txt
go to top
CMake is invoked by using a CMakeLists.txt
file placed in the root directory of a project (though you can nest projects too!) This file tells CMake what to do and configures the final makefile
that is produced, which will then be subsequently used with Make
to build your project.
2.3 CMake Build Order
go to top
Image Source
CMake first configures itself by checking the CMakeLists.txt
file. The configuration should specify the targets for build, their linkages, as well as any build properties or configurations, as well as run any configuration time logic.
Then, it runs that configuration through a generation step to produce a Makefile or some other build pipeline that can be used to actually build your targets.
2.4 Project Structure
go to top
There’s no standardised way to structure a C++ project, but here is a generally acceptable project structure.
This is especially important since CMake will generate a whole bunch of files where the cmake
command is invoked. And no one wants generated files clogging up their source directories.
my_project
- src/
- include/
- build/
- CMakeLists.txt
There are also a couple of additional folder directories that you can put in, and a couple of other files that you can place in your project directory. Here’s a full description of them!
Also, do note that you can nest projects!
Folders
Name | Description |
---|---|
src/ |
Stores source files and private headers |
include/ |
Stores public headers for using the project’s public API |
build/ |
Invoke CMake from within this folder for neatness’ sake, so all CMake-generated files go in here |
extern/ |
External projects go here. Cool thing is that these projects can have their own CMakeLists.txt and you can invoke those files in the parent project’s CMakeLists.txt |
doc/ |
Store documentation |
test/ |
Stores test scripts |
data/ |
Stores files that are used by the executables |
Files
Name | |
---|---|
CMakeLists.txt |
Input to CMake build system for building software packages |
README.md |
README for the project. Check out how to write a good one! |
LICENSE |
License for your code |
2.5 Invoking CMake
go to top
We’ll assume your project follows the structure detailed in the previous section, and that it already has a preconfigured CMakeLists.txt
. As such, we’ll build our project in the build/
directory.
# So, starting in the project directory... cd build cmake .. make
So we:
- Move into the
build/
directory - Invoke CMake on the
CMakeLists.txt
in the project directory to generate the makefile - And call Make to build the project based off of the generated
Makefile
2.6 Configuring CMake
go to top
You can also configure CMake further with command line arguments though! I can’t go through all of them, but here are a couple of handy ones.
Specify a non-default generator
The default pipeline that is created when CMake is invoked are makefiles. But you can specify a custom one to use!
Just use the -G
flag!
cmake --help # See all available generators cmake -G "GENERATOR_NAME_HERE" cmake -G "Ninja" # Example
Set Variable (Example: Change Build Type)
Use the -D
flag!
So for example, to change the build type,
cmake -DCMAKE_BUILD_TYPE=Debug # Build unoptimised code with debug flags cmake -DCMAKE_BUILD_TYPE=MinSizeRel # Build optimised code, but at minimal size cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo # Build optimised code with debug flags cmake -DCMAKE_BUILD_TYPE=Release # Build optimised code with no debug flags cmake -D CMAKE_BUILD_TYPE=Release # This also works!
Additionally, do note that this is actually the best practice for invoking CMake
View Cached Variables
So you can see what variables were used during the latest invocation of CMake
Setting Build Directory
So, actually, you don’t have to call CMake from inside the build folder because you can actually just specify the build folder to build into using a couple of flags.
So your usual invocation of
Becomes
2.7 Ordering
go to top
You should think of CMake as a kind of scripting language that eventually produces files that are used to command the build of a project. (Because it actually IS a scripting language.)
As such, it is important to keep in mind the fact that the order of calls in the CMakeLists file matters.
3. CMake Scripting
Now that we have a rough understanding of how to use CMake. Let’s look at some basic programming constructs within CMake!
We’ll cover this before the actual build configuration part of CMake because it helps give a good background understanding moving forward, especially once variables and conditions start getting thrown all around.
So do be patient!
3.1 Code Style
go to top
If you’ve seen CMakeLists.txt
files from projects around the web, you’ll notice that there’s a whole mix of styles being used, some with lowercase function names, some with uppercase, and a lot more.
Here’s a very rough code style guide that I like to use, and that is used in this tutorial:
-
Keywords and function names are all lowercase
- Eg.
if
,for
,message
, etc.
- Eg.
-
CMake function and macro arguments are all UPPERCASE
- Eg.
message(STATUS "message body here...)
- Eg.
-
Indentation is in spaces, and each level of indent is 2 spaces
-
You may split function arguments into multiple lines, but do so indented
-
Eg.
# Split message(STATUS 1 2 3 4 5 ) # Not split message(STATUS 1 2 3 4 5)
-
-
And finally, string variables should be wrapped in double-quotes
-
Eg.
# Do this message("Rawr") # Don't do this message(Rawr)
-
3.2 Minimal CMakeLists.txt
go to top
But before we can even begin, if you’d like to test your code, you need to write CMakeLists.txt
files. So let’s just start with an example of how a minimal CMakeLists file that builds code.
The bare minimum CMakeLists that does meaningful work involves three things
- CMake requirements
- Project name
- Build targets
But actually, if you just want to test the basic programming stuff in CMake, you can have an empty
CMakeLists.txt
file with just the statements to test (in most cases!) It won’t build anything, but it’s good enough.But to be safe, at least include the
cmake_minimum_required()
statement. Lots of statements require it.
So suppose our file structure was as such
minimal_example
- build/
- src/
- example.cpp
- CMakeLists.txt
Then the corresponding CMakeLists.txt will look like this
cmake_minimum_required(VERSION 2.6) project(minimal_example) add_executable(Example src/example.cpp)
So we target src/example.cpp for compilation into the Example executable. Very simple!
(In this care the executable is only made of one translation unit. You can add more after the first to link them together)
You can also compile libraries, and link them! So for example, in some other directory…
cmake_minimum_required(VERSION 2.6) project(another_example) add_library(WOW somelib.cpp) target_include_directories(WOW include) # Specifying the header locations add_executable(Example src/example.cpp) target_link_libraries(Example WOW)
As we move on, you’ll notice that more and more stuff will get added, but usually only after the project()
call. But just keep this basic structure in mind.
Also, it is important to notice that in this case, we had to build the
WOW
library before linking it to the executable.This is another example of the importance of the order of statements inside a
CMakeLists.txt
file! So in this case it is necessary to runadd_library()
beforeadd_executable()
.
3.3 Comments
go to top
# This is a comment # It's nothing special
3.4 Printing
go to top
General syntax
message([<mode>] "message to display" ...)
Where the modes are:
Name | Description |
---|---|
(None) | Important Info |
STATUS |
Status Info |
WARNING |
Warning Info |
AUTHOR_WARNING |
Warning for Devs |
SEND_ERROR |
Error, skip generation, but continue processing |
FATAL_ERROR |
Error, stop processing and generation |
DEPRECIATION |
Warning only if CMAKE_ERROR_DEPRECIATED or CMAKE_WARN_DEPRECIATED is enabled |
There are also a couple others in the more recent versions of CMake, but at the time of writing the tutorial those versions don’t come shipped in an easily installable distribution of CMake.
Name | Description |
---|---|
VERBOSE |
Detailed messages enabled by setting CMAKE_VERBOSE_MAKEFILE to ON |
DEBUG |
Debug messages enabled by calling CMake with --debug-output |
Example usage
message("HI!") message(STATUS "This is a status message!") message(WARNING "RAWR. BIG WARNS.") message(FATAL_ERROR "Time to skedaddle out of this compilation script.") message("This message shouldn't be printing")
Nice! Multi-line works within the quotation marks too!
(And of course, the backslash escapes characters.)
message(" ______ < Wow! > ------ \ \ . . . |\-^-/| . /| } O.=.O { |\ /' \ \_ ~ _/ / '\ /' | \-/ ~ \-/ | '\ | | /\\ //\ | | \|\|\/-""-""-\/|/|/ ______/ / '------ ")
Furthermore, subsequent arguments are just concatenated together
3.5 Variables
go to top
# Set variables set(VARIABLE_NAME "value") set(WOW_NUM 10) # Or even as a list! # Lists are just semi-colon delimited strings! set(SOME_LIST "a" "b") set(EQUIVALENT_LIST "a;b") # These are the same! set(NUM_LIST 1 2) set(EQUIVALENT_NUM_LIST 1;2) # You can even append to the lists! set(NUM_LIST ${NUM_LIST} 3 4) # So-so way list(APPEND NUM_LIST 5 6) # Better way # Use variables message("WOW_NUM: ${WOW_NUM}") message("EQUIVALENT_NUM_LIST: ${EQUIVALENT_NUM_LIST}") # And yes, variables can be called even within a string declaration! # Escape the characters if you don't want this to happen # Like so: message("EQUIVALENT_NUM_LIST: ${EQUIVALENT_NUM_LIST}")
Output
Note: Variable names are case sensitive!
Additionally, there are some pre-defined environment or CMake variables that are set automatically! You can access them by using them in much the same way. The full list of environment variables will be put below.
But here’s an example call.
# Example CMake variable call message(${CMAKE_MAJOR_VERSION}) # Environment variable set and get set(ENV{variable_name} 10) message($ENV{variable_name})
3.6 Variable Scope and Directories
go to top
Image Source
Variables are created in the scope they are set in. And scopes are defined by directories and functions.
Directories!?
Yes! Directories! Since each
CMakeLists.txt
file governs its directory, so treat them modularly like that
So just think of C++ scopes, and you should be fine.
Luckily, there is a way to state that a variable is set in its parent scope, instead of its current scope.
# Just use the PARENT_SCOPE argument! set(PARENT_SCOPED_VAR 10 PARENT_SCOPE)
3.7 User Input: Cache Variables
go to top
CMake uses a cache, which is really just a text file called CMakeCache.txt
to remember settings so you don’t have to restate them when running CMake.
Normal variables aren’t placed in the cache unless you explicitly tell CMake to though! Adding a variable as a cache variable also exposes them in the command line. If you don’t set them in the command line, they’ll use their default value, so it’s a good way to create settable variables with default values!
Listing Cache Variables
You can list out all the available settable cache variables from the command line!
Just use these commands:
cmake -L # List all non-advanced cache variables cmake -LA # List all cache varialbes (including advanced ones) cmake -LAH # List all cache variables, and also display help for them
Setting Cache Variables in the Command Line
Same as setting variables in the command line.
cmake -DCACHE_VAR_NAME=rawr
# Or with type hints
cmake -DTYPED_CACHE_VAR_NAME:STRING=raa
Setting Cache Variables
# General call set(<variable> <value>... CACHE <type> <docstring> [FORCE])
The available types are
Name | Description |
---|---|
BOOL |
ON or OFF |
FILEPATH |
Path to a file on disk |
PATH |
Path to a directory on disk |
STRING |
Line of text |
INTERNAL |
Line of text (does not show up inside cmake-gui ) |
Important: Variables will only go into the cache if it doesn’t exist yet in the cache. So command line cache variable setting will override these
set()
calls. Unless, of course, you use theFORCE
argument.Additionally: Normal variables bound to the same name as a cache variable will be deleted when the cache variable is set.
Example usage
# Example call, no force # In this case, 10 will be CACHE_VAR's default value, if it isn't set in the command line set(CACHE_VAR 10 CACHE STRING "Here's some description of the var...") # Example call, force # In this case, FORCED_VAR will overwrite whatever was stated in the command line set(FORCED_VAR 10 CACHE STRING FORCE)
Recall:
You can set variables into the cache like so
# In this case we are setting the variable: VAR_NAME cmake . -DVAR_NAME=10 # You can also do it with an explicit typing! cmake. -DVAR_NAME:STRING=10
3.8 User Input: Options
go to top
These are just boolean cache variables that you can set ON or OFF in the command line.
Listing Options
If you want to use options, you can use the same cache variable interface.
You can list out all the available settable cache variables from the command line!
Just use these commands:
cmake -L # List all non-advanced cache variables cmake -LA # List all cache varialbes (including advanced ones) cmake -LAH # List all cache variables, and also display help for them
Setting Options in the Command Line
Same as setting variables in the command line.
Setting Options
option(OPTION_NAME "SOME_RANDOM_DOCSTRINGS" ON) # OPTION_NAME is default ON # You can also set a dependent option! # This one defaults to ON # If and only if ON_ME_BEING_ON is ON and AND_ON_ME_BEING_OFF is OFF CMAKE_DEPENDENT_OPTION(DEP_OPTION "I'm dependent!!" ON "ON_ME_BEING_ON;NOT AND_ON_ME_BEING_OFF" OFF)
3.9 Prefixes
go to top
You can simulate data types or structures by using prefixes. Like: METHYL_A
, METHYL_B
, METHYL_C
The cool thing is you can resolve variables when within the declaration of the name of a variable! So it’s one very easy way to state prefixes!
set(PREFIX "methyl") set(${PREFIX}_A 1) set(${PREFIX}_B 2) set(${PREFIX}_C 3) # These generate: methyl_A, methyl_B, and methyl_C # Which contain: 1, 2, and 3 respectively
3.10 Conditionals
go to top
if(CONDITION) # Do stuff elseif(CONDITION) # Otherwise, do stuff else() # Or, do this if everything else fails endif()
Cool! You can control program flow depending on whether stuff evaluates TRUE
or FALSE
.
Boolean Constants
Named constants are case-insensitive.
True | False |
---|---|
1 | 0 |
ON |
OFF |
YES |
NO |
TRUE |
FALSE |
Y |
N |
Additionally, the following are evaluated to false:
IGNORE
, NOTFOUND
, an empty string, or strings that end with the suffix «-NOTFOUND»
Example Usage
set(VAR TRUE) if(VAR) message("Wow") else() message("Oh no") endif() # This should message "Wow"
Wait a sec. Why wasn’t VAR encapsulated by ${}?
The
if()
statement is a little weird in this regard, because it takes in both variables and constants! In this case, if we wrapped VAR and passed it in asif(${VAR})
, the if statement evaluates it as the string «TRUE» as opposed to the boolean constantTRUE
.
Logic
You can do logic with the expressions that go into if()
!
Here’s a handy list. But for the full list please check the reference
# Logical Operations if(NOT <expression>) # Negation if(<expression_1> AND <expression_2>) # AND if(<expression_1> OR <expression_2) # OR if( (<expression_1> AND <expression_2>) OR <expression_3> ) # Parentheses # Comparisons if(<expression_1> LESS <expression_2>) # Less than if(<expression_1> GREATER <expression_2>) # Greater than if(<expression_1> EQUAL <expression_2>) # Equals to # Existence Checking if(DEFINED <variable>) # Check if variable is defined if(EXISTS <path>) # Check if path exists # Misc if(<string> MATCHES regex) # Regex matching
3.11 Loops
go to top
For Loop
Notice that in this case, loop_var is each successive element in the list passed into the for loop.
cmake_minimum_required(VERSION 3.10) set(LIST_VAR 1 2 3) foreach(loop_var ${LIST_VAR}) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
You can also pass in individual elements, of course! So, these two code snippets have equivalent outputs
cmake_minimum_required(VERSION 3.10) foreach(loop_var 1 2 3) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
There’s also alternate syntax, like so
cmake_minimum_required(VERSION 3.10) set(LIST_VAR 1 2 3) foreach(loop_var IN LISTS LIST_VAR) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
For Loop over a Range
cmake_minimum_required(VERSION 3.10) foreach(loop_var RANGE 10) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
Or to have a specified start top and a step:
cmake_minimum_required(VERSION 3.10) foreach(loop_var RANGE 10 20 2) # Start, stop, step message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
While Loop
The while loop conditions are resolved in the same way the if()
command resolves conditions.
while(<condition>) # do stuff endwhile()
3.12 Functions
go to top
Define your own functions for convenience!
The signature is function(<NEW_FUNCTION_NAME> [ARG_1 ARG_2 ARG_3...])
cmake_minimum_required(VERSION 3.10) function(echo_stuff num_argument) foreach(i RANGE ${num_argument}) message("ECHOING: ${i}") endforeach() endfunction() echo_stuff(10)
Also note that any variables set()
within a function is local to its function’s scope, and cannot be accessed outside of the function.
3.13 Macros
go to top
Macros are just like functions, except instead of being called normally, when macros are called, they inline the contents of the macro.
As such, while functions run in their own scope, macros don’t! (Since the variables are inlined directly.)
cmake_minimum_required(VERSION 3.10) macro(echo_stuff num_argument) set(n ${num_argument}) foreach(i RANGE ${num_argument}) message("ECHOING: ${i}") endforeach() endmacro() echo_stuff(10) message(${n}) # This is legal! Since macros constrain no scopes
. .
. |-^-/| .
/| } O.=.O { |
CMake Crash Course — Basics and Scripting
Author: methylDragon
Contains a syntax reference for CMake. We’ll be going through some concepts, the CLI, and scripting with CMake!
Pre-Requisites
Assumed knowledge
- Have a rudimentary understanding of C/C++
- Since CMake is used to build C/C++ projects!
- Understood the linkage and build concepts from this tutorial
- The tutorial is written with Linux users in mind, specifically Ubuntu
- But the scripting section should apply in general
- Recommended: Check out this 15 minute tutorial to familiarise yourself with CMake!
Table Of Contents
- Introduction
- CMake Concepts and Command Line
2.1 Installation
2.2 CMakeLists.txt
2.3 CMake Build Order
2.4 Project Structure
2.5 Invoking CMake
2.6 Configuring CMake
2.7 Ordering - CMake Scripting
3.1 Code Style
3.2 Minimal CMakeLists.txt
3.3 Comments
3.4 Printing
3.5 Variables
3.6 Variable Scope and Directories
3.7 User Input: Cache Variables
3.8 User Input: Options
3.9 Prefixes
3.10 Conditionals
3.11 Loops
3.12 Functions
3.13 Macros
1. Introduction
CMake is Cross-platform Make, where Make
is the utility for building programs from source code.
CMake is an extensible, open-source system that manages the build process in an operating system and compiler-independent manner. Unlike many cross-platform systems, CMake is designed to be used in conjunction with the native build environment. Simple configuration files placed in each source directory (called CMakeLists.txt files) are used to generate standard build files (e.g., makefiles on Unix and projects/workspaces in Windows MSVC) which are used in the usual way.
In other words, CMake is used to generate files that are used with Make to build a project. It’s like a meta-build system. (Though, actually, it can be used with a lot more build pipelines like Visual Studio, XCode, and more!)
Additionally, CMake is more generally used nowadays due to its convenience and simplicity (relative to pure Make
), and so, it is far more useful to learn as a build tool. This is especially true since anything you can do in make
, you can do in CMake.
When comparing CMake with Make, there are several advantages of using CMake:
- Cross platform discovery of system libraries.
- Automatic discovery and configuration of the toolchain.
- Easier to compile your files into a shared library in a platform agnostic way, and in general easier to use than make.
Also note, this tutorial will not serve to be an exhaustive guide on everything CMake, but is designed to be just enough to get you up to speed and able to work somewhat effectively on projects.
Additionally, since CMake is mostly used with C or C++, this tutorial will focus on use of CMake with those languages.
In this section, we’ll go through some concepts, the command line interface, and scripting basics.
2. CMake Concepts and Command Line
Before we can even begin trying to code with CMake, we need to know what it actually does, and how to use it from the command line. So let’s take a look!
2.1 Installation
go to top
Wow! So easy! If this doesn’t work, check your software repositories, or seek out a PPA repository.
2.2 CMakeLists.txt
go to top
CMake is invoked by using a CMakeLists.txt
file placed in the root directory of a project (though you can nest projects too!) This file tells CMake what to do and configures the final makefile
that is produced, which will then be subsequently used with Make
to build your project.
2.3 CMake Build Order
go to top
Image Source
CMake first configures itself by checking the CMakeLists.txt
file. The configuration should specify the targets for build, their linkages, as well as any build properties or configurations, as well as run any configuration time logic.
Then, it runs that configuration through a generation step to produce a Makefile or some other build pipeline that can be used to actually build your targets.
2.4 Project Structure
go to top
There’s no standardised way to structure a C++ project, but here is a generally acceptable project structure.
This is especially important since CMake will generate a whole bunch of files where the cmake
command is invoked. And no one wants generated files clogging up their source directories.
my_project
- src/
- include/
- build/
- CMakeLists.txt
There are also a couple of additional folder directories that you can put in, and a couple of other files that you can place in your project directory. Here’s a full description of them!
Also, do note that you can nest projects!
Folders
Name | Description |
---|---|
src/ |
Stores source files and private headers |
include/ |
Stores public headers for using the project’s public API |
build/ |
Invoke CMake from within this folder for neatness’ sake, so all CMake-generated files go in here |
extern/ |
External projects go here. Cool thing is that these projects can have their own CMakeLists.txt and you can invoke those files in the parent project’s CMakeLists.txt |
doc/ |
Store documentation |
test/ |
Stores test scripts |
data/ |
Stores files that are used by the executables |
Files
Name | |
---|---|
CMakeLists.txt |
Input to CMake build system for building software packages |
README.md |
README for the project. Check out how to write a good one! |
LICENSE |
License for your code |
2.5 Invoking CMake
go to top
We’ll assume your project follows the structure detailed in the previous section, and that it already has a preconfigured CMakeLists.txt
. As such, we’ll build our project in the build/
directory.
# So, starting in the project directory... cd build cmake .. make
So we:
- Move into the
build/
directory - Invoke CMake on the
CMakeLists.txt
in the project directory to generate the makefile - And call Make to build the project based off of the generated
Makefile
2.6 Configuring CMake
go to top
You can also configure CMake further with command line arguments though! I can’t go through all of them, but here are a couple of handy ones.
Specify a non-default generator
The default pipeline that is created when CMake is invoked are makefiles. But you can specify a custom one to use!
Just use the -G
flag!
cmake --help # See all available generators cmake -G "GENERATOR_NAME_HERE" cmake -G "Ninja" # Example
Set Variable (Example: Change Build Type)
Use the -D
flag!
So for example, to change the build type,
cmake -DCMAKE_BUILD_TYPE=Debug # Build unoptimised code with debug flags cmake -DCMAKE_BUILD_TYPE=MinSizeRel # Build optimised code, but at minimal size cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo # Build optimised code with debug flags cmake -DCMAKE_BUILD_TYPE=Release # Build optimised code with no debug flags cmake -D CMAKE_BUILD_TYPE=Release # This also works!
Additionally, do note that this is actually the best practice for invoking CMake
View Cached Variables
So you can see what variables were used during the latest invocation of CMake
Setting Build Directory
So, actually, you don’t have to call CMake from inside the build folder because you can actually just specify the build folder to build into using a couple of flags.
So your usual invocation of
Becomes
2.7 Ordering
go to top
You should think of CMake as a kind of scripting language that eventually produces files that are used to command the build of a project. (Because it actually IS a scripting language.)
As such, it is important to keep in mind the fact that the order of calls in the CMakeLists file matters.
3. CMake Scripting
Now that we have a rough understanding of how to use CMake. Let’s look at some basic programming constructs within CMake!
We’ll cover this before the actual build configuration part of CMake because it helps give a good background understanding moving forward, especially once variables and conditions start getting thrown all around.
So do be patient!
3.1 Code Style
go to top
If you’ve seen CMakeLists.txt
files from projects around the web, you’ll notice that there’s a whole mix of styles being used, some with lowercase function names, some with uppercase, and a lot more.
Here’s a very rough code style guide that I like to use, and that is used in this tutorial:
-
Keywords and function names are all lowercase
- Eg.
if
,for
,message
, etc.
- Eg.
-
CMake function and macro arguments are all UPPERCASE
- Eg.
message(STATUS "message body here...)
- Eg.
-
Indentation is in spaces, and each level of indent is 2 spaces
-
You may split function arguments into multiple lines, but do so indented
-
Eg.
# Split message(STATUS 1 2 3 4 5 ) # Not split message(STATUS 1 2 3 4 5)
-
-
And finally, string variables should be wrapped in double-quotes
-
Eg.
# Do this message("Rawr") # Don't do this message(Rawr)
-
3.2 Minimal CMakeLists.txt
go to top
But before we can even begin, if you’d like to test your code, you need to write CMakeLists.txt
files. So let’s just start with an example of how a minimal CMakeLists file that builds code.
The bare minimum CMakeLists that does meaningful work involves three things
- CMake requirements
- Project name
- Build targets
But actually, if you just want to test the basic programming stuff in CMake, you can have an empty
CMakeLists.txt
file with just the statements to test (in most cases!) It won’t build anything, but it’s good enough.But to be safe, at least include the
cmake_minimum_required()
statement. Lots of statements require it.
So suppose our file structure was as such
minimal_example
- build/
- src/
- example.cpp
- CMakeLists.txt
Then the corresponding CMakeLists.txt will look like this
cmake_minimum_required(VERSION 2.6) project(minimal_example) add_executable(Example src/example.cpp)
So we target src/example.cpp for compilation into the Example executable. Very simple!
(In this care the executable is only made of one translation unit. You can add more after the first to link them together)
You can also compile libraries, and link them! So for example, in some other directory…
cmake_minimum_required(VERSION 2.6) project(another_example) add_library(WOW somelib.cpp) target_include_directories(WOW include) # Specifying the header locations add_executable(Example src/example.cpp) target_link_libraries(Example WOW)
As we move on, you’ll notice that more and more stuff will get added, but usually only after the project()
call. But just keep this basic structure in mind.
Also, it is important to notice that in this case, we had to build the
WOW
library before linking it to the executable.This is another example of the importance of the order of statements inside a
CMakeLists.txt
file! So in this case it is necessary to runadd_library()
beforeadd_executable()
.
3.3 Comments
go to top
# This is a comment # It's nothing special
3.4 Printing
go to top
General syntax
message([<mode>] "message to display" ...)
Where the modes are:
Name | Description |
---|---|
(None) | Important Info |
STATUS |
Status Info |
WARNING |
Warning Info |
AUTHOR_WARNING |
Warning for Devs |
SEND_ERROR |
Error, skip generation, but continue processing |
FATAL_ERROR |
Error, stop processing and generation |
DEPRECIATION |
Warning only if CMAKE_ERROR_DEPRECIATED or CMAKE_WARN_DEPRECIATED is enabled |
There are also a couple others in the more recent versions of CMake, but at the time of writing the tutorial those versions don’t come shipped in an easily installable distribution of CMake.
Name | Description |
---|---|
VERBOSE |
Detailed messages enabled by setting CMAKE_VERBOSE_MAKEFILE to ON |
DEBUG |
Debug messages enabled by calling CMake with --debug-output |
Example usage
message("HI!") message(STATUS "This is a status message!") message(WARNING "RAWR. BIG WARNS.") message(FATAL_ERROR "Time to skedaddle out of this compilation script.") message("This message shouldn't be printing")
Nice! Multi-line works within the quotation marks too!
(And of course, the backslash escapes characters.)
message(" ______ < Wow! > ------ \ \ . . . |\-^-/| . /| } O.=.O { |\ /' \ \_ ~ _/ / '\ /' | \-/ ~ \-/ | '\ | | /\\ //\ | | \|\|\/-""-""-\/|/|/ ______/ / '------ ")
Furthermore, subsequent arguments are just concatenated together
3.5 Variables
go to top
# Set variables set(VARIABLE_NAME "value") set(WOW_NUM 10) # Or even as a list! # Lists are just semi-colon delimited strings! set(SOME_LIST "a" "b") set(EQUIVALENT_LIST "a;b") # These are the same! set(NUM_LIST 1 2) set(EQUIVALENT_NUM_LIST 1;2) # You can even append to the lists! set(NUM_LIST ${NUM_LIST} 3 4) # So-so way list(APPEND NUM_LIST 5 6) # Better way # Use variables message("WOW_NUM: ${WOW_NUM}") message("EQUIVALENT_NUM_LIST: ${EQUIVALENT_NUM_LIST}") # And yes, variables can be called even within a string declaration! # Escape the characters if you don't want this to happen # Like so: message("EQUIVALENT_NUM_LIST: ${EQUIVALENT_NUM_LIST}")
Output
Note: Variable names are case sensitive!
Additionally, there are some pre-defined environment or CMake variables that are set automatically! You can access them by using them in much the same way. The full list of environment variables will be put below.
But here’s an example call.
# Example CMake variable call message(${CMAKE_MAJOR_VERSION}) # Environment variable set and get set(ENV{variable_name} 10) message($ENV{variable_name})
3.6 Variable Scope and Directories
go to top
Image Source
Variables are created in the scope they are set in. And scopes are defined by directories and functions.
Directories!?
Yes! Directories! Since each
CMakeLists.txt
file governs its directory, so treat them modularly like that
So just think of C++ scopes, and you should be fine.
Luckily, there is a way to state that a variable is set in its parent scope, instead of its current scope.
# Just use the PARENT_SCOPE argument! set(PARENT_SCOPED_VAR 10 PARENT_SCOPE)
3.7 User Input: Cache Variables
go to top
CMake uses a cache, which is really just a text file called CMakeCache.txt
to remember settings so you don’t have to restate them when running CMake.
Normal variables aren’t placed in the cache unless you explicitly tell CMake to though! Adding a variable as a cache variable also exposes them in the command line. If you don’t set them in the command line, they’ll use their default value, so it’s a good way to create settable variables with default values!
Listing Cache Variables
You can list out all the available settable cache variables from the command line!
Just use these commands:
cmake -L # List all non-advanced cache variables cmake -LA # List all cache varialbes (including advanced ones) cmake -LAH # List all cache variables, and also display help for them
Setting Cache Variables in the Command Line
Same as setting variables in the command line.
cmake -DCACHE_VAR_NAME=rawr
# Or with type hints
cmake -DTYPED_CACHE_VAR_NAME:STRING=raa
Setting Cache Variables
# General call set(<variable> <value>... CACHE <type> <docstring> [FORCE])
The available types are
Name | Description |
---|---|
BOOL |
ON or OFF |
FILEPATH |
Path to a file on disk |
PATH |
Path to a directory on disk |
STRING |
Line of text |
INTERNAL |
Line of text (does not show up inside cmake-gui ) |
Important: Variables will only go into the cache if it doesn’t exist yet in the cache. So command line cache variable setting will override these
set()
calls. Unless, of course, you use theFORCE
argument.Additionally: Normal variables bound to the same name as a cache variable will be deleted when the cache variable is set.
Example usage
# Example call, no force # In this case, 10 will be CACHE_VAR's default value, if it isn't set in the command line set(CACHE_VAR 10 CACHE STRING "Here's some description of the var...") # Example call, force # In this case, FORCED_VAR will overwrite whatever was stated in the command line set(FORCED_VAR 10 CACHE STRING FORCE)
Recall:
You can set variables into the cache like so
# In this case we are setting the variable: VAR_NAME cmake . -DVAR_NAME=10 # You can also do it with an explicit typing! cmake. -DVAR_NAME:STRING=10
3.8 User Input: Options
go to top
These are just boolean cache variables that you can set ON or OFF in the command line.
Listing Options
If you want to use options, you can use the same cache variable interface.
You can list out all the available settable cache variables from the command line!
Just use these commands:
cmake -L # List all non-advanced cache variables cmake -LA # List all cache varialbes (including advanced ones) cmake -LAH # List all cache variables, and also display help for them
Setting Options in the Command Line
Same as setting variables in the command line.
Setting Options
option(OPTION_NAME "SOME_RANDOM_DOCSTRINGS" ON) # OPTION_NAME is default ON # You can also set a dependent option! # This one defaults to ON # If and only if ON_ME_BEING_ON is ON and AND_ON_ME_BEING_OFF is OFF CMAKE_DEPENDENT_OPTION(DEP_OPTION "I'm dependent!!" ON "ON_ME_BEING_ON;NOT AND_ON_ME_BEING_OFF" OFF)
3.9 Prefixes
go to top
You can simulate data types or structures by using prefixes. Like: METHYL_A
, METHYL_B
, METHYL_C
The cool thing is you can resolve variables when within the declaration of the name of a variable! So it’s one very easy way to state prefixes!
set(PREFIX "methyl") set(${PREFIX}_A 1) set(${PREFIX}_B 2) set(${PREFIX}_C 3) # These generate: methyl_A, methyl_B, and methyl_C # Which contain: 1, 2, and 3 respectively
3.10 Conditionals
go to top
if(CONDITION) # Do stuff elseif(CONDITION) # Otherwise, do stuff else() # Or, do this if everything else fails endif()
Cool! You can control program flow depending on whether stuff evaluates TRUE
or FALSE
.
Boolean Constants
Named constants are case-insensitive.
True | False |
---|---|
1 | 0 |
ON |
OFF |
YES |
NO |
TRUE |
FALSE |
Y |
N |
Additionally, the following are evaluated to false:
IGNORE
, NOTFOUND
, an empty string, or strings that end with the suffix «-NOTFOUND»
Example Usage
set(VAR TRUE) if(VAR) message("Wow") else() message("Oh no") endif() # This should message "Wow"
Wait a sec. Why wasn’t VAR encapsulated by ${}?
The
if()
statement is a little weird in this regard, because it takes in both variables and constants! In this case, if we wrapped VAR and passed it in asif(${VAR})
, the if statement evaluates it as the string «TRUE» as opposed to the boolean constantTRUE
.
Logic
You can do logic with the expressions that go into if()
!
Here’s a handy list. But for the full list please check the reference
# Logical Operations if(NOT <expression>) # Negation if(<expression_1> AND <expression_2>) # AND if(<expression_1> OR <expression_2) # OR if( (<expression_1> AND <expression_2>) OR <expression_3> ) # Parentheses # Comparisons if(<expression_1> LESS <expression_2>) # Less than if(<expression_1> GREATER <expression_2>) # Greater than if(<expression_1> EQUAL <expression_2>) # Equals to # Existence Checking if(DEFINED <variable>) # Check if variable is defined if(EXISTS <path>) # Check if path exists # Misc if(<string> MATCHES regex) # Regex matching
3.11 Loops
go to top
For Loop
Notice that in this case, loop_var is each successive element in the list passed into the for loop.
cmake_minimum_required(VERSION 3.10) set(LIST_VAR 1 2 3) foreach(loop_var ${LIST_VAR}) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
You can also pass in individual elements, of course! So, these two code snippets have equivalent outputs
cmake_minimum_required(VERSION 3.10) foreach(loop_var 1 2 3) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
There’s also alternate syntax, like so
cmake_minimum_required(VERSION 3.10) set(LIST_VAR 1 2 3) foreach(loop_var IN LISTS LIST_VAR) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
For Loop over a Range
cmake_minimum_required(VERSION 3.10) foreach(loop_var RANGE 10) message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
Or to have a specified start top and a step:
cmake_minimum_required(VERSION 3.10) foreach(loop_var RANGE 10 20 2) # Start, stop, step message("CURRENT_LIST_ELEMENT: ${loop_var}") endforeach()
While Loop
The while loop conditions are resolved in the same way the if()
command resolves conditions.
while(<condition>) # do stuff endwhile()
3.12 Functions
go to top
Define your own functions for convenience!
The signature is function(<NEW_FUNCTION_NAME> [ARG_1 ARG_2 ARG_3...])
cmake_minimum_required(VERSION 3.10) function(echo_stuff num_argument) foreach(i RANGE ${num_argument}) message("ECHOING: ${i}") endforeach() endfunction() echo_stuff(10)
Also note that any variables set()
within a function is local to its function’s scope, and cannot be accessed outside of the function.
3.13 Macros
go to top
Macros are just like functions, except instead of being called normally, when macros are called, they inline the contents of the macro.
As such, while functions run in their own scope, macros don’t! (Since the variables are inlined directly.)
cmake_minimum_required(VERSION 3.10) macro(echo_stuff num_argument) set(n ${num_argument}) foreach(i RANGE ${num_argument}) message("ECHOING: ${i}") endforeach() endmacro() echo_stuff(10) message(${n}) # This is legal! Since macros constrain no scopes
. .
. |-^-/| .
/| } O.=.O { |
Некоторое время назад мы познакомились с Autotools. Несмотря на то, что Autotools до сих пор используется во многих известных проектах с открытым исходным кодом, инструмент этот трудно назвать особо удобным. Кроме того, нормально работает он только в *nix системах, а в каком-нибудь Windows пользоваться Autotools, скажем так, весьма непросто. В общем, Autotools — это легаси, и нормальные программисты в наше время пытаются использовать CMake или, например, SCons. В этой заметке мы познакомимся с CMake.
Говоря простыми словами, CMake — это такая штука, в которой вы описываете проект, а она вам генерирует Makefile’ы в *nix системах, проекты Visual Studio под Windows, файлы конкретных редакторов и IDE, например Sublime Text, Code::Blocks, Eclipse или KDevelop, и так далее. Несмотря на спорный в некоторых моментах синтаксис, в последнее время CMake становится стандартом де-факто в мире C/C++. В частности, CMake используется в LLVM, Qt, MariaDB, Blender, KiCad, GNU Radio и ряде других проектов. Кроме того, в CLion, IDE для C/C++ от компании JetBrains, по умолчанию также создаются проекты, основанные на CMake.
Примечание: В этом контексте вас может заинтересовать заметка Как править в CLion код любых проектов на С++, даже тех, в которых не используется CMake.
Использование CMake в простейшем случае выглядит следующим образом. В корне репозитория создается файл CMakeLists.txt примерно такого содержания:
cmake_minimum_required(VERSION 3.1)
# так пишутся комментарии
project(project_name)
find_library(PTHREAD_LIBRARY pthread)
find_library(PCRE_LIBRARY pcre)
include_directories(include)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED on)
set(CMAKE_CXX_FLAGS «${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror»)
add_executable(main src/Main.cpp src/HttpServer.cpp)
target_link_libraries(main ${PTHREAD_LIBRARY} ${PCRE_LIBRARY})
Хочется надеяться, какая строчка здесь что означает, пояснять не нужно. Затем исходники складываются в каталог src, а заголовочные файлы — в каталог include. Для сборки проекта говорим:
mkdir build
cd build
cmake ..
make
Просто, не правда ли?
Помимо приведенного выше find_library
в CMake есть ряд скриптов для подключения конкретных библиотек. В частности, подключение OpenGL осуществляется как-то так:
find_package(OpenGL REQUIRED)
include_directories(${OPENGL_INCLUDE_DIR})
target_link_libraries(main ${OPENGL_LIBRARY} ${CMAKE_DL_LIBS})
CMake можно указать конкретный тип Makefile’ов, которые вы хотите получить на выходе:
cmake -G «Unix Makefiles» ..
cmake -G «MinGW Makefiles» ..
# для просмотра списка всех доступных генераторов:
cmake -G
В частности, многие программисты для ускорения сборки проектов предпочитают использовать Ninja:
cmake -G Ninja ..
ninja -j1
Выбор между отладочной и релизной сборкой осуществляется так:
cmake -DCMAKE_BUILD_TYPE=Release -G Ninja ..
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -G Ninja ..
cmake -DCMAKE_BUILD_TYPE=MinSizeRel -G Ninja ..
# Debug используется по умолчанию
cmake -DCMAKE_BUILD_TYPE=Debug -G Ninja ..
Вместо запуска напрямую make или ninja можно сказать что-то вроде:
cmake —build . —config Release —target main
Можно выбрать конкретный компилятор для сборки проекта
cmake -DCMAKE_C_COMPILER=`which clang`
-DCMAKE_CXX_COMPILER=`which clang++` -G Ninja ..
… а также указать дополнительные флаги компиляции:
cmake -DCMAKE_C_FLAGS=«-O0 -g» -DCMAKE_CXX_FLAGS=«-O0 -g» ..
Например, для определения степени покрытия кода тестами при помощи lcov нужно сказать что-то вроде:
cmake -DCMAKE_C_FLAGS=«-O0 -g -fprofile-arcs -ftest-coverage»
-DCMAKE_EXE_LINKER_FLAGS=«-lgcov» ..
В мире C/C++ нередко бывает, что сторонние библиотеки, использующие CMake, подключаются к проекту при помощи сабмодулей Git. Подключение таких библиотек к проекту осуществляется довольно просто:
cmake_minimum_required(VERSION 2.8)
project(c-algorithms-examples)
include_directories(deps/algorithms/include)
add_subdirectory(deps/algorithms/src)
add_executable(rbtree_example rbtree_example.c)
target_link_libraries(rbtree_example CAlgorithms)
В свою очередь, у библиотеки файл src/CMakeList.txt должен быть примерно таким:
cmake_minimum_required(VERSION 2.8)
project(c-algorithms)
add_library(CAlgorithms STATIC
struct/ilist.c
struct/rbtree.c
struct/htable.c
common/utils.c
)
Вообще, add_subdirectory может принимать путь до любого каталога, в котором есть файл CMakeLists.txt. Это позволяет разбивать проект на подпроекты даже в рамках одного репозитория. Опять же, в случае с библиотеками это позволяет поместить тесты в отдельный подпроект, который не будет собираться при подключении библиотеки в сторонние проекты.
Например, в корне библиотеки CMakeList.txt может быть таким:
cmake_minimum_required(VERSION 2.8)
project(c-algorithms-root)
enable_testing()
include_directories(include)
add_subdirectory(src)
add_subdirectory(test)
Непосредственно тесты добавляются в проект следующим образом:
cmake_minimum_required(VERSION 2.8)
project(c-algorithms-struct-tests)
set(CMAKE_C_FLAGS «${CMAKE_C_FLAGS} -O0 -g»)
add_executable(test_htable test_htable.c)
target_link_libraries(test_htable CAlgorithms)
add_executable(test_rbtree test_rbtree.c)
target_link_libraries(test_rbtree CAlgorithms)
add_test(test_htable «./test_htable»)
add_test(test_rbtree «./test_rbtree»)
Запуск тестов осуществляется простой командой:
make test
# или, с включением отладочного вывода:
make test ARGS=«-V»
# или, если используете Ninja:
ninja test
… выполненной в каталоге build. Если вас интересует тема написания модульных тестов на C++, она более подробно раскрыта в заметке Тестирование кода на C++ с помощью Google Test.
Если же вы используете какой-нибудь PyTest, просто допишите в CMakeList.txt что-то вроде:
find_package(PythonInterp REQUIRED)
enable_testing()
add_test(NAME python_test
COMMAND py.test —capture=no ${CMAKE_SOURCE_DIR}/tests/run.py)
Вывод тестов пишется в файл Testing/Temporary/LastTest.log. Кстати, подробности о переменных окружения, доступных в CMake, таких, как CMAKE_SOURCE_DIR, можно найти здесь.
Помимо рассмотренных выше возможностей часто можно встретить поддержку сборки проектов с различными опциями. В частности, это используется в Assimp и LLDB. При сборке проекта опции выбираются так:
cmake -DLLDB_DISABLE_CURSES:BOOL=TRUE …
cmake -DASSIMP_BUILD_ASSIMP_TOOLS=OFF …
Опции обычно описывают в документации, но в крайнем случае их можно посмотреть и через curses-интерфейс:
В рамках одного поста, конечно, не представляется возможным рассмотреть все возможности CMake. Однако представленной выше информации вам должно вполне хватить в 90% случаев. Полноценные рабочие примеры использования CMake вы найдете, например, в этом, этом, а также в этом репозиториях на GitHub. Примеры использования опций и условных операторов можно найти в репозиториях уже упомянутых Assimp и LLDB. Ну и, конечно же, массу полезного вы найдете на официальном сайте CMake.
А пользуетесь ли вы CMake и если да, используете ли какие-то его возможности, о которых не было рассказано выше?
Метки: C/C++, Кроссплатформенность.