= CMake: управление проектом
:title-separator: {sp}|
:category: Программирование
:tags: программирование, cmake,
:toc:

== Полезные ссылки

* https://github.com/onqtam/awesome-cmake[Каталог ссылок]
* https://cgold.readthedocs.io/en/latest/index.html[CGold: The
Hitchhiker’s Guide to the CMake]

== Структура каталогов проекта

Файлы проекта и результаты компиляции размещаются в каталогах:

....
└── cmex
    ├── _build
    │   ├── Debug
    │   └── Release
    ├── .git
    ├── cmake
    │   ├── cmlib
    │   ├── etc
    │   ├── find
    │   └── generators
    ├── doc
    ├── files
    │   ├── etc
    │   ├── share
    │   └── var
    ├── l10n
    ├── src
    │   ├── app
    │   └── lib
    ├── thirdparty
    └── tools
....

Назначение каталогов приведено в таблице.

[cols="1,3",options="header",]
|===
|Каталог |Назначение
|`cmex/_build` |Результаты компиляции
|`cmex/_build/Debug` |Результаты компиляции в режиме отладки
|`cmex/_build/Release` |Результаты компиляции в режиме выпуска
|`cmex/.git` |Репозиторий git
|`cmex/cmake` |Файлы с дополнительными функциями для CMake
|`cmex/cmake/cmlib` |Библиотека функций для CMake
|`cmex/cmake/find` |Модули CMake для поиска внешних программ и библиотек
|`cmex/cmake/etc` |Файлы настроек, используемые в CMake
|`cmex/cmake/generators` |Генераторы проектов
|`cmex/doc` |Документация для проекта
|`cmake/files` |Каталог для дополнительных файлов
|`cmake/files/etc` |Каталог для файлов настроек проекта
|`cmake/files/share` |Каталог для неизменяемых файлов
|`cmake/files/var` |Каталог для изменяемых файлов
|`cmex/l10n` |Файлы переводов
|`cmex/src` |Исходные тексты
|`cmex/src/app` |Исходные тексты программ
|`cmex/src/lib` |Исходные тексты библиотек
|`cmex/thirdparty` |Исходные тексты сторонних проектов
|`cmex/tools` |Дополнительные утилиты
|===

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

== Начало проекта

В каталоге `cmex` нужно создать файл `CMakeLists.txt`:

[source,cmake]
----
# Минимальная версия Cmake
cmake_minimum_required(VERSION 3.3)
cmake_policy(VERSION 3.0.2..3.7)

# Название и версия проекта и используемые языки программирования
project(cmex VERSION 0.2.0 LANGUAGES C CXX)
----

Значение версии следует формировать согласно правилам
https://semver.org/lang/ru/[семантического версионирования].

В каталог `cmake/cmlib` установить подмодуль CMLib, содержащий функции
для CMake:

....
git submodule add ssh://git@gitlab-server/root/cmlib cmake/cmlib
....

и подключить в файле `CMakeLists.txt`:

[source,cmake]
----
# В каталоге cmake/cmlib находятся файлы с библиотечными функциями
if(IS_DIRECTORY ${CMAKE_SOURCE_DIR}/cmake/cmlib)
  list(INSERT CMAKE_MODULE_PATH 0 ${CMAKE_SOURCE_DIR}/cmake/cmlib)
else()
  message(FATAL_ERROR "CMake library directory does not exist")
endif()
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/find)

include(CMLibCommon)
----

В файле `cmake/etc/organization.txt` записать название организации,
которой принадлежит проект:

....
ORG, Inc.
....

В файле `cmake/etc/cpack_ignore.txt` перечислить шаблоны для исключения
из архива, создаваемого целью `dist`. Например:

....
cmake/lib/.git$
.git$
files/var
CMakeLists.txt.user
~$
\\\\..*\\\\.bak$
\\\\..*\\\\.tmp$
\\\\..*\\\\.swp$
....

Чтобы проверить корректность файла `CMakeLists.txt`, нужно создать
каталог `_build` в каталоге `cmex`, перейти в него и выполнить команды:

[source,sh]
----
cmake ..
make
----

== Поиск системных библиотек

