Основы сборки проектов на С/C++ при помощи CMake

10 мая 2017

Некоторое время назад мы познакомились с 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-интерфейс:

ccmake .

В рамках одного поста, конечно, не представляется возможным рассмотреть все возможности CMake. Однако представленной выше информации вам должно вполне хватить в 90% случаев. Полноценные рабочие примеры использования CMake вы найдете, например, в этом, этом, а также в этом репозиториях на GitHub. Примеры использования опций и условных операторов можно найти в репозиториях уже упомянутых Assimp и LLDB. Ну и, конечно же, массу полезного вы найдете на официальном сайте CMake.

А пользуетесь ли вы CMake и если да, используете ли какие-то его возможности, о которых не было рассказано выше?

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.