Введение в CMake

imageCMake — кроcсплатформенная утилита для автоматической сборки программы из исходного кода. При этом сама CMake непосредственно сборкой не занимается, а представляет из себя front-end. В качестве back-end`a могут выступать различные версии make и Ninja. Так же CMake позволяет создавать проекты для CodeBlocks, Eclipse, KDevelop3, MS VC++ и Xcode. Стоит отметить, что большинство проектов создаются не нативных, а всё с теми же back-end`ами.

Для того что бы собрать проект средствами CMake, необходимо в корне дерева исходников разместить файл CMakeLists.txt, хранящий правила и цели сборки, и произвести несколько простых шагов.
Разберёмся на примерах.

Пример 1. Hello, World:


Для начала напишем простейший хеловорлд и создадим структуру проекта:
main.cpp
#include <iostream>
int main(int argc, char** argv)
{
	std::cout << "Hello, World!" << std::endl;
	return 0;
}


CMakeLists.txt
cmake_minimum_required(VERSION 2.8) # Проверка версии CMake.
									# Если версия установленой программы
									# старее указаной, произайдёт аварийный выход.

add_executable(main main.cpp)		# Создает исполняемый файл с именем main
									# из исходника main.cpp



Синтаксис CMake похож на синтаксис bash, всё что после символа "#" является комментарием и обрабатываться программой не будет. CMake позволяет не засорять дерево исходных кодов временными файлами — очень просто и без лишних телодвижений сборка производится «Out-of-Source».

Создадим пустую директорию для временных файлов и перейдём туда.
fshp@panica-desktop:~$ mkdir tmp
fshp@panica-desktop:~$ cd tmp/
fshp@panica-desktop:~/tmp$

Теперь запустим команду cmake, передав ей в качестве параметра путь к папке с исходниками:
fshp@panica-desktop:~/tmp$ cmake ~/cmake/example_1/

— Build files have been written to: /home/fshp/tmp
fshp@panica-desktop:~/tmp$
fshp@panica-desktop:~/tmp$ ls
CMakeCache.txt CMakeFiles cmake_install.cmake Makefile
fshp@panica-desktop:~/tmp$

Видим, что в папке появилось несколько временных файлов, необходимых для сборки проекта.
Теперь можно запустить непосредственно make:
fshp@panica-desktop:~/tmp$ make
Scanning dependencies of target main
[100%] Building CXX object CMakeFiles/main.dir/main.cpp.o
Linking CXX executable main
[100%] Built target main
fshp@panica-desktop:~/tmp$ ./main
Hello, World!
fshp@panica-desktop:~/tmp$

Итак, наша программа собралась.
Папку tmp можно очищать\удалять без риска поломать исходники. Если CMakeLists.txt был изменен, то вызов make автоматически запустит cmake. Если исходники были перемещены, то нужно очистить временную директорию и запустить cmake вручную.

Пример 2. Библиотеки:


Если ваш проект содержит библиотеку, то CMake соберет ее без проблем.
Для этого усложним пример.
foo.h
void hello_world();


foo.cpp
#include <iostream>
void hello_world()
{
	std::cout << "Hello, World!" << std::endl;
}


main.cpp
#include "foo.h"
int main(int argc, char** argv)
{
	hello_world();
	return 0;
}


CMakeLists.txt
cmake_minimum_required(VERSION 2.8)	 # Проверка версии CMake.
										# Если версия установленой программы
										# старее указаной, произайдёт аварийный выход.

project(hello_world)			# Название проекта

set(SOURCE_EXE main.cpp)		# Установка переменной со списком исходников для исполняемого файла

set(SOURCE_LIB foo.cpp)			# Тоже самое, но для библиотеки

add_library(foo STATIC ${SOURCE_LIB})	# Создание статической библиотеки с именем foo

add_executable(main ${SOURCE_EXE})	# Создает исполняемый файл с именем main

target_link_libraries(main foo)		# Линковка программы с библиотекой



Переменные могут хранить списки значений, разделённых пробелами\табуляциями\переносами:
set(SOURCE main.cpp foo.cpp)
set(HEADER main.h
			foo.h)

Оба варианта правильные
Что бы получить значение переменной ипользуем конструкцию:
${var_name}

Итак, эта версия нашего проекта включает в себя одну статическую библиотеку, собираемую из исходников. Если заменить «STATIC» на «SHARED», то получим библиотеку динамическую. Если тип библиотеки не указать, по умолчанию она соберётся как статическая.
При линковке указываются все необходимые библиотеки:
target_link_libraries(main  foo
							ogg
							vorbis)

