Skip to content

Development: target‐based CMake quick start guide

BUYT-1 edited this page Mar 24, 2026 · 11 revisions

Target-based Cmake quick start guide

Important

Это совсем упрощённый вариант, чтобы максимально быстро собрать и запустить новый код. Перед созданием PR обязательно разберитесь, как всё устроено, и причешите CMakeLists.

При возникновении любых проблем обращайтесь к документации CMake и определениям наших макросов (полезно прочитать документацию к ним, начать лучше с файла desbodante_helpers.cmake). Также можно спрашивать в чате.

Important

Текущая система сборки является черновым вариантом. Код вполне может не следовать этим рекомендациям, это ожидаемо. Не стоит просто копировать его, если вы хотя бы примерно понимаете, что происходит. Не стоит думать "тут так принято, поэтому я сделаю так же", потому что пока ещё ничего не принято. Если вы уверены, что можете сделать лучше, делайте и говорите об этом.

Important

Некоторые советы касательно стиля кода, приведённые в этом документе, отражают представление о прекрасном конкретных людей, не закреплённое никакими стайлгайдами. Не нужно воспринимать их как какие-то незыблемые правила.

Пример

В этом гайде мы сконфигурируем систему сборки для алгоритма Myner (My Miner) для майнинга AWD (Awesome Dependency). Мы уже написали весь код, и получилась следующая структура:

src
|- core
|  |...
|  `- algorithms
|     |- ...
|     `- awd
|        |- common
|        |  |- awesome_pli
|        |  |  |- awesome_pli.h
|        |  |  `- awesome_pli.cpp
|        |  `- awd_inducer
|        |     |- awd_inducer.h
|        |     `- awd_inducer.cpp
|        |- util
|        |  `- awd_util.h
|        |- myner
|        |  |- myner.h
|        |  `- myner.cpp
|        `- awd.h
|- python_bindings
|  |- ...
|  `- awd
|     |- bind_awd.h
|     |- bind_awd.cpp
|     |- bind_myner.h
|     `- bind_myner.cpp
`- tests
   |- ...
   `- test_myner.cpp

По возможности, в каждом разделе будет сначала приведена общая информация, а в конце -- как это применить к Myner.

Основные принципы

Итоговая библиотека теперь собирается из большого числа отдельных таргетов (см. add_library, add_executable). Таргеты бывают нескольких типов (здесь только те, которые интересны нам):

  • Интерфейсная библиотека add_library(<name> INTERFACE ...) -- не собирается. При указании в качестве зависимости, распространяет свои свойства (target_include_directories, target_link_libraries, etc.) на зависимые таргеты.
  • Объектная библиотека (add_library(<name> OBJECT ...)) -- собирается в набор объектных файлов, не линкуется. Используется исключительно для линковки к другим таргетам. Внутренние таргеты вашего алгоритма почти наверняка должны иметь этот тип.
  • Библиотека (add_library(<name> ...)) -- собирается в (обычную) библиотеку (в нашем случае, статическую, то есть .a-файл). Этот тип имеют те таргеты, которые мы хотим показать пользователю. В случае добавления алгоритма, его таргет должен иметь этот тип.
  • Исполняемый файл (add_executable(<name> ...)) -- собирается в исполняемый файл. При реализации алгоритма вам не понадобится, используется в макросе для добавления теста. Вам не нужно создавать такой таргет явно.

Для простоты (и для того, чтобы продемонстрировать больше различных таргетов) будем считать, что каждая директория, содержащая хотя бы один исходный файл (не заголовок), должна содержать и CMakeLists.txt. Другой вариант: иметь только один таргет на весь алгоритм, -- тоже приемлем, если Вы уверены, что код не понадобится разделять с другими алгоритмами. У нас пока нет правил на этот счёт, поэтому можно делать как Вам больше нравится.

