-
Notifications
You must be signed in to change notification settings - Fork 100
Development: 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-таргет, в котором укажем все зависимости.
В общем случае 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.
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(...)В 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 нужно изменить в двух местах:
- добавить директории (в нашем случае это
awd) в списокSUBDIRSв самом начале - добавить наш основной таргет в список зависимостей для
create_algo(в нашем случае туда добавится${DESBORDANTE_PREFIX}::awd::myner) [NOTE: это ошибка в билд системе из-за кое-какого легаси, не ищите здесь глубокий смысл]
Для байндингов всё гораздо проще -- они все добавляются в 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
)Тесты -- аналогично байндингам, только используется макрос 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 обязательно нужно причесать все CMakeLists.txt, которые мы создавали. Как минимум, нужно:
- Убедиться, что везде указаны нужные зависимости, нигде не указано лишних, и они правильно разделены на
PRIVATEиPUBLIC - Пройтись по всем файлам
cmake-format. Если он даёт странные результаты, проверьте, что подтянулся конфиг из.cmake-format.yaml-- он иногда не обнаруживается автоматически
Это значит, что у вас не подлинкована библиотека, от которой зависит ваш таргет.
Помимо простого варианта, где вы просто забыли её указать, это может быть также связано с тем, что какие-то таргеты-библиотеки, связываемые с вашиим таргетом, приватно линкуют свои библиотеки, которые должны быть слинкованы публично. Например, эти библиотеки могут использоваться в их хедерах, но линковаться приватно. Это ошибка.
Правильное решение --- исправить линковку. Быстрое решение --- связать свой таргет с библиотекой, на которую жалуется компилятор.