Используем Cmake для автоматической генерации makefile в проектах

в 12:23, , рубрики: cmake, мануал, начинающим, Программирование, метки: , ,

  Вступление большое, так как подробно объясняет зачем нужен cmake. Можете сразу под кат, если уже знаете.

Вступление

  Компилирование проекта руками — пустая трата времени. Это фактически аксиома и об этом знают те, кто программирует. Но чтобы всё скомпилировалось автоматически необходимо задать правила, так ведь? Часто и по-старинке используют makefile для *nix или какой-нибудь nmake для windows.
  Я хоть и не первый год программирую, и руками составлял простые автосборщики проектов на основе makefile, но стоит немного подзабыть и приходится заново изучать как же составить эту хитрую схему. В основном приходится делать проекты расчитанные на какую-то одну систему, будь то linux или windows, и часто между собой не кросскомпилируемые. Для переносимости makefile используется automake и autogen, но их синтаксис ещё более запутан. Не скажу, что выбор идеальный, но для себя я решил перейти на cmake, благо он портирован под всё доступное. Мне он показался более человекопонятным. Попробую объяснить основы. Вы пишите словами правила, а из них генерируется makefile, который вы уже запускаете стандартным способом.

Ликбез

  Зачем он нужен? Чтобы при переносе на другую машину, с другими путями вы двумя командами собрали проект ничего не исправляя в файле makefile. Но есть же configure? Это альтернатива. И configure не кросплатформенный, для его генерации нужен autoconf/autogen, для которых идёт ещё свой набор правил. Только преимущества? Компиляция автосгенерированным makefile получается немного медленнее старого способа. Но Qt пользуются cmake и никто не жалуется.

Приступая к работе

  В текущей обучалке я расскажу о самом простом шаблоне С++ проекта, который должен компилироваться на линуксе. Ни о какой переносимости не пойдёт речи, не будут назначаться разные компиляторы и т.д. В одну статью не влезет, да и нагромождение условностей слишком запутает новичка.
  Сначала о негласно принятой структуре проектов на cmake. Обычно в корне проекта лежат папки src (для закрытых исходников), include (для публичных заголовков), lib (для подключаемых библиотек, если они не системные), пустая папка build (в ней происходит сборка, если нет, то создаём), я ещё добавляю bin (или out, для получившихся бинарников). Так же требуется наличие в корне файлов AUTHOTS, COPYING, INSTALL, NEWS, README, ChangeLog. Они могут быть пустыми, но наличие обязательно. Опциональным может быть config.h.in файл (о нём далее). Файлы конфигурации cmake лежат в каждой подключаемой папке с названием CMakeLists.txt. Это название придумано авторами cmake и не меняется.
  Документация изначально тяжела для понимания, хотя и полная. Это ещё один минус. Но по сравнению с autotools…
Список типов makefile которые может генерировать cmake:

Скрытый текст

Borland Makefiles
MSYS Makefiles
MinGW Makefiles
NMake Makefiles
NMake Makefiles JOM
Ninja
Unix Makefiles
Visual Studio 10
Visual Studio 10 IA64
Visual Studio 10 Win64
Visual Studio 11
Visual Studio 11 ARM
Visual Studio 11 Win64
Visual Studio 6
Visual Studio 7
Visual Studio 7 .NET 2003
Visual Studio 8 2005
Visual Studio 8 2005 Win64
Visual Studio 9 2008
Visual Studio 9 2008 IA64
Visual Studio 9 2008 Win64
Watcom WMake
Xcode
CodeBlocks — MinGW Makefiles
CodeBlocks — NMake Makefiles
CodeBlocks — Ninja
CodeBlocks — Unix Makefiles
Eclipse CDT4 — MinGW Makefiles
Eclipse CDT4 — NMake Makefiles
Eclipse CDT4 — Ninja
Eclipse CDT4 — Unix Makefiles

  Пусть у нас есть некий source.cpp из которого получаем софт — положим его в папку src. Но у нас же проект, поэтому есть ещё несколько файлов core.cpp, core.hpp, common.hpp, types.hpp, которые тоже положим в src и нужна какая-нибудь библиотека, например pthread. Разобрались с исходниками, приступаем к описанию проекта и подготовке его к автокомпилированию.

  Начинается всё с создания в корне проекта файла CMakeLists.txt. Правила cmake похожи на скриптовый язык, среднее между javascript и php. Только проще гораздо. Есть условия, функции, переменные, константы, подключаемые модули.

  Файл CMakeLists.txt я разобью на несколько частей, чтобы объяснять их. Часть 1:

# комментарии начинаются с решётки
cmake_minimum_required (VERSION 2.6) 

cmake_policy(SET CMP0011 NEW)
cmake_policy(SET CMP0003 OLD)

OPTION(WITH_DEBUG_MODE "Build with debug mode" ON)

if ( NOT UNIX )
    message (FATAL_ERROR "Not Unix!")