Как и при ручной компиляции, имена библиотек указываются без стандартного префикса «lib».
Итак, сборка библиотек с CMake не вызывает проблем, при этом тип библиотеки статическая\динамическая меняется лишь одним параметром.

Пример 3. Подпроекты:


Подпроекты очень удобны, если ваша программа разбита на несколько библиотек или же проект состоит из нескольких программ.
Каждый подпроект является по сути полноценным проектом и может использоваться самостоятельно.
Теперь у нас «foo» находится в субдирректории и там же находится CMakeLists.txt подпроекта.
CMakeLists.txt
cmake_minimum_required(VERSION 2.8) # Проверка версии CMake.
									# Если версия установленой программы
									# старее указаной, произайдёт аварийный выход.

project(hello_world)				# Название проекта

set(SOURCE_EXE main.cpp)			# Установка переменной со списком исходников

include_directories(foo)			# Расположение заголовочных файлов

add_executable(main ${SOURCE_EXE})	# Создает исполняемый файл с именем main

add_subdirectory(foo)				# Добавление подпроекта, указывается имя дирректории

target_link_libraries(main foo)		# Линковка программы с библиотекой


main.cpp
#include "foo.h"
int main(int argc, char** argv)
{
	hello_world();
	return 0;
}


foo/CMakeLists.txt
cmake_minimum_required(VERSION 2.8) # Проверка версии CMake.
									# Если версия установленой программы
									# старее указаной, произайдёт аварийный выход.

project(foo)				# Название проекта

set(SOURCE_LIB foo.cpp)		# Установка переменной со списком исходников

add_library(foo STATIC ${SOURCE_LIB})# Создание статической библиотеки


foo/foo.h
void hello_world();


foo/foo.cpp
#include <iostream>
void hello_world()
{
	std::cout << "Hello, World!" << std::endl;
}



В файле подпроекта ничего нового для вас нет. А вот в основном файле новые команды:

include_directories(foo)
main.cpp мы не меняли, а foo.h перенесли. Команда указывает компилятору, где искать заголовочные файлы. Может быть вызвана несколько раз. Хидеры будут искаться во всех указаных директориях.

add_subdirectory(foo)
Указываем директорию с подпроектом, который будет собран как самостоятельный.
Вывод: проекты на CMake можно объединять в довольно сложные иерархические структуры, причем каждый подпроект в реальности является самостоятельным проектом, который в свою очередь может сам состоять из подпроектов. Это позволяет легко разбить вашу программу на необходимое количество отдельных модулей. Примером такого подхода может служить KDE.

Пример 4. Поиск библиотек:


CMake обладает достаточно развитыми средствами поиска установленых библиотек, правда они не встроеные, а реализованы в виде отдельных модулей. В стандартной поставке довольно много модулей, но некоторые проекты (например Ogre) поставляют свои. Они позволяют системе автоматически определить наличие необходимых для линковки проекта библиотек.
На debian модули располагаются в /usr/share/cmake-2.8/Modules/ (у вас версия может отличаться). За поиск библиотек отвечают модули, называющиеся FindNAME.cmake, где NAME — имя библиотеки.
find_package(SDL REQUIRED)
if(NOT SDL_FOUND)
	message(SEND_ERROR "Failed to find SDL")
	return()
else()
	include_directories(${SDL_INCLUDE_DIR})
endif()
##########################################################
find_package(LibXml2 REQUIRED)
if(NOT LIBXML2_FOUND)
	message(SEND_ERROR "Failed to find LibXml2")
	return()
else()
	include_directories(${LIBXML2_INCLUDE_DIR})
endif()
##########################################################
find_package(Boost COMPONENTS thread-mt REQUIRED)
if(NOT Boost_FOUND)
	message(SEND_ERROR "Failed to find boost::thread-mt.")
	return()
else()
	include_directories(${Boost_INCLUDE_DIRS})
endif()
##########################################################
target_link_libraries(${TARGET} 
								${SDL_LIBRARY}
								${LIBXML2_LIBRARIES}
								${Boost_LIBRARIES})

