V
V
Vladimir Semenyakin2018-07-21 20:15:25
C++ / C#
Vladimir Semenyakin, 2018-07-21 20:15:25

How to properly organize a modular project using CMake?

I am writing a cross-platform project in C++. Since CMake is the de facto standard for cross-platform solutions, the project uses CMake. I used to add dependencies - third-party libraries - pre-assembling them manually in a separate folder and then copying them to the project structure or transferring third-party sources to the code directly. However, during the next assembly of the next version of the third-party library, I got tired - I decided to organize everything according to my mind, through the package mechanism (FIND_PACKAGE and others like it).
Sketched the desired project structure in the first iteration:

Project structure
packages_test
|-- .git
|-- build // Результат сборки
| |-- details // Папка для сборки, тут всякий временный хлам (make-файлы, .o-файлы, и проч.)
| |-- res.exe // Исполняемый файл
| |-- . . . // Ресурсы (необходимые .dll/.so, контент, и т.д.)
|-- CMakeLists.txt // Конфигурация сборки проекта
|-- main.cpp // Точка входа, main()
|-- . . . // Прочие файлы проекта
|--dependencies // Зависимости (подключаются через механизм submodule)
. |--vendor_package_0 // У каждой зависимости своя внутренняя организация
. |--vendor_package_1
. |-- . . .

I started writing a CMake file for a project, but ran into difficulties. Here is the current CMake file:
CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(playrix_entrance_test)
# ------------------------ Настройка зависимостей -----------------------------
# Переменная для хранения пути к папке, в которой лежат зависимости
set(DEPENDENCIES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dependencies")
# - - - - - - - - - - - - - Настройка библиотеки assimp - - - - - - - - - - - - - - -
# Тут может быть любая сторонняя библиотека. Указываю реально
# подключаемую чтобы подчеркнуть, что детали подключения каждой
# зависимости могут отличаться - минимум потому, что нет стандарта
# структуры проектов, использующих CMake
# Добавление папки с find-модулями CMake для assimp
# Это нужно чтобы find_package(...) смог извлечь информацию об assimp
list(APPEND CMAKE_MODULE_PATH "${DEPENDENCIES_DIR}/assimp/cmake-modules")
# Импорт переменных, описывающих пакет assimp. Данная команда
# создаёт всякие переменные с расположением заголовочных файлов пакета,
# расположением самого файла библиотеки ({.dll/.so} + .lib/.a), информацию
# о результате поиска пакета, и проч., однако не выполняет сборку пакета
find_package(assimp REQUIRED)
# Неизвестный код для сборки assimp
# ??? >>> add_subdirectory() <<< ???
# ??? >>> install() <<< ???
# - - - - - - - - - - - - - - - - -
# . . . - тут может быть код для подключения и сборки других сторонних библиотек
# ------------------------ Сборка проекта -----------------------------
add_executable(TestProject
"main.cpp")
# - - - - - - - - - - - - Подключение зависимостей - - - - - - - - - - - - -
# assimp
include_directories("${assimp_INCLUDE_DIR}")
target_link_libraries(TestProject assimp)

The question, in fact, is how to correctly configure the build of a third-party package within the build configuration of your project - if the third-party package is not built in advance. Judging by the variables provided by FIND_PACKAGE(...), it is assumed that the package should already be built, and the variables can be used to pass paths for linking and including header files.
Also, maybe I'm doing something wrong - I still have a poor understanding of CMake. I would be grateful if you share how it is customary to organize such things.
PS: In general, a "flat" project structure seems to be ideal. Something like this:
CMakeLists.txt
packages_test // Папка, содержащая все создаваемые и используемые пакеты
|-- project_package // Пакет проекта
| |-- .git
| |-- CMakeLists.txt // Конфигурация сборки пакета
| |-- main.cpp // Точка входа, main()
| |-- . . . // Прочие файлы проекта
| |-- product // Результат сборки пакета
| | |-- res.exe // Исходник
| | |-- . . . // Прочие необходимые ресурсы (необходимые .dll/.so, контент, и т.д.)
|--vendor_package_0 // Папка-пакет стороннего проекта
| |-- .git
| |-- product // Результат сборки, готовый к использованию ({.dll/.so} + .a/.lib + headers)
| |-- . . .
|--vendor_package_1
| |-- .git
| |-- product
| |-- . . .
|-- . . .

But I understand that to implement a build system that supports such a structure, it will take too much time - at least to write wrappers that adjust the build configuration of third-party projects to the one described.
PPS: Just in case, the platform on which I perform the test build is Windows, the toolchain is MinGW, the IDE is QtCreator.

Answer the question

In order to leave comments, you need to log in

1 answer(s)
4
4rtzel, 2018-07-22
@semenyakinVS

Since CMake allows you to get the same result in a bunch of different ways, I will try to describe the approach that I myself would use.
Structure:

packages_test
├── .git
├── cmake // Папка с доп. CMake скриптами если в этом есть необходимость
├── build // Результат сборки
│   └─ res.exe // Исполняемый файл
├── CMakeLists.txt // Конфигурация сборки проекта
├── src
│   ├─ main.cpp // Точка входа, main()
│   └─ ...// Прочие файлы проекта
├── dependencies // Зависимости (подключаются через механизм submodule)
│   ├─ vendor_package_0 // У каждой зависимости своя внутренняя организация
│   └─ vendor_package_1
└── test // Тесты
    ├── CMakeLists.txt
    └── src
        └── test_main.cpp

Now how are we going to add dependencies? It depends on where the libraries are located and how they are built. In your case, as I understand it, all vendor_package_x are CMake projects using add_library and obtained from git submodules. This means we can use add_subdirectory to import their targets.
Main CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(playrix_project VERSION 1.0 LANGUAGES CXX)
add_executable(playrix src/main.cpp)

# Импортируем наши зависимости. Это не приводит к сборке, но просто позволяет нам использовать target'ы этих проектов.
add_subdirectory(dependencies/vendor_package_0)
add_subdirectory(dependencies/vendor_package_1)

# Забудьте про include_directories и link_directories! В современном CMake следует использовать targets и properties.
# Опредеяем зависимости нашего проекта
target_link_libraries(playrix # Имя нашего executable'а
    PRIVATE # Определяет область видимости зависимостей для внешних проектов
        vendor_package_0_target # Настоящее имя target'а надо смотреть в vendor_package_0 CMakeLists.txt (add_library)
        vendor_package_1_target
)

target_link_libraries will take care of building all the dependencies, linking them (if needed) to our project, and including the necessary directories. Now you can build the project!
Additional links:
  • https://pabloariasal.github.io/2018/02/19/its-time...
  • https://rix0r.nl/blog/2015/08/13/cmake-guide/

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question