Ещё одно важное правило: ваш CMakeLists.txt должен быть добавлен с помощью add_subdirectory из CMakeLists.txt выше по иерархии, иначе ваш код не будет собран. При этом, понятно, что каждый CMakeLists.txt должен включать с помощью этой команды только CMakeLists.txt в своих непосредственных поддиректориях (либо "внуков", если "дети" не содержат CMakeLists.txt), иначе получится хаос. То есть ожидается, что дерево CMakeLists.txt будет примерно соответствовать дереву директорий.

Пример

Для Myner нам нужно добавить следующие CMakeLists.txt (в скобках указан тип таргета, который добавляется непосредственно в нём):

src
`- core
   `- awd
      |- common
      |  |- awesome_pli
      |  |  `- CMakeLists.txt   (OBJECT)
      |  |- awd_inducer
      |  |  `- CMakeLists.txt   (OBJECT)
      |  `- CMakeLists.txt      (только add_subdirectory)
      |- util                   (нет CMakeLists.txt)
      |- myner
      |  `- CMakeLists.txt      (LIBRARY)
      `- CMakeLists.txt         (INTERFACE)

Кроме того, надо отредактировать уже имеющиеся CMakeLists.txt:

  • Добавить awd в SUBDIRS вверху src/core/algorithms/CMakeLists.txt (чтобы было вызвано add_subdirectory(awd))
  • Добавить некоторые таргеты в src/core/algorithms/CMakeLists.txt, src/python_bindings/CMakeLists.txt, src/tests/unit/CMakeLists.txt -- об этом чуть позже

Почему именно такие типы таргетов?

  • awesome_pli и awd_inducer -- это внутренние библиотеки, они будут линковаться к другим таргетам. Поэтому OBJECT.
  • common -- не содержит файлов, поэтому таргет не нужен. Можно в src/core/awd/CMakeLists.txt в add_subdirectory указать common/awesome_pli и common/awd_inducer вместо common, тогда этот CMakeLists.txt будет не нужен. Возможно, когда-нибудь в нашем стайлгайде появится указание, как лучше поступать.
  • util -- не содержит исходных файлов. Обратите внимание, что, без таргета, util.h вообще ни к чему не линкуется, и все его зависимости надо будет указывать явно во всех таргетах, где этот файл инклюдится. Здесь мы считаем, что util.h практически не имеет зависимостей, и используется только в одном-двух файлах по-соседству, поэтому такое поведение допустимо. Если нет, то нужно использовать INTERFACE-таргет (об этом чуть дальше).
  • myner -- это наш алгоритм, мы его отдаём пользователю. Поэтому LIBRARY.
  • awd -- здесь только заголовки, поэтому собирать нечего. Но, при этом, мы считаем, что awd.h содержит некоторые нетривиальные зависимости (например, boost или spdlog), кроме того, этот файл потенциально может понадобиться в самых разных местах (например, скорее всего, он инклюдится в байндингах). Поэтому мы сделаем здесь INTERFACE-таргет, в котором укажем все зависимости.

Core

В общем случае CMakeLists.txt для core-таргета (грубо говоря, всё, что находится в src/core) выглядит так:

# Рекурсивно включаем директории (по одной на каждый вызов макроса):
add_subdirectory(...)
...

# Здесь указывается имя таргета (должно быть уникальным по всему проекту):
set(NAME <Имя таргета>)
# Эта функция вызывает add_library с некоторыми специальными параметрами.
# Важно: после этого вызова содержимое переменной NAME меняется, поэтому везде нужно использовать именно ${NAME}, а не <Имя таргета>
desbordante_add_lib(NAME <Тип таргета>)
# Указываем исходники:
target_sources(
    ${NAME}
    # Здесь указываются только .cpp-файлы (за редким исключением, см. дальше)
    PRIVATE <Список исходников, через пробел>
)
# Линкуем зависимости:
target_link_libraries(
    ${NAME}
    # Здесь те зависимости, которые нужно экспортировать (например, если алгоритм использует в качестве параметра тип из них):
    PUBLIC <Public-зависимости, через пробел>
    # А здесь те, которые не нужно (какая-то чисто внутренняя история):
    PRIVATE <Private-зависимости, через пробел>
)

Имя таргета пишется через точку и после вызова desbordante_add_lib превращается в имя библиотеки, которое потом везде и указывается. Например, awd.common.awesome_pli превратится в Desbordante::awd::common::awesome_pli. По-хорошему имена таргетов образуют дерево, примерно соответствующее расположению CMakeLists.txt в дереве директорий.

<Тип таргета> не указывается для обычных библиотек, для объектных библиотек указывается OBJECT, для интерфейсных --- INTERFACE.

В target_sources нужно указать все исходники нашего таргета. Здесь указываются только .cpp-файлы (кроме тех таргетов, где только заголовки).

В target_link_libraries прописываем все зависимости. Наши зависимости пишутся в формате ${DESBORDANTE_PREFIX}::model::table, (условно) внешние -- так: Boost::headers spdlog::spdlog_header_only better-enums. Зависимости делятся на PUBLIC и PRIVATE. В нулевом приближении, в PUBLIC указываем зависимости заголовков, в PRIVATE -- исходников.

Основной таргет

Это таргет нашего алгоритма, тот, который мы отдаём пользователю (в примере это myner). Он всегда имеет тип LIBRARY. Его лучше называть как-нибудь понятно, например, Desbordante::awd::myner (за примерами смотрите этот список).

Пример

Для Myner основной таргет (это src/core/algorithms/awd/myner/CMakeLists.txt) будет выглядеть так:

# Очень рекомендуется основные таргеты называть именно так: <тип закономерности>.<алгоритм>
set(NAME awd.myner)
# Таргеты по-умолчанию имеют тип обычной библиотеки, его указывать не нужно
desbordante_add_lib(NAME)
target_sources(
    ${NAME}
    PRIVATE myner.cpp
)
target_link_libraries(
    ${NAME}
    # Предположим, что в myner.h есть такие строчки: `#include <boost/dynamic_bitset.h>`, `#include "core/algorithms/awd/awd.h"`
    PUBLIC ${DESBORDANTE_PREFIX}::awd Boost::headers
    # А эти зависимости используются только в myner.cpp
    # Обратите внимание на ${DESBORDANTE_PREFIX}::algos -- этот таргет содержит algorithm.h.
    # Если вместо него случайно прилинковаться к ${DESBORDANTE_PREFIX}::create_algo, то CMake будет ругаться на циклические зависимости
    PRIVATE ${DESBORDANTE_PREFIX}::awd::awesome_pli ${DESBORDANTE_PREFIX}::awd::awd_inducer
        ${DESBORDANTE_PREFIX}::algos spdlog::spdlog_header_only
)