Думаю, смысл должен быть понятен. Первый и второй блок — поиск библиотеки. Если в системе её нет, выведется сообщение об ошибке и завершается выполнение cmake. Третий блок похож, только он ищет не целый пакет библиотек, а лишь необходимый компонент. Каждый такой автоматизированый поиск определяет после выполнения как минимум 3 переменные:
SDL_FOUND, LIBXML2_FOUND, Boost_FOUND — признак присутствия бибилиотеки;
SDL_LIBRARY, LIBXML2_LIBRARIES, Boost_LIBRARIES — имена библиотек для линковки;
SDL_INCLUDE_DIR, LIBXML2_INCLUDE_DIR, Boost_INCLUDE_DIRS — пути к заголовочным файлам.
Если с первыми более или менее понятно, то вторые и третьи мне доставили много хлопот — половина имеет имена в единственном числе, половина — во множественном. Но оказалось, это легко отследить. В каждом модуле вначале есть коментарии, там описаны определяемые переменные. Посмотрите, например, /usr/share/cmake-2.8/Modules/FindLibXml2.cmake
Как видите, CMake способен сам определить наличие и местоположение необходимых библиотек и заголовочных файлов. В принципе, это должна уметь любая система автоматической сборки, иначе смысл в ней?

Пример 5. Внешние библиотеки и объектные файлы:


Если вы пишите для «дяди», а злой «дядя» любит самописные библиотеки и делиться исходниками не желает, поэтому присылает готовую библиотеку, то вы по адресу.
Объектные файлы в CMake стоят на ряду с исходниками — достаточно включить объектник в список файлов для компиляции.
С библиотеками потуже. Как известно, статическая библиотека это не что иное, как ar-архив, внутри которого лежат обычные объектники, никак не связаные между собой. Вы, наверное, уже догадались, как я поступал сначала. Да, просто потрошил библиотеку. Но потом был найден способ поэлегантнее:
add_library(netutil STATIC IMPORTED)
set_property(TARGET netutil PROPERTY
             IMPORTED_LOCATION Binary/game_client/libnetutil.a)

Слово «IMPORTED», указывает, что библиотека берётся извне.
В CMake каждая цель имеет параметры, а set_property позволяет их изменять.
Линкуется такая библиотека стандартно:
target_link_libraries(${TARGET} netutil)

Для динамических библиотек все аналогично, только тип «SHARED», расширение — ".so".
К сожалению, поддержка несистемных библиотек реализована немного костыльно. Возможно, я просто не знаю правильного варианта, поэтому буду рад, если «ткнете мордочкой». С другой стороны это не навороченый экзоскелет с системой жизнеобеспечения, а простейший костыль из двух строк.

Генераторы:


Как было сказано в начале, CMake умеет генерировать множество различных видов проектов. Это удобно и позволяет использовать CMake для практически любой популярной IDE.
Если запустить cmake без параметров, в конце будут описаны доступные генераторы. Пользоваться так:
fshp@panica-desktop:~/tmp$ cmake ~/cmake/example_3/ -G "KDevelop3 — Unix Makefiles"

Заключение:


Это не перевод мануала, а результат использования CMake в одном коммерческом проекте. Буду рад, если статья поможет хотя бы одному человеку — на русском языке подобной документации довольно мало.

