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


Введение


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


Язык CMake, будучи транслированным в нативный файл сборки (например, Makefile или Ninja), определяет процесс всего управления проектом. В Вашем распоряжении, с функциональной стороны, есть лишь команды, которые могут образовываться в довольно сложные конструкции. С них мы и начнём.


Запуск CMake


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


Команды


Команды в CMake подобны функциям во многих языках программирования. Чтобы вызвать команду, необходимо написать её имя, а затем передать ей обрамлённые в круглые скобки аргументы, отделённые символами пробелов. В приведённом примере команде message передаются шесть аргументов для вывода в консоль:


# Напечатает в консоль "CMake is the most powerful buildsystem!"
message("CMake " "is " "the " "most " "powerful " "buildsystem!")

Аргументы


Аргументы, обрамлённые в двойные кавычки, позволяют внутри себя совершать экранирование и подстановку переменных. Необрамлённые аргументы не позволяют производить подобных вещей и не могут включать в себя символы ()#"\ и пробелы, однако более удобны для использования. Пример:


# Напечатает "Hello, my lovely CMake", один таб и "!":
message("Hello, my lovely CMake\t!")

# Напечатает "Hello,_my_lovely_CMake!" без пробелов:
message(Hello,_my_lovely_CMake!)

Стоит отметить, что аргумент Walk;around;the;forest расширится до списка Walk around the forest, так как любой необрамлённый аргумент автоматически расширяется до списка значений (при условии, что значения изначального аргумента разделены символами точки с запятой), но с обрамлённым в двойные кавычки аргументом такая трансформация не происходит (символы точки с запятой просто исчезают). Об этой особенности упомянули в комментариях.


Комментарии


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


Переменные


Переменные можно определить путём вызова команды set, а удалить вызовом unset. Получить значение переменной можно по конструкции ${VARIABLE}. Если переменная ещё не определена и где-то потребовалось получить её значение, то данная переменная обратится в пустую строку. Пример:


# Определить переменную VARIABLE со значением "Mr. Thomas":
set(VARIABLE "Mr. Thomas")

# Напечает "His name is: Mr. Thomas":
message("His name is: " ${VARIABLE})

# Напечатает "'BINGO' is equal to: []", так как "BINGO" не определена:
message("'BINGO' is equal to: [${BINGO}]")

# Удалить переменную VARIABLE:
unset(VARIABLE)

Опции


CMake поддерживает задание опций, подлежащих модицификациям пользователей. Опции похожи на переменные и задаются командой option, принимающей всего три аргумента: имя переменной, строковое описание переменной и значение переменной по умолчанию (ON или OFF):


# Задать опцию `USE_ANOTHER_LIBRARY` с описанием
# "Do you want to use an another library?" и значением "OFF":
option(USE_ANOTHER_LIBRARY "Do you want to use an another library?" OFF)

Логические выражения


Прежде чем приступать к изучению условных операторов и циклических конструкций, необходимо понимать работу логических выражений. Логические выражения используются при проверки условий и могут принимать одно из двух значений: правда или ложь. Например, выражение 52 LESS 58 обратится в правду, так как 52 < 58. Выражение 88 EQUAL 88 обратится в правду, 63 GREATER 104 обратится в ложь. Сравнивать можно не только числа, но и строки, версии, файлы, принадлежность к списку и регулярные выражения. Полный список логических выражений можно посмотреть тут.


Условные операторы


Условные операторы в CMake работают в точности как в других языках программирования. В данном примере сработает лишь первый условный оператор, который проверяет, что 5 > 1. Второе и третье условия ложны, так как 5 не может быть меньше или равняться одному. Блоки команд elseif и else необязательны, а endif обязательна и сигнализирует о завершении предыдущих проверок.


# Напечатает "Of course, 5 > 1!":
if(5 GREATER 1)
    message("Of course, 5 > 1!")
elseif(5 LESS 1)
    message("Oh no, 5 < 1!")
else()
    message("Oh my god, 5 == 1!")
endif() 

Циклы