Вспомогательные таргеты

По сути, это все остальные таргеты, которые лежат в src/core. Сюда попадают разного рода utility, интерфейсы и так далее. От основного таргета они отличаются тем, что мы их не хотим показывать пользователю (по крайней мере, это не является основной целью), поэтому они имеют типы OBJECT или INTERFACE.

Примеры
OBJECT

CMakeLists.txt для awesome_pli будет выглядеть следующим образом (для awd_inducer -- аналогично):

set(NAME awd.awesome_pli)
desbordante_add_lib(NAME OBJECT)
target_sources(
    ${NAME}
    PRIVATE awesome_pli.cpp
    PUBLIC ...
)
target_link_libraries(...)
INTERFACE

В awd нет исходников, поэтому мы используем INTERFACE-таргет:

add_subdirectory(common)
# util не указываем, поскольку там нет CMakeLists.txt
add_subdirectory(miner)
set(NAME awd)
desbordante_add_lib(NAME INTERFACE)
# Для INTERFACE параметры target_sources немного отличаются
target_sources(${NAME} INTERFACE FILE_SET HEADERS FILES awd.h BASE_DIRS "${PROJECT_SOURCE_DIR}/src")
# Вообще, объект зависимости должен быть максимально простым, и уж никак не должен зависеть от буста, но для примера предположим, что это так
# Очевидно, что здесь все зависимости должны быть PUBLIC
target_link_libraries(${NAME} Boost::headers)