endif ()

  Тут cmake_minimum_required функция проверки версии.
Операторы записываются как if () открывающий и endif() закрывающий. Аналогично foreach() и endforeach().
Функция message выводит наше сообщение. Я использовал флаг FATAL_ERROR для обозначения типа сообщения.
Заметьте так же, что не ставится точка с запятой (;) в конце команд. Тут одна строка — одна команда. Скобки отодвинуты от операторов просто для удобства чтения.
Каждая команда обычно имеет несколько вариантов задания параметров, поэтому без заглядывания в мануал не обходится никак.
Быстрое ознакомление и простой пример есть в документации, но на мой взгляд слишком простые.

Часть 2:

message ("Starting cmake")

# я вынес настройки путей, флаги компиляции в отдельный фаил, чтобы не громоздить здесь лишнего
include (myproj.cmake)

# создаём новый проект
set (PROJECT myproj)

# в текущем проекте ничего не нужно дополнительно компилировать
set (LIBRARIES)
# следующий код нужен для компиляции и подключения сторонних библиотек 
    foreach (LIBRARY ${LIBRARIES})
        find_library("${LIBRARY}_FOUND" ${LIBRARY})
        message(STATUS "Check the ${LIBRARY} is installed: " ${${LIBRARY}_FOUND})
        if ( "${${LIBRARY}_FOUND}" STREQUAL "${LIBRARY}_FOUND-NOTFOUND" )
            message(STATUS "Adding library sources")
            add_subdirectory (../${LIBRARY} lib/${LIBRARY})
        endif ()
    endforeach ()

# никаких дополнительных целей нет
set (TARGETS "")

set (HEADERS "")

message ( STATUS "SOURCES: ${SOURCES}")

add_subdirectory (src)

  Функция set() создаёт или перезаписывает переменные. Если нет значения, то переменная будет пустой. Заданные здесь переменные названы по смыслу, который они несут в генерации.
include() подключает внешний файл с куском конфигурации в текущее место. Тут без объяснений.
И add_subdirectory (src) указывает где продолжить работу по сборке makefile проекта.

  Если здесь задавались только общие правила, то внутри директории src в CMakeLists.txt будут заданы опции объединения конкретных исходных файлов.

  Не упомянул ещё о cmake_policy(). Решил не озадачивать сейчас вниканием в это. Пускай тут повесит © Просто облегчает сборку.
Про foreach() цикл и библитеки будет рассказано дальше. Пропустим пока.

  Что же такого было вынесено в отдельный cmake файл? Давайте рассмотрим:

set ("${PROJECT}_BINARY_DIR"  bin)
set ("${PROJECT}_SOURCE_DIR" src:include)
set ("${PROJECT}_LIB_DIR" lib)

set (CMAKE_INCLUDE_PATH ${${PROJECT}_SOURCE_DIR})
set (CMAKE_LIBRARY_PATH ${${PROJECT}_LIB_DIR})
set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/${${PROJECT}_BINARY_DIR})
set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_BUILD_TYPE Debug)

set (ERR_NO_UNIX "Cannot build on non Unix systems")

if ( WITH_DEBUG_MODE )
     ADD_DEFINITIONS( -DMY_DEBUG_MODE=1)
endif()

if ( CMAKE_COMPILER_IS_GNUCXX )
    set(MY_CXX_FLAGS  "-Wall -std=c++0x -fmessage-length=0 -v -L/usr/local/lib -L/usr/lib")
    set(CMAKE_CXX_FLAGS "-O0 ${MY_CXX_FLAGS}")
    # я отключил настройку разных флагов для релиза и отладки. Пока что не нужно.
    #set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fno-reorder-blocks -fno-schedule-insns -fno-inline")
    #set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
else ()
    message (FATAL_ERROR ${ERR_NO_UNIX})
endif ()

  Видим, что в set () использована странная (или уже привычная) конструкция в виде ${название} — она подставляет раннее определённую переменную с помощью того же set () или самим cmake (все определяемые cmake переменные есть в документации). Например ${PROJECT} вставит значение myproj из ранее определённой переменной PROJECT.
Запись src:include — просто строка, которая в линуксе означает перечисление путей (они там разделяются двоеточием, а не точкой с запятой).
  Первые три строки это мною заданные переменные. А вот дальше задаются необходимые переменные, требуемые cmake. Их можно и не задавать явно, они уже определены с запуском, но будут указывать не туда, куда нужно с данной структурой папок.

  Порою необходимо название переменной составлять исходя из других переменных, и такое возможно: ${${PROJECT}_SOURCE_DIR}. Сначала разыменуется PROJECT и получится ${myproj_SOURCE_DIR}, которая уже определена вначале. В итоге будет её значение. Всё это необходимые сложности, дабы если поменяется название проекта с myproj на superpuper не пришлось лазить по всему коду меняя названия переменных и прочего.

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

  if ( WITH_DEBUG_MODE ) — встречается особый вид переменных, которые начинаются на WITH_. Создаются они с помощью функции option() — функции, которая определяет пользовательские опции (она была в коде в самом начале). Эти переменные принимают только два значения ON или OFF и могут использоваться в качестве булевых значений, к примеру. И ADD_DEFINITION( -DMY_DEBUG_MODE=1) добавит компилятору опцию -DMY_DEBUG_MODE, если включён дебаг режим. Для С++ компилятора (в данном случае) это будет означать добавление дефайна.