Циклы в CMake подобны циклам других языков программирования. В приведённом примере устанавливается значение переменной VARIABLE в Airport, а затем четыре вложенные команды последовательно исполняются пока значение переменной VARIABLE будет равняться Airport. Последняя четвёртая команда set(VARIABLE "Police station") устанавливает значение проверяемой переменной в Police station, поэтому цикл сразу остановится, не дойдя до второй итерации. Команда endwhile сигнализирует о завершении списка вложенных в цикл команд.


# Напечатает в консоль три раза "VARIABLE is still 'Airport'":
set(VARIABLE Airport)
while(${VARIABLE} STREQUAL Airport)
    message("VARIABLE is still '${VARIABLE}'")
    message("VARIABLE is still '${VARIABLE}'")
    message("VARIABLE is still '${VARIABLE}'")
    set(VARIABLE "Police station")
endwhile()

Данный пример цикла foreach работает следующим образом: на каждой итерации данного цикла переменной VARIABLE присваивается следующее значение из списка Give me the sugar please!, а затем исполняется команда message(${VARIABLE}), которая выводит текущее значение переменной VARIABLE. Когда значений в списке не остаётся, то цикл завершает своё выполнение. Команда endforeach сигнализирует о завершении списка вложенных в цикл команд.


# Напечатает "Give me the sugar please!" с новых строк:
foreach(VARIABLE Give me the sugar please!)
    message(${VARIABLE})
endforeach()

Существуют ещё 3 формы записи цикла foreach. Первый цикл в данном примере на место списка генерирует целые числа от 0 до 10, второй цикл генерирует в диапазоне от 3 до 15, а третий цикл работает в сегменте от 50 до 90, но с шагом 10.


# Напечатает "0 1 2 3 4 5 6 7 8 9 10" с новых строк:
foreach(VARIABLE RANGE 10)
    message(${VARIABLE})
endforeach()

# Напечатает "3 4 5 6 7 8 9 10 11 12 13 14 15" с новых строк:
foreach(VARIABLE RANGE 3 15)
    message(${VARIABLE})
endforeach()

# Напечатает "50 60 70 80 90" с новых строк:
foreach(VARIABLE RANGE 50 90 10)
    message(${VARIABLE})
endforeach()

Функции и макросы


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


# Определение функции "print_numbers":
function(print_numbers NUM1 NUM2 NUM3)
    message(${NUM1} " " ${NUM2} " " ${NUM3})
endfunction()

# Определение макроса "print_words":
macro(print_words WORD1 WORD2 WORD3)
    message(${WORD1} " "  ${WORD2} " " ${WORD3})
endmacro()

# Вызов функции "print_numbers", которая напечатает "12 89 225":
print_numbers(12 89 225)

# Вызов макроса "print_words", который напечатает "Hey Hello Goodbye":
print_words(Hey Hello Goodbye)

Команда function первым аргументов принимает имя будущей функции, а остальные аргументы — это имена параметров, с которыми можно работать как с обычными переменными. Параметры видимы лишь определяемой функции, значит вне функции доступ к её параметрам мы получить не можем. Более того, все другие переменные, определяемые и переопределяемые внутри функции, видны лишь ей самой.


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


Как отметили в комментариях, макросы в CMake подобны макросам в препроцессоре языка Си: если в тело макроса поместить команду return, то произойдёт выход из вызвавшей функции (или из всего скрипта), что демонстрирует данный пример:


# Определить макрос, содержащий команду выхода:
macro(demonstrate_macro)
    return()
endmacro()

# Определить функцию, вызывающую предыдущий макрос:
function(demonstrate_func)
    demonstrate_macro()
    message("The function was invoked!")
endfunction()

# Напечатает "Something happened with the function!"
demonstrate_func()
message("Something happened with the function!")

В приведённом выше примере функция demonstrate_func не успеет напечатать сообщение The function was invoked!, так как прежде, на место вызова макроса demonstrate_macro будет подставлена и выполнена команда выхода.


Разбор аргументов


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


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


Работа механизма разбора аргументов заключается в преобразовании полученных аргументов в значения переменных. Таким образом, рассмотренная команда для каждой опции и ключевого слова определяет собственную переменную вида <Prefix>_<OptionOrKeyword>, инкапсулирующую некоторое значение. Для опций — это булевы значения (истина — опция указана, иначе — ложь), а для ключевых слов — все переданные значения, расположенные после них.