Системные библиотеки можно искать с помощью программы `pkgconfig`,
которая хранит базу данных параметров, включающую пути к заголовочным
файлами и перечни библиотек, необходимых для компоновки. Сначала
производится наличие модуля `PkgConfig`, в котором определена функция
`pkg_check_modules`, которая и осуществляет поиск. Например, для поиска
библиотек `gsl`, `fftw3` и `udev` можно написать:

[source,cmake]
----
# Поиск библиотек с помощью pkgconfig
find_package(PkgConfig)
pkg_check_modules(GSL REQUIRED gsl)
pkg_check_modules(FFTW3 REQUIRED fftw3)
pkg_check_modules(UDEV udev)
----

Если системная библиотека поставляется без файла описания для
`pkgconfig`, то для её поиска может быть написан специальный модуль для
`CMake`, который вызывается функцией `find_package`. Кроме того функция
`find_package` может возвращать дополнительные значения, например, пути
к исполняемым файлам.

[source,cmake]
----
# Поиск с помощью функции find_package
find_package(LibXml2)
find_package(CURL)
----

Если для библиотеки нет модуля, выполняющего её поиск, то можно
произвести поиск с помощью функции `find_library`. Например,

[source,cmake]
----
# Поиск библиотеки с помощью функции find_library
find_library(MATHGL mgl PATHS /usr/lib /usr/lib/x86_64-linux-gnu)
find_library(MATHGLQT5 mgl-qt5 PATHS /usr/lib /usr/lib/x86_64-linux-gnu)
----

== Автоматически генерируемый заголовочный файл

На этапе конфигурирования проекта можно создать файл, в который будут
записаны параметры, полученные на данной стадии. В библиотеке CMLib
присутствует функция `cmlib_config_hpp_generate()`, создающая файл
`${CMAKE_BUILD_DIR}/include/config.hpp`, в который записывается
информация о имени и версии проекта, дате и типе сборки.

[source,cmake]
----
# Автоматически генерируемый заголовочный файл
cmlib_config_hpp_generate()
----

== Базовая библиотека

В файле `cmex/CMakeLists.txt` должна быть строка, включающая поиск файла
`CMakeLists.txt` в подкаталоге `src/lib`:

[source,cmake]
----
add_subdirectory(src/libcmex)
----

В каталоге `cmex/src/libcmex` нужно создать файл `cmex.hpp`:

[source,cpp]
----
#ifndef LIBCMEX_CMEX_HPP_
#define LIBCMEX_CMEX_HPP_

#include <stdint.h>

int32_t cmex_init(int32_t i);

#endif // LIBCMEX_CMEX_HPP_
----

файл `cmex.cpp`:

[source,cpp]
----
#include "cmex.hpp"

int32_t cmex_init(int32_t i = 0) {
    return i;
}
----

и файл `CMakeLists.txt`:

[source,cmake]
----
# Название основной цели и имя библиотеки в текущем каталоге
set(current_target cmex)

# Список файлов исходных текстов
set(current_target_sources
  cmex.cpp
)

# Список заголовочных файлов (используется для установки)
set(current_target_headers
  cmex.hpp
)

add_common_library(TARGET ${current_target} SOURCES ${current_target_sources})
common_target_properties(${current_target})

# Цель, используемая только для установки заголовочных файлов, без компиляции проекта
add_custom_target(${current_target}-install-headers
  COMMAND "${CMAKE_COMMAND}"
  -DCMAKE_INSTALL_COMPONENT=headers -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
  )

# Правила для установки
install(TARGETS ${current_target}_static ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
if(BUILD_SHARED_LIBS)
  install(TARGETS ${current_target}_shared LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})
endif()
install(FILES ${CMAKE_BINARY_DIR}/include/config.hpp ${current_target_headers}
  COMPONENT headers DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${current_target})
----

== Базовое приложение

В файле `cmex/CMakeLists.txt` должна быть строка, включающая поиск файла
`CMakeLists.txt` в подкаталоге `src/cmex`:

[source,cmake]
----
add_subdirectory(src/cmex)
----

В каталоге `cmex/src/cmex` нужно создать файл `main.cpp`:

[source,cpp]
----
#include "compiler_features.hpp"
#include "config.hpp"

#include <iostream>

#include "cmex.hpp"

int main(int argc, char **argv) {
    std::cout << CMEX_COMPILER_VERSION_MAJOR << std::endl;  // Значение из compiler_features.hpp
    std::cout << BUILD_TYPE << std::endl;                   // Значение из config.hpp
    std::cout << CMEX_VERSION_STR << std::endl;             // Значение из config.hpp
    std::cout << cmex_init(4) << std::endl;                 // Функция из внутренней библиотеки
    return 0;
}
----

и файл `CMakeLists.txt`:

[source,cmake]
----
# Название основной цели в текущем каталоге
set(current_target cmex_app)

# Список файлов исходных текстов
set(current_target_sources
  main.cpp
  )

# Цель для создания исполняемого файла
add_executable(${current_target} ${current_target_sources})
common_target_properties(${current_target})

# Зависимость от библиотеки из текущего проекта
add_dependencies(${current_target} cmex)

# Добавление внутреннего каталога src/libcmex к списку путей для поиска заголовочных файлов
target_include_directories(${current_target} PUBLIC
  $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/src/libcmex>)

# Имя выходного файла для цели (параметр OUTPUT_NAME)
set_target_properties(${current_target}
  PROPERTIES
  OUTPUT_NAME cmex
  RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}
  )

# Путь поиска библиотек внутри проекта
link_directories(${CMAKE_INSTALL_LIBDIR})

# Сначала внутренние статические библиотеки
target_link_libraries(${current_target} cmex_static)

# Правила для установки
install(TARGETS ${current_target} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
----

== Подключение внешнего проекта

В каталоге `cmex/thirdparty` нужно создать каталог `cmext` с проектом,
состоящим из файлов `cmext.hpp`:

[source,c]
----
#ifndef CMEXT_CMEXT_HPP_
#define CMEXT_CMEXT_HPP_

#include <stdint.h>

int32_t cmext_init(int32_t i);

#endif
----

`cmext.cpp`:

[source,c]
----
#include "cmext.hpp"

int32_t cmext_init(int32_t i = 0) {
    return i;
}
----

и `CMakeLists.txt`:

[source,cmake]
----
cmake_minimum_required(VERSION 3.3)
project(cmext)

include(GNUInstallDirs)
add_library(cmext cmext.cpp)
install(TARGETS cmext ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES cmext.hpp COMPONENT headers DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${CMAKE_PROJECT_NAME})
----

В файле `cmex/CMakeLists.txt` нужно подключить стандартный модуль
`ExternalProject` и описать правила для его загрузки, настройки,
компиляции и установки для сопряжения с текущим проектом:

[source,cmake]
----
# Подключение внешних проектов
include(ExternalProject)

ExternalProject_Add(cmext
  EXCLUDE_FROM_ALL  TRUE
  SOURCE_DIR        ${CMAKE_SOURCE_DIR}/thirdparty/cmext
  INSTALL_DIR       ${CMAKE_BINARY_DIR}
  DOWNLOAD_COMMAND  ""
  BUILD_BYPRODUCTS  <INSTALL_DIR>/lib/libcmext.a
  CMAKE_ARGS        -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_BUILD_TYPE=Release
  )
----

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

В файле `cmex/src/cmex/CMakeLists.txt` нужно подключить внешний проект
`cmext`:

[source,cmake]
----
# Зависимость от библиотеки из внешнего проекта проекта
add_dependencies(${current_target} cmext)
----

[source,cmake]
----
# Добавление каталога, в который устанавливаются заголовочные файлы от внешнего
# проекта cmext, к списку путей для поиска заголовочных файлов
target_include_directories(${current_target} PUBLIC
  $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/include/cmext>)
----

[source,cmake]
----
# Библиотека из внешнего проекта cmext
target_link_libraries(${current_target} ${CMAKE_BINARY_DIR}/lib/libcmext.a)
----

Для проверки работоспособности в файле `cmex/src/cmex/main.cpp` нужно
вызвать функцию `cmext_init` из библиотеки, предоставляемой внешним
проектом. Например:

[source,cpp]
----
#include "compiler_features.hpp"
#include "config.hpp"

#include <iostream>
#include <cmext/cmext.hpp>

#include "cmex.hpp"

QTextStream& qStdOut()
{
    static QTextStream ts(stdout);
    return ts;
}

int main(int argc, char **argv) {
        // Значение из compiler_features.hpp
        qStdOut() << QObject::tr("Compiler version: ") << CMEX_COMPILER_VERSION_MAJOR << endl;
        // Значение из config.hpp
        qStdOut() << QObject::tr("Project version: ")  << CMEX_VERSION_STR            << endl;
        // Значение из config.hpp
        qStdOut() << QObject::tr("Build type: ")             << BUILD_TYPE            << endl;
        // Функция из внутренней библиотеки
        qStdOut() << QObject::tr("libcmex function call: ")  << cmex_init(4)          << endl;
        // Функция из внешней библиотеки
        qStdOut() << QObject::tr("libcmext function call: ") << cmext_init(9)         << endl;

    return 0;
}
----

== Qt5

Для поиска необходимых компонентов Qt5 нужно в файл
`cmex/CMakeLists.txt` добавить строки:

[source,cmake]
----
find_package(Qt5 COMPONENTS Core Network Gui Widgets DBus Concurrent Sql REQUIRED)
----

Библиотека CMLib автоматически подключает вызов препроцессора `moc` и
компилятора ресурсов `rcc`, если цель использует модуль `Core`, и
вызывает компилятор файлов описания интерфейса, если цель использует
модуль `Widgets`.

== Консольное приложение

В файл `cmex/src/cmex/CMakeLists.txt` добавить строки:

[source,cmake]
----
# Qt5
qt_translation(TARGET ${current_target} TS_DIR ${CMAKE_SOURCE_DIR}/l10n LANGUAGES ru_RU)
target_include_directories(${current_target} SYSTEM PUBLIC ${Qt5Core_INCLUDE_DIRS})
target_compile_options(${current_target} PUBLIC "${Qt5Core_EXECUTABLE_COMPILE_FLAGS}")

target_link_libraries(${current_target} Qt5::Core)
----

Для проверки работоспособности подключения Qt5 файл
`cmex/src/cmex/main.cpp` нужно заменить на:

[source,cpp]
----
#include "compiler_features.hpp"
#include "config.hpp"

#include <QtCore>
#include <iostream>
#include <cmext/cmext.hpp>

#include "cmex.hpp"

QTextStream& qStdOut()
{
    static QTextStream ts(stdout);
    return ts;
}

int main(int argc, char **argv) {
    QCoreApplication app(argc, argv);
    QTranslator translator;

    if (translator.load(QLocale(), "cmex_app", QLatin1String("_"), QLatin1String(":/qm")))
    {
        app.installTranslator(&translator);
    }
    // Значение из compiler_features.hpp
    qStdOut() << QObject::tr("Compiler version: ") << CMEX_COMPILER_VERSION_MAJOR << endl;
    // Значение из config.hpp
    qStdOut() << QObject::tr("Project version: ")  << CMEX_VERSION_STR            << endl;
    // Значение из config.hpp
    qStdOut() << QObject::tr("Build type: ")             << BUILD_TYPE            << endl;
    // Функция из внутренней библиотеки
    qStdOut() << QObject::tr("libcmex function call: ")  << cmex_init(4)          << endl;
    // Функция из внешней библиотеки
    qStdOut() << QObject::tr("libcmext function call: ") << cmext_init(9)         << endl;

    return 0;
}
----

После сборки проекта в каталоге `cmex/l10n` появится файл
`cmex_app_ru_RU.ts`, в котором нужно отредактировать переводы с помощью
программы `linguist`. После сохранения переводов проект нужно
пересобрать, файл переводов в скомпилированном виде будет встроен в
исполняемый файл `cmex`, а доступ к нему будет осуществляться с помощью
кода:

[source,cpp]
----
    if (translator.load(QLocale(), "cmex_app", QLatin1String("_"), QLatin1String(":/qm")))
    {
        app.installTranslator(&translator);
    }
----

== Графическое приложение

Для создания минимального графического приложения нужно создать файл
описания интерфейса `cmex/src/cmex/my_main_window.ui`:

[source,xml]
----
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MyMainWindow</class>
 <widget class="QMainWindow" name="MyMainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>678</width>
    <height>415</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Main Window</string>
  </property>
  <widget class="QWidget" name="centralwidget"/>
 </widget>
 <resources/>
 <connections/>
</ui>
----

заголовочный файл `cmex/src/cmex/my_main_window.hpp`:

[source,cpp]
----
#ifndef CMEX_MY_MAIN_WINDOW_HPP_
#define CMEX_MY_MAIN_WINDOW_HPP_

#include <QWidget>
#include "ui_my_main_window.h"

class MyMainWindow : public QWidget, private Ui::MyMainWindow {
    Q_OBJECT
    public:
        MyMainWindow(QWidget* parent = 0);
        virtual ~MyMainWindow();
};

#endif /* CMEX_MY_MAIN_WINDOW_HPP_ */
----

и файл с реализацией конструктора и деструктора
`cmex/src/cmex/my_main_window.cpp`:

[source,cpp]
----
#include "my_main_window.hpp"

MyMainWindow::MyMainWindow(QWidget* parent) {

}

MyMainWindow::~MyMainWindow() {

}
----

Для отображения графического окна нужно заменить файл
`cmex/src/cmex/main.cpp` на:

[source,cpp]
----
#include "compiler_features.hpp"
#include "config.hpp"

#include <QtCore>
#include <QtWidgets>
#include <iostream>
#include <cmext/cmext.hpp>

#include "cmex.hpp"
#include "my_main_window.hpp"

QTextStream& qStdOut()
{
    static QTextStream ts(stdout);
    return ts;
}

int main(int argc, char **argv) {
    QApplication app(argc, argv);
    QTranslator translator;

    if (translator.load(QLocale(), "cmex_app", QLatin1String("_"), QLatin1String(":/qm")))
    {
        app.installTranslator(&translator);
    }

    // Значение из compiler_features.hpp
    qStdOut() << QObject::tr("Compiler version: ") << CMEX_COMPILER_VERSION_MAJOR << endl;
    // Значение из config.hpp
    qStdOut() << QObject::tr("Project version: ")  << CMEX_VERSION_STR            << endl;
    // Значение из config.hpp
    qStdOut() << QObject::tr("Build type: ")             << BUILD_TYPE            << endl;
    // Функция из внутренней библиотеки
    qStdOut() << QObject::tr("libcmex function call: ")  << cmex_init(4)          << endl;
    // Функция из внешней библиотеки
    qStdOut() << QObject::tr("libcmext function call: ") << cmext_init(9)         << endl;

    MyMainWindow* mmw = new MyMainWindow();
    mmw->show();
    return app.exec();
}
----

В файле `cmex/src/cmex/CMakeLists.txt` добавить новые файлы к списку
файлов, используемых для компиляции:

[source,cmake]
----
set(current_target_sources
  main.cpp
  my_main_window.cpp
  )

set(current_target_uis
  my_main_window.ui
  )
----

[source,cmake]
----
# Цель для создания исполняемого файла
add_executable(${current_target} ${current_target_sources} ${current_target_uis})
----

и добавить строки для подключения графических библиотек Qt5 и
соответствующих им заголовочных файлов:

[source,cmake]
----
target_include_directories(${current_target} SYSTEM PUBLIC ${Qt5Gui_INCLUDE_DIRS})
target_include_directories(${current_target} SYSTEM PUBLIC ${Qt5Widgets_INCLUDE_DIRS})
target_link_libraries(${current_target} Qt5::Gui)
target_link_libraries(${current_target} Qt5::Widgets)
----

Во время сборки проекта в файл переводов `cmex/l10n/cmex_app_ru_RU.ts`
будут добавлены новые строки, их нужно перевести с помощью `linguist` и
снова скомпилировать проект.

== Удаление установленных файлов

В библиотеку CMLib добавлена цель `uninstall`, позволяющая удалить
файлы, перечисленные в файле `${CMAKE_BUILD_DIR}/install_manifest.txt`.

== Архивирование проекта

Стандартный модуль `CPack` осуществляет архивирование проекта. В файле
`cproj/cmake/etc/cpack_ignore.txt` определён список типовых масок файлов
для исключения из архива:

....
.git$
files/var
CMakeLists.txt.user
~$
\\\\..*\\\\.tmp$
\\\\..*\\\\.bak$
\\\\..*\\\\.swp$
\\\\..*\\\\.o$
....

По умолчанию цель для упаковки проекта называется `package_source`. В
библиотеке CMLib определены значения основных параметров, а также
дополнительная цель `dist`.