Чем понравился CMake лично мне:
  • один проект — один файл. Не нужно хранить кучу скриптов настройки, сборки и прочего хлама;
  • Скорость работы в сравнении с autotools;
  • простой и понятный синтаксис, конечно с элегантностью питона не потягаться, но и не брейнфак, в конце концов.;
  • является front-end`ом для множества IDE;
  • отображение прогресса — довольно удобно;
  • цветной вывод — в серые будни немного краски не помешает;

Для Sublime Text есть плагин, добавляющий подсветку синтаксиса CMake, он так и называется — «CMake».
Примеры
Share post

Comments 22

  • UFO just landed and posted this here
      +2
      1. В нашем проекте используется пачка статических библиотек, Ogre, Xml2, OpenAL, SDL, Boost и ещё несколько. Всё это разбито на 3 подпроекта + главный проектный файл, итого 181 сточка. Необходимо учитывать, что сюда входит список исходников.

      2. C XML вы тоже что-то путаете. Слишком весомый. JSON это да. Но есть одно «но»: JSON это стандарт rfc4627, датируемый 2006 годом, первые версии CMake появились в 2003. Поэтому за основу был взять Bash.

      3. Тут вы правы, но когда начальство требует, что бы проект открывался в CodeBlocks, это экономит кучу времени.
      • UFO just landed and posted this here
          +1
          1. Заведено 4 CMakeLists.txt — 3 подпроекта: движок и логика в виде библиотек, лаунчер. Все это объедятся одним родительским проектом.

          2. Личные предпочтения. Возможно, я тоже предпочту XML, если вы мне подскажете хороший инструментарий.

          3. Начальство бывает разное, некоторое и код иногда просматривает, а некоторое и само свои проекты имеет.
      +5
      простой и понятный синтаксис;


      Поделили на ноль. Куча неочевидных конструкций, сплошные макросы с непонятной областью видимости, загрязнение глобальной области видимости. Идиотский синтаксис условных выражений и придурковатый способ поиска зависимостей.
      В итоге написал пару более менее декларативных оберток над этим ужасом, но с каждым днем все больше хочется от него избавится!
      github.com/gorthauer/cmake-utils

      Я уже молчу про чудовищно тормозную компиляцию на windows'е. Просто ни в какие ворота не лезет, qbs на том же самом коде в 10 раз быстрее справляется!
        +3
        Имея лишь начальные знания английского языка, любое ключевое слово не остается для меня загадкой.
        Все функции имеют более или менее унифицированный способ вызова — на первом месте всегда стоит объект, подвергающийся обработке. Разное количество параметров — уж извините, для разных действий необходимы различные параметры.

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

        Тормозная компиляция это проблема не CMake. Она ничего сама не компилирует.

        За ссылку спасибо.
        +3
        Там где можно было бы написать одну строку,

        includes += required(libxml2).include_dir
        


        Пишется семь:

        find_package(LibXml2 REQUIRED)
        if(NOT LIBXML2_FOUND)
            message(SEND_ERROR "Failed to find LibXml2")
            return()
        else()
            include_directories(${LIBXML2_INCLUDE_DIR})
        endif()
        


        И, при выставленном флаге REQUIRED, зачем-то всё равно проверяется статус операции.

        Отличный императивный язык, да. И парсить его будет несложно. Автору спасибо.
          0
          Я не претендую на специалиста по CMake, я лишь попытался описать то, с чем сам сталкивался. Знаете, при выставленном флаге REQUIRED и не установленной библиотеке ветка if выполнялась все равно. debian sid/wheezy. Может я и не умею готовить CMake, но таким образом я отгородил себя от ошибок на стадии сборки. Не забывайте, статья все-таки для новичков.
          И да, ваш пример красив, есть куда расти.
            0
            Я думаю, автор комментария в первом примере привел текст для qmake.
          • UFO just landed and posted this here
            +1
            Два дня, две отличных статьи по CMake. Спасибо.
              0
              Всегда пожалуйста. Не было статей, не было. А тут что-то двоих сразу пропёрло.
                0
                Мне чем CMake приглянулся, так это тем, что позволяет собирать проекты не только для ПК, но и для микроконтроллеров (в принципе разницы нет). Например для STM32. Я уже приспособил Sublime Text 2 для этого дела, а системой сборки как раз выбрал Cmake, благо была статья одного товарища, где он всё немного пояснил по этому поводу.
                Но полного понимания работы пока нет. А такие статьи, как ваша, очень помогают в этом.
                  0
                  Ну CMake в этом плане довольно гибкое средство. Можно вообще LLVM прикрутить и будет вам счастье. Он не привязан к конкретному набору компиляторов, хоть по умолчанию и GNU набор использует.
              –1
              Если проект на C++ и активно используется Boost, то почему бы не использовать Boost-Jam (bjam, b2), странно что этот топик тут не освещён совсем.
                0
                Ну из буста в нашем проекте используются лишь нити, поэтому я бы не назвал его «активно использующий буст».
                И да, топик про CMake. Он более универсален. Как написали выше, его прикрутили для сборки под МК. Вряд ли под МК буст востребован.
                +1
                Спасибо, буду давать сюда линк тем, кто начинает осваивать :)
                  0
                  И вам спасибо. :-)
                  0
                  Очень жаль, что пример 5 не описан так детально, как и предыдущие, а просто копипаст с cmake-кого wiki. Не могу разобратся куда и как вставить этот кусок кода, чтобы все собралось без ошибок. Может кто еще здесь подскажет?
                    0
                    Что-то я пропустил ваш комментарий. Надеюсь, я ещё могу вам помочь?
                      0
                      Большое спасибо, что все-таки откликнулись, но я уже решил эту проблему!
                    0
                    Автор молодец!
                    Отличная статья, все здорово расписано

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