Работа с исходным кодом

Вот и добрались до папки src. В ней тоже необходим CMakeLists.txt, рассмотрим его детальнее.

set ("${PROJECT}_VERSION_MAJ" 0)
set ("${PROJECT}_VERSION_MIN" 1)
set ("${PROJECT}_VERSION_A" 1)
set ("${PROJECT}_VERSION_B" 1)
set ("${PROJECT}_VERSION" ${${PROJECT}_VERSION_MAJ}0${${PROJECT}_VERSION_MIN}0${${PROJECT}_VERSION_A}0${${PROJECT}_VERSION_B})

message(STATUS ${${PROJECT}_VERSION})

# основной файл программы
set (MAIN_SOURCES
    source.cpp
    )

# непубличные пары исходников
set (PRIVATE_CLASSES
        core
    )

# файлы только заголовочные, у которых нет пары-исходника
SET (HEADERS_ONLY
        types
        common
    )

# публичные исходники
set (PUBLIC_CLASSES)

# используемые в программе библиотеки
set (ADDITIONAL_LIBRARIES
    stdc++
    pthread
    )

set (PUBLIC_HEADERS)
set (SOURCES)

foreach (class ${PRIVATE_CLASSES})
    LIST (APPEND SOURCES ${class}.cpp)
    LIST (APPEND HEADERS ${class}.hpp)
endforeach ()

foreach (class ${HEADERS_ONLY})
    LIST (APPEND HEADERS ${class}.hpp)
endforeach ()
    
foreach (class ${PUBLIC_CLASSES})
    LIST (APPEND SOURCES ${class}.cpp)
    LIST (APPEND HEADERS ../include/${PROJECT}/${class}.hpp)
    LIST (APPEND PUBLIC_HEADERS ../include/${PROJECT}/${class}.hpp)
endforeach ()

add_executable (${PROJECT} ${MAIN_SOURCES} ${SOURCES})

target_link_libraries (${PROJECT} ${ADDITIONAL_LIBRARIES})

set_target_properties(${PROJECT} PROPERTIES VERSION "${${PROJECT}_VERSION}" SOVERSION "0")

INSTALL (
    TARGETS
    ${PROJECT}
    DESTINATION
    lib${LIB_SUFFIX}
)

INSTALL (
    FILES
    ${PUBLIC_HEADERS}
    DESTINATION
    include/${PROJECT}
)

  Здесь записаны конкретные файлы и их сборка. Вначале определили версию программы. Дальше определяем группы (или как их тут называют, списки) исходников, все названия записываются через пробел. MAIN_SOURCES — один основной исходник, PRIVATE_CLASSES — список пар исходников (исходник.cpp-заголовок.hpp с общим названием), PUBLIC_CLASSES — для данного проекта пустой, HEADERS_ONLY — список лишь заголовочных файлов, ADDITIONAL_LIBRARIES — подключаемые в проект С++ библиотеки.

  Следующими циклами объединяются списки заголовков и исходников в один подключаемый список (замечу, что предыдущее разделение на публичный и скрытый было чисто условным). И, наконец, правило обозначающее «собрать проект в запускаемый файл» add_executable(). После сборки бинарника к нему нужно добавить «прилинковать» подключаемые библиотеки с помощью target_link_libraries() и всё. Правила проекта определены.
Для удобства можно добавить правила установки-инсталяции с помощью install().

  Переходим в директорию build, что в корне проекта, и запускаем

cmake ..

из командной строки. Если всё прошло удачно (а должно пройти удачно), получаем

— Configuring done
— Generating done
— Build files have been written to: /home/username/tmp/fooproj/build

и находящийся рядышком makefile
После чего уже привычным способом

make

Вместо заключения

Я нашёл данный способ более простым и удобным, нежели конфигурирование configure. Если вдруг захочется по спорить на этот счёт, то я воздержусь от комментирования. Потому что зачем?
Если вы что-то поменяли и cmake выдаёт ошибки, то сперва удалите весь кеш файлов из директории build (грубо говоря всё) или как минимум CMakeCache.txt из той же директории билда.

Что было не рассмотрено в обучалке:
1) Подключение библиотек Qt или Boost
2) Поиск установленных библиотек
3) Мультиплатформенность
4) Опции различных компиляторов

Документация:

1) Основная странцица документации
2) Документация по версии 2.8.9
3) Исходник этого безобразия прилагается.

Автор: romy4


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js