src/core/algorithms/CMakeLists.txt

src/core/algorithms/CMakeLists.txt нужно изменить в двух местах:

  • добавить директории (в нашем случае это awd) в список SUBDIRS в самом начале
  • добавить наш основной таргет в список зависимостей для create_algo (в нашем случае туда добавится ${DESBORDANTE_PREFIX}::awd::myner) [NOTE: это ошибка в билд системе из-за кое-какого легаси, не ищите здесь глубокий смысл]

Bindings

Для байндингов всё гораздо проще -- они все добавляются в src/python_bindigs/CMakeLists.txt с помощью макроса desbordante_add_bind:

...
desbordante_add_bind(
    # Здесь обычно просто указывается название типа закономерности
    <Имя таргета для байндингов>
    # Здесь скорее всего будет <название типа закономерности>/<все .cpp-файлы>
    SRCS <Исходники байндингов>
    # Здесь будет наш основной таргет, и, возможно, что-нибудь ещё (скорее всего, понадобится Boost::headers)
    LIBS <Зависимости байндингов>
)
...

Пример

Для байндингов awd нужно будет добавить такой вызов макроса:

desbordante_add_bind(
    awd
    # Обычно все алгоритмы для майнинга привязываются в одном файле, аналогично для верификации, но делайте, как удобнее
    SRCS bind_awd.cpp
    # Здесь, как минимум, должны быть все библиотеки, типы из которых привязываются
    LIBS ${DESBORDANTE_PREFIX}::awd ${DESBORDANTE_PREFIX}::awd::myner
)

Tests

Тесты -- аналогично байндингам, только используется макрос desbordante_add_test:

...
desbordante_add_test(
    # Здесь указывается название типа закономерности или алгоритма -- смотря, что мы тестируем.
    # Если для одного типа закономерности есть несколько алгоритмов, тестовые таргеты тоже можно организовывать в дерево (так же через точку)
    <Имя таргета для тестов>
    # Здесь скорее всего будет test_<название типа закономерности или алгоритма>.cpp
    SRCS <Исходники тестов>
    # Здесь будет наш основной таргет, и, возможно, что-нибудь ещё.
    # gtest указывать не надо -- он уже указан для всех тестов. А вот gmock, если используете, -- надо.
    LIBS <Зависимости тестов>
)
...

У нас каждый файл в src/tests/unit -- это отдельный самостоятельный набор тестов, поэтому и все тестовые таргеты должны содержать ровно один файл в SRCS.

Пример

Тесты для Myner конфигурируются так:

desbordante_add_test(
    awd.myner
    SRCS test_myner.cpp
    # ${DESBORDANTE_PREFIX}::awd можно не указывать, так как отдельно awd для теста не имеет смысла --- мы тестируем не awd, тип AWD из неё мы используем только потому, что myner возвращает их в качестве результата
    LIBS ${DESBORDANTE_PREFIX}::awd::myner
)

Перед созданием PR

Перед созданием PR обязательно нужно причесать все CMakeLists.txt, которые мы создавали. Как минимум, нужно:

  • Убедиться, что везде указаны нужные зависимости, нигде не указано лишних, и они правильно разделены на PRIVATE и PUBLIC
  • Пройтись по всем файлам cmake-format. Если он даёт странные результаты, проверьте, что подтянулся конфиг из .cmake-format.yaml -- он иногда не обнаруживается автоматически

Не собирается с ошибкой "Файл не найден"!

Это значит, что у вас не подлинкована библиотека, от которой зависит ваш таргет.

Помимо простого варианта, где вы просто забыли её указать, это может быть также связано с тем, что какие-то таргеты-библиотеки, связываемые с вашиим таргетом, приватно линкуют свои библиотеки, которые должны быть слинкованы публично. Например, эти библиотеки могут использоваться в их хедерах, но линковаться приватно. Это ошибка.

Правильное решение --- исправить линковку. Быстрое решение --- связать свой таргет с библиотекой, на которую жалуется компилятор.