Функция custom_function содержит вызов команды cmake_parse_arguments, а затем команды печати значений определённых переменных. Далее, функция вызывается с аргументами LOW NUMBER 30 COLORS red green blue, после чего производится печать на экран:


function(custom_function)
    # Вызвать механизм обработки аргументов для текущей функции:
    cmake_parse_arguments(CUSTOM_FUNCTION "LOW;HIGH" "NUMBER" "COLORS" ${ARGV})

    # Напечатает "'LOW' = [TRUE]":
    message("'LOW' = [${CUSTOM_FUNCTION_LOW}]")
    #Напечатает "'HIGH' = [FALSE]":
    message("'HIGH' = [${CUSTOM_FUNCTION_HIGH}]")
    # Напечатает "'NUMBER' = [30]":
    message("'NUMBER' = [${CUSTOM_FUNCTION_NUMBER}]")
    # Напечатает "'COLORS' = [red;green;blue]":
    message("'COLORS' = [${CUSTOM_FUNCTION_COLORS}]")
endfunction()

# Вызвать функцию "custom_function" с произвольными аргументами:
custom_function(LOW NUMBER 30 COLORS red green blue)

Области видимости


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


Как упомянули в комментариях, переменные можно определять в "родительской" области видимости с помощью команды set(VARIABLE ... PARENT_SCOPE). Данный пример демонстрирует эту особенность:


# Функция, определяющая переменную "VARIABLE" со значением
# "In the parent scope..." в родительской области видимости:
function(demonstrate_variable)
    set(VARIABLE "In the parent scope..." PARENT_SCOPE)
endfunction()

# Определить переменную "VARIABLE" в текущей области видимости:
demonstrate_variable()

# Теперь возможно получить к переменной "VARIABLE" доступ:
message("'VARIABLE' is equal to: ${VARIABLE}")

Если из определения переменной VARIABLE убрать PARENT_SCOPE, то переменная будет доступна лишь функции demonstrate_variable, а в глобальной области видимости она примет пустое значение.


Заключение


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

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 18

    +9
    Первое что я говорю тем кто в команде начинается знакомиться с CMake — выучить правила для variable expansion.
    1) Двойные кавычки позволяют превратить список в строку (разделенную символом ";")
    2) Подстановку переменной любого типа не в контексте строковой константы — превращает ее автоматически в список всегда. символы ";" исчезают.
    Второе, что следует запомнить, что практически все API — все эти if-ы, foreach и прочие add_target — работают со списком строк. т.е. как бы vector<string>
    соответственно, можно делать штуки вроде
    if (${some_long_condition})
    и отсюда приходит понимание почему иногда выдает ошибку код
    if (${foo} STREQUAL "bar")
    — если foo неопределена (или пуста), условие превратится в список из двух строк «STREQUAL» и «bar».
    Поэтому третье что следует понять при чтении документации, когда допустимо значение, когда строка, а когда и то, и то (и это в документации обычно явно отражено фразами вроде «string or variable»)
      +1
      Напишите статью-гайд для новичков со своим видением.
        0
        Простите, я Вашу статью нисколько не хотел принизить, просто хотел дополнить теми вещами, с которыми прежде всего (по моему опыту) сталкиваются новички.
          +1
          Это не его статья ;)
            +1
            Ой!)
            Я думаю, люди все равно читают первые комментарии к статье, так что, пожалуй просто продолжу комментировать существующие, чтобы не плодить сущности =)
        +4
        Ваши замечания я описал в разделе «Аргументы», спасибо за содержательные комментарии.
        +4
        Для отладки я бы так же посоветовал упомянуть помимо message, замечательные флаги --trace и --trace-expand, они позволяют вывести каждую выполненную команду именно в том порядке, как они были выполнены интерпретатором. Так же они помогают понять «узкие места» в коде, которые тормозят выполнение, косвенно, конечно.
          +4
          Макросы аналогичны функциям за тем исключением, что они не имеют собственной области видимости

          я бы СРАЗУ упомянул еще про одну особенности, что они буквально работают как макросы в препроцессоре C — т.е. если в макросе вставить return(), то произойдет выход из родительской функции, вызвавшей макрос.
          В связи с тем, что родительские переменные можно выставлять через SET(… PARENT_SCOPE), я бы лично не советовал использовать макросы в большинстве случаев. Вызов функций в cmake достаточно дешевая операция, по сравнению со многими командами.
            +3
            Спасибо за комментрий, ваши замечания я описал в статье.
            +5
            На самом деле, все переменные по умолчанию считаются глобальными (доступ к ним есть везде)

            Есть одно исключение из этого правила — при вызове add_subdirectory — все включенное в дочернем CMakeLists.txt не просочится наружу (я о переменных). В то время как include() такой особенности не имеет.
              +2

              Здравствуйте. Спасибо за комментарии, ваши замечания будут учтены, по возможности, в следущей статье)

                +2
                С точки зрения стиля кодирования я бы вообще не рекомендовал где-то сильно рассчитывать на глобальность переменных, т.к. может быть такая ситуация:
                — Написал вспомогательный cmake-файл, в нем поменял CMAKE_CXX_FLAGS, например. Функций в нем нет.
                — Потом спустя какое-то время, пишется функция, и в ней включается этот файл. Сюрприз-сюрприз, после выхода из функции CMAKE_CXX_FLAGS останутся прежними.
                Поэтому rule of thumb я бы сформулировал «не используйте запись в глобальные переменные cmake для передачи информации между частями скрипта».
                В случаях, когда это позарез надо, использовать явно set(… CACHE… "" FORCE)
                0

                Благодарю за полезное замечение. Оно добавлено в новую статью.

                +2
                Пришлось столкнуться с Смаке при сборке FLTK. Насторожило три вещи:

                1. Отказалась работать на Windows — демки FLTK не собираются из-за какого то глюка спавна только что собранного .exe с путаницей прямого/обратного разделителя пути. Кроссплатформа под вопросом
                2. Добавить тулчейн в систему сборки сложно. Внутренности скриптов Смейка — адский ад
                3. Генерируемые мейкфайлы весьма изобретательны и практически нечитаемы. Разобрать проблемы вышеуказанных пунктов — отдельная проблема

                Итого осталось впечатление сырости.
                  +1
                  1. Ну это претензии к конкретному проекту — с любым проектом может быть такое что сборка под Windows находится в сыром/полуподдерживаемом состоянии, не только с CMake. Даже с предельно выскокоуровневым qbs в исходниках qtc периодически были коммиты «починка сборки под win», хотя там разработчики активно на всех платформах сидят.
                  2. полностью поддерживаю. Вся внутренняя кухня cmake — что скрипты, что исходники — адовый ад.
                  3. Генерируемый код на то и есть таковой, чтобы его не нужно было читать глазками) Впрочем, по своему опыту могу сказать — я почти всегда использую генератор Ninja — там в 1 файле Rules складываются, в основном — все параметры. Вполне очень наглядно и читаемо, попробуйте. Я пока отлаживал свою систему распределённой сборки, а также вспомогательные утилиты (типа msbuild2ninja) очень активно использовал раскуривание генерируемого cmake ninja-файла, все вполне наглядно.

                  Про сырость — впечатление обманчивое, т.к. cmake используется очень давно в куче продакшн систем, и в целом хорошо себя зарекомендовал — хорошая стабильность в плане регрессий, хорошая совместимость (люди последовательно мигрировавшие с какого-нибудь 2.8.11 до 3.12 меня поймут). Он попахивающий, если лезть внутрь, но не сырой)
                  0
                  Какие хорошие и полезные статьи пошли!
                    0
                    Думаю, для option стоило ещё добавить ссылку на cmake -LH, который выводит список опций из кеша (-L) и их описание (-LH).

                    А то минут 30 копался, как из CMakeLists.txt по-человечески достать description для option.
                      0
                      Вышел выпуск подкаста C++Cast с участием Крейг Скотт: "Professional CMake". Крейг является co-maintainer в проекте CMake и автором книги "Professional CMake: A Practical Guide". Заодно хочется проинформировать, что 17 сентября он выступит с докладом "Deep CMake for Library Authors" на CppCon.

                      Only users with full accounts can post comments. Log in, please.