CMake Cookbook

Что такое CMake?

Утилита для автоматизации сборки программных проектов. Утилита CMake предназначена для генерации файлов для различных систем сборки (GNU make, Visual Studio и других). Поэтому CMake можно назвать мета-системой сборки. Для системы сборки GNU make CMake будет генерировать makefile, а для Visual Studio — файлы решения и проекта (проектов) .sln и .vcxproj. Хорошая статья, объясняющая что такое CMake — The Architecture of Open Source Applications: CMake.

CMakeLists.txt

Чтобы воспользоваться утилитой CMake для генерации файлов проекта, нужно написать скрипт CMakeLists.txt, выполнение которого и есть задача утилиты CMake. Скрипт CMakeLists.txt содержит команды, которые описывают структуру проекта (targets, dependencies, команды и прочее).

Структура скрипта CMakeLists

Скрипт на языке cmake состоит из вызовов функций (и комментариев). Комментарии начинаются с символа hashtag (решетка). Вызов функции состоит из имени функции и списка аргументов (аргументы отделяются друг от друга символами пробела или перевода строки).

# comment 1
foo(arg1 arg2 arg3)

# comment 2
bar(arg1 arg2)

Аргументом функции может быть выражение. Результатом вычисления выражения всегда является строка. Выражение может состоять из текста и ссылок на переменные (см. далее).

Переменные

Переменные в cmake — это штука, у которой есть имя и есть значение. Значение переменной — это всегда строка. Переменную в cmake не нужно объявлять, достаточно присвоить ей значение при помощи функции set, например так:

set(MY_VARIABLE "variable value")

Значение переменной можно очистить (значение становится неопределенным) при помощи функции unset:

unset(MY_VARIABLE)

Имя переменной — это тоже строка (важно, что имя переменной не должно содержать пробелы). Когда мы используем имя переменной как таковое (что бывает редко, обычно — только при задании значения переменной командой set), это выглядит так:

# MY_VARIABLE is the name of a variable
set(MY_VARIABLE "variable value")

message(MY_VARIABLE)
# prints: MY_VARIABLE

message("MY_VARIABLE")
# prints: MY_VARIABLE

Когда мы используем значение переменной (что бывает гораздо чаще), то мы используем следующий синтаксис со знаком доллара и фигурными скобками:

# MY_VARIABLE is the name of a variable
# ${MY_VARIABLE} is the value of that variable
message(${MY_VARIABLE})

Значение переменной можно неоднократно изменять при помощи той же команды set. Вот еще несколько примеров:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

# variable value is a string
set(MY_VARIABLE "Quick brown fox")
message(${MY_VARIABLE})
# prints: Quick brown fox

# variable name is also a string
set("MY_VARIABLE" "jumps over the lazy dog")
message(MY_VARIABLE " is a variable")
# prints: MY_VARIABLE is a variable
message(${MY_VARIABLE})
# prints: jumps over the lazy dog

unset(MY_VARIABLE)
# prints an emty string
message("${MY_VARIABLE}")

# error: message cannot be called with an undefined parameter value
message(${MY_VARIABLE})

Имя переменной — это строка, и эта строка может сама являться результатом вычисления выражения. Ниже показан пример, в котором имя переменной вычисляется как выражение "${VAR1}VARIABLE" и значение этой переменной получается путем «разыменования» этого выражения ${${VAR1}VARIABLE}:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

set(VAR1 "MY_")
message(${VAR1})
# prints: MY_

set("${VAR1}VARIABLE" "quick brown fox")
message(${MY_VARIABLE})
# prints: quick brown fox

message("${VAR1}VARIABLE")
# prints: MY_VARIABLE

message(${${VAR1}VARIABLE})
# prints: quick brown fox

String or List?

Переменные — это всегда строки, однако в некоторых контекстах значение переменной может быть интерпретировано как список. В следующем примере значение переменной MY_VARIABLE будет иногда интерпретироваться как список из трех строк:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

# list
set(MY_VARIABLE value1 value2 value3)
# same as
# set(MY_VARIABLE "value1;value2;value3")

message(${MY_VARIABLE})
# value1value2value3

message("${MY_VARIABLE}")
# value1;value2;value3

foreach(arg MY_VARIABLE)
  message("${arg}")
endforeach()
# MY_VARIABLE

foreach(arg ${MY_VARIABLE})
  message("${arg}")
endforeach()
# value1
# value2
# value3

foreach(arg "${MY_VARIABLE}")
  message("${arg}")
endforeach()
# value1;value2;value3

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

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

# list
set(MY_VARIABLE "value1 value2 value3")

message("--- 1 ---")
message(${MY_VARIABLE})
# value1 value2 value3

message("--- 2 ---")
message("${MY_VARIABLE}")
# value1 value2 value3

message("--- 3 ---")
foreach(arg MY_VARIABLE)
  message("${arg}")
endforeach()
# MY_VARIABLE

message("--- 4 ---")
foreach(arg ${MY_VARIABLE})
  message("${arg}")
endforeach()
# value1 value2 value3

message("--- 5 ---")
foreach(arg "${MY_VARIABLE}")
  message("${arg}")
endforeach()
# value1 value2 value3

Мораль такова:
Строка без кавычек интерпретируется как строка, а не как список, только если в ней нет пробелов.
Строка без кавычек и с пробелами интерпретируется как список.
Строка в кавычках и с пробелами интерпретируется как строка, а не список.

Функции

В примерах скриптов cmake мы можем видеть такого рода вызовы функций:

foo("arg1" "arg2" "arg3")

…или такого рода:

bar(ARG1 "quick"
    ARG2 "brown"
    ARG3 "fox")

…или такого рода:

bas("arg1"
    "arg2"
    ARG3 "quick"
    ARG4 "brown"
    ARG5 "fox")

На что тут надо обратить внимание: параметры функции могут быть обязательными и неименованными («arg1» «arg2» «arg3» в 1-ом примере в функции foo; «arg1» «arg2» в функции bas) или опциональными и именованными (ARG1 ARG2 ARG3 в функции bar; ARG3 ARG4 ARG5 в функции bas). Опциональные (необязательные) параметры могут отсутствовать в вызове функции; они также могут идти в произвольном порядке. Обязательные параметры должны присутствовать и должны идти в строго определенном порядке.

Понять эту систему с обязательными и необязательными параметрами можно, прочитав про команду function и заметку Programming in CMake.

Область видимости переменных

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

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

function(foo)
    set(MY_VAR "hello")
endfunction()

foo()

message("${MY_VAR}")
# prints nothing

Однако функции могут создавать переменные в «вышележащей» области видимости, для чего используется параметр PARENT_SCOPE в функции set:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

function(foo)
    set(MY_VAR "hello" PARENT_SCOPE)
    message("${MY_VAR}") # prints empty string
endfunction()

foo()

message("${MY_VAR}")
# prints: hello

Заметим, что использование PARENT_SCOPE в функции set задает значение переменной в только в «вышележащей» области видимости, но не в локальной области видимости. Чтобы задать переменную в обоих областях видимости, я бы поступил следующим образом:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)

function(foo)
    set(MY_VAR "hello")
    set(MY_VAR ${MY_VAR} PARENT_SCOPE)
    message("${MY_VAR}") # prints: hello
endfunction()

foo()

message("${MY_VAR}")
# prints: hello

Примеры скриптов CMakeLists.txt

Разобравшись с синтаксисом языка скриптов CMake, мы должны лишь понять какие функции и с каким параметрами нам нужно вызывать, чтобы достичь желаемой цели. Можно пройти официальный туториал. Я же приведу несколько примеров с объяснениями.

Сценарий 1.
Проект на языке C++ без зависимостей от сторонних библиотек.
Целей две:
1) построение библиотеки для математических вычислений с векторами;
2) построение исполняемой программы, которая тестирует функции из библиотеки.
Исходный код библиотеки находится в двух файлах: Vector2d.cpp и Matrix2x2.cpp.
Исходный код исполняемой программы находится в одном файле main.cpp.

cmake_minimum_required(VERSION 3.19 FATAL_ERROR)

# specifies the project name (can be used for example as a Visual Studio project name)
project(VMath # PROJECT-NAME
        VERSION 1.0
        DESCRIPTION "Vector Math Library"
        LANGUAGES CXX)

# set default values for configuration variables
if(NOT CONFIGURED_ONCE)
    set(LIB_NAME "VMath" CACHE STRING "Library file name.")
    set(TEST_NAME "test" CACHE STRING "Test executable file name.")
endif()

option(BUILD_SHARED_LIBS "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" ON)

# defines the library file name (target) and its dependencies
add_library("${LIB_NAME}" Vector2d.cpp Matrix2x2.cpp)

# defines the executable file name (target) and its dependencies
add_executable("${TEST_NAME}" main.cpp)
# make the test project dependent on the library project
add_dependencies("${TEST_NAME}" "${LIB_NAME}")
# link test executable to the library
target_link_libraries("${TEST_NAME}" PRIVATE "${LIB_NAME}")

if(BUILD_SHARED_LIBS)
    set_target_properties(${LIB_NAME}  PROPERTIES COMPILE_DEFINITIONS "__DLL_EXPORT")
    set_target_properties(${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "__DLL_IMPORT")
else()
    set_target_properties(${LIB_NAME}  PROPERTIES COMPILE_DEFINITIONS "__STATIC_LIB")
    set_target_properties(${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "__STATIC_LIB")
endif()

# set our executable test project as the sturtup
# project in the Visual Studio solution
set_property(
    DIRECTORY "${PROJECT_SOURCE_DIR}"
    PROPERTY VS_STARTUP_PROJECT "${TEST_NAME}")

set(CONFIGURED_ONCE TRUE CACHE INTERNAL
    "A flag showing that CMake has configured at least once")

cmake_minimum_required — с этой команды всегда начинается скрипт. Задает минимальную версию CMake, необходимую для выполнения данного скрипта.

project — задает имя проекта (обязательный аргумент) и ряд переменных (PROJECT_SOURCE_DIR, PROJECT_BINARY_DIR и др.).

if — осуществляет ветвление. Аргументом является выражение, которое считается истинными, если его вычисленное значение определено и не равно константам 0, OFF, NO, FALSE, N, IGNORE или NOTFOUND.

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

set — устанавливает значение переменной, причем параметр CACHE создает кэшированную переменную — которая сохраняется в файле CMakeCache.txt. Для кэшированной переменной также указывается тип поля ввода — для удобства редактирования в cmake-gui (возможны например типы STRING, BOOL, PATH, FILEPATH).

option — насколько я понимаю, функция option представляет собой обертку над функцией set:

option(BUILD_SHARED_LIBS "Build shared libraries" ON)
# is equal to
set(BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries")

Переменная BUILD_SHARED_LIBS не простая — она неявно влияет на то, в каком виде будет строиться библиотека (см. команду add_library). Если BUILD_SHARED_LIBS=ON, то будет построена динамическая библиотека, а если BUILD_SHARED_LIBS=OFF, то — статическая.

add_library — создает цель (target) — построение библиотеки. Например, для GNU Make это будет означать создание цели, а для Visual Studio — создание проекта внутри решения. В качестве параметров обычно указывается имя цели (имя библиотеки, если угодно) и список путей к файлам исходного кода, из которых она строится. Путь к файлу задается относительно source_dir. Можно не указывать список файлов исходного кода, но тогда они должны быть указаны позже командой target_sources.

add_executable — аналогично add_library, только создает не библиотеку, а исполняемую программу.

add_dependencies — указывает, что одна цель (указанная первым параметром) зависит от списка других целей (все остальные параметры). Т. е. при построении этой одной цели также должны быть построены и цели, от которых она зависит.

target_link_libraries — указывает библиотеки, с которыми надо компоновать (линковать) цель (которая сама является исполняемой программой либо библиотекой). В нашем примере мы указываем, что наше исполняемое приложение нужно скомпоновать с нашей же библиотекой.

set_target_properties — устанавливает некие «свойства», которые влияют на то, как будет строиться цель. В нашем примере мы задаем свойства COMPILE_DEFINITIONS, которые добавляют в командную строку компилятора макроопределения, которые затем будут использоваться в коде для условной компиляции.

set_property — устанавливает свойство… того, что мы укажем (например DIRECTORY, TARGET и пр.). В нашем случае для DIRECTORY мы задаем свойство VS_STARTUP_PROJECT, которое является, так сказать, «платформозависимым», так как оно имеет силу только для Visual Studio и указывает проект, который будет запускаться при запуске отладки в Visual Studio.

Наконец, следующий код создает кэшированную переменную CONFIGURED_ONCE, о которой уже шла речь выше. Тип INTERNAL указывает на то, что переменная не должна отображаться в cmake-gui:

set(CONFIGURED_ONCE TRUE CACHE INTERNAL
    "A flag showing that CMake has configured at least once")

Сценарий 2.
Модифицируем предыдущий пример. Допустим, что мы решили сделать отдельный скрипт CMakeLists для библиотеки, и отдельный — для исполняемого приложения. Все файлы исходного кода библиотеки помещаем в отдельную подпапку VMathLib; наверху остается только файл исходного кода исполняемого приложения main.cpp:

.
├── CMakeLists.txt
├── VMathLib
│   ├── CMakeLists.txt
│   ├── ExportImport.h
│   ├── Matrix2x2.cpp
│   ├── Matrix2x2.h
│   ├── Vector2d.cpp
│   └── Vector2d.h
└── main.cpp

В подпапку VMathLib мы помещаем отдельный файл CMakeLists.txt, который будет отвечать только за построение библиотеки. А верхний файл CMakeLists будет отвечать за построение исполняемого приложения.
Вот нижний файл, отвечающий за построение библиотеки:

cmake_minimum_required(VERSION 3.19 FATAL_ERROR)

# specifies the project name (can be used for example as a Visual Studio project name)
project(VMath VERSION 1.0
              DESCRIPTION "Vector Math Library"
              LANGUAGES CXX)

if(NOT CONFIGURED_ONCE)
    set(LIB_NAME "VMath" CACHE STRING "Library name.")
endif()

# defines the library file name (target) and its dependencies
add_library("${LIB_NAME}" Vector2d.cpp Matrix2x2.cpp)

if(BUILD_SHARED_LIBS)
    set_target_properties(${LIB_NAME} PROPERTIES COMPILE_DEFINITIONS "__DLL_EXPORT")
else()
    set_target_properties(${LIB_NAME} PROPERTIES COMPILE_DEFINITIONS "__STATIC_LIB")
endif()

А вот верхний файл, отвечающий за построение исполняемого приложения:

cmake_minimum_required(VERSION 3.19 FATAL_ERROR)

# specifies the project name (can be used for example as a Visual Studio project name)
project(VMath VERSION 1.0
              DESCRIPTION "Test"
              LANGUAGES CXX)

if(NOT CONFIGURED_ONCE)
    set(TEST_NAME "test" CACHE STRING "Test executable file name.")
endif()

option(BUILD_SHARED_LIBS "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" ON)

# defines the executable file name (target) and its dependencies
add_executable("${TEST_NAME}" main.cpp)

# execute CMakeLists.txt from subdirectory VMathLib
add_subdirectory(VMathLib)
# make the test project dependent on the library project
add_dependencies("${TEST_NAME}" "${LIB_NAME}")
# link test executable to the library
target_link_libraries("${TEST_NAME}" PRIVATE "${LIB_NAME}")
# add include directory with the library header files
target_include_directories("${TEST_NAME}" PRIVATE "${PROJECT_SOURCE_DIR}/VMathLib")

if(BUILD_SHARED_LIBS)
    set_target_properties(${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "__DLL_IMPORT")
else()
    set_target_properties(${TEST_NAME} PROPERTIES COMPILE_DEFINITIONS "__STATIC_LIB")
endif()

# set our executable test project as the sturtup project in the Visual Studio solution
set_property(
    DIRECTORY "${PROJECT_SOURCE_DIR}"
    PROPERTY VS_STARTUP_PROJECT "${TEST_NAME}")

set(CONFIGURED_ONCE TRUE CACHE INTERNAL
    "A flag showing that CMake has configured at least once")

Здесь мы встречаем две новые команды:

add_subdirectory — выполняет скрипт CMakeLists, который находится в папке, указанной в качестве параметра функции. Путь к этой папке может быть абсолютным либо относительным — относительно текущей папки, т. е. той, в которой находится файл CMakeLists.txt, который вызывает функцию.

target_include_directories — добавляет директорию для поиска заголовочных файлов.

<раздел будет дополняться>

Запуск CMake (cmake-gui)

Можно запустить графическую утилиту cmake-gui. Сразу после запуска в окне утилиты нужно указать пути к двум папкам:

  • Where is the source code (далее для краткости буду называть ее source_dir) — папка, в которой находится файл CMakeLists.txt. Не путаем эту папку с папкой, в которой лежит исходный код вашего проекта (например на C++).
  • Where to build the binaries (далее для краткости буду называть ее binary_dir) — папка, в которую CMake поместит сгенерированные файлы проекта (makefile, .sln, .vcxproj и прочее).

После этого нажимаем кнопку Configure. Появляется диалог, в котором выбираем целевую систему сборки (например GNU make или Visual Studio). CMake читает скрипт CMakeLists.txt и пока еще не генерирует файлы проекта, а позволяет пользователю задать значения для переменных, которые объявлены в скрипте. Операция Configure создает в папке binary dir файл CMakeCache.txt, в котором сохранены значения кэшируемых переменных (при последующих запусках CMake значения этих переменных будут восстанавливаться из файла CMakeCache.txt). Также операция Configure скачивает зависимости проекта и сохраняет их в папке binary_dir/_deps.

После конфигурирования переменных нажимаем кнопку Generate. Эта операция генерирует в папке binary_dir файлы проекта (например makefile или .sln и .vcxproj).

Запуск CMake из командной строки

Прежде всего, мы переходим в папку source_dir (папка, в которой лежит файл CMakeLists.txt):

> cd source_dir

Замечу, что наличие в пути к папке source_dir символов кириллицы приводит к проблемам. Наличие пробелов вроде бы не приводит к проблемам.
А еще, если вы хотите генерировать файлы проекта для сред от фирмы Microsoft, т. е. для NMake и Visual Studio, то запускать cmake нужно из Developer Commant Prompt for VS, а не из обычной cmd.exe.

Далее приведем несколько примеров запуска утилиты из командной строки. Заметим, что при запуске утилиты cmake из командной строки не существует разделения на шаги Configure и Generate как в cmake-gui — будет выполнено как конфигурирование, так и генерация за один запуск.

> cmake .

Здесь мы указываем единственный параметр командной строки — путь к папке source_dir. Точка означает «текущая директория». В данном случае мы не указали целевую систему сборки, поэтому она будет выбрана автоматически (например, если вы работаете в Windows, то будет сгенерировано решение Visual Studio). Мы также не указали папку, в которую CMake должна поместить сгенерированные файлы, поэтому они будут помещены в текущую директорию, т. е. в source_dir.

> cmake –G "Unix Makefiles" -S . -B ./make

Ключ -S задает путь к папке source_dir, ключ -G задает целевую систему сборки (Visual Studio, makefiles, Ninja, XCode и т. д.), ключ -B задает путь к папке binary_dir (папка, в которой CMake должна поместить сгенерированные файлы).

> cmake –G "NMake Makefiles" -S . -B ./nmake

Еще один пример с другой целевой системой сборки и папкой binary_dir.

> cmake -G "Visual Studio 15 2017" -S . -B ./vs2017

Еще один пример с другой целевой системой сборки и папкой binary_dir.

> cmake –G "Visual Studio 16 2019" -S . -B ./vs2019

Еще один пример с другой целевой системой сборки и папкой binary_dir.

> cmake –D BUILD_SHARED_LIBS=OFF –D TEST_NAME=tst ^
        –G "Visual Studio 16 2019" ^
        –A Win32 ^
        -S . ^
        -B ./vs2019

Здесь мы при помощи ключа -D указываем значения переменных BUILD_SHARED_LIBS и TEST_NAME. Ключ -A задает целевую архитектуру (например, Win32, x64, ARM, ARM64).

После того, как файлы проекта сгенерированы, можно сразу построить проект (не запуская непосредственно GNU make, Visual Studio и т. д.):

> cmake --build <binary_dir>

Здесь <binary_dir> — это путь к папке, в которой лежат сгенерированные файлы проекта.

Заметим, что во всех файлах, которые генерирует CMake, будь то makefile’ы или проекты Visual Studio, в исполняемых командах встречаются вызовы самой CMake. Таким образом, сгенерированные файлы для системы сборки зависят от CMake. Т. е. вы не сможете, например, сгенерировать файлы для системы сборки и распространять их как «самостоятельный продукт» — они не будут работать на машине, на которой не установлена CMake или на машине, где CMake установлена в папке отличной от той, в которой она установлена у вас. Проще говоря, пользоваться сгенерированными файлами для системы сборки можно только на той машине, на которой они были сгенерированы.

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

Ваш адрес email не будет опубликован.