diff --git a/docs/article.md b/docs/article.md new file mode 100644 index 0000000..55e6859 --- /dev/null +++ b/docs/article.md @@ -0,0 +1,380 @@ +На кого нацелена статья: +- Backend разработчики +- C++ разработчики + +Цель: +Рассказать о том, как мы создали переиспользуемый шаблон API на C++. + +Задачи: +- Рассказать, почему мы решили попробовать написать шаблон API на C++. +- Рассказать, какие инструменты для разработки мы нашли и почему выбрали определённые. +- Рассказать, какой проект мы хотим реализовать в качестве шаблона. +- Рассказать, какие подводные камни нам встретились по ходу разработки. + +Темы, которые нужно осветить: +- Желание получить шаблон API на C++. +- Желание построить опыт работы с шаблоном подобно create-react-app. [!] +- Отсутствие готовых решений, статей и других ресурсов на поверхности. +- Потенциал для встраивания библиотек с другим рантаймом. [!] +- Простота использования. Получил бинарник -> используешь. [!] +- Полная власть над проектом, потенциал хорошего перформанса. +- Выбор пакетного менеджера, пакетный менеджер Conan. +- Выбор компилятора, компилятор Clang. +- Выбор фреймворка, фреймворк Drogon. +- Желание избежать прямых SQL-запросов, ORM ODB. +- Отсутствие conan-рецептов для libodb и libodb-pgsql. +- Отсутствие функционала для миграций базы данных, внедрение Alembic. + +[ --- ] + +# Взять и использовать или как мы упростили себе жизнь при разработке на C++ + +Представьте, что вы решили начать разработку нового API C++ бекенда и вместо того, чтобы создавать новый репозиторий, искать подходящие инструменты и настраивать всю инфраструктуру с нуля, нажимаете пару кнопок и шаблон для вашего будущего API с настроенной инфраструктурой уже готов. + +Представили? Мы тоже и ограничиваться одним только представлением не стали, поэтому всерьез задумались о воплощении такого шаблона в жизнь. К тому же, разработку вели в формате open-source, поэтому каждый из вас может просто [взять](https://github.com/TourmalineCore/to-dos-api-cpp) и [использовать](https://github.com/TourmalineCore/to-dos-api-cpp/blob/master/README.md) его для своих целей. + +С его помощью вы получаете готовую инфраструктуру API C++ бекенда: настроенную систему сборки под разные конфигурации, менеджер пакетов, библиотеку тестирования, пайплайн и миграции - остается лишь склонировать и попробовать, вдруг это подойдет и для вас. + +Но до этого давайте разберемся, как появилась идея создания подобного шаблона. Ранее мы уже занимались разработкой чего-то подобного и в закромах своего GitHub имеем несколько шаблонов, которые вместе реализуют приложение для управления задачами - To-Dos. Кстати, один из них использовался в качестве примера в докладе нашего руководителя Александра, который проводил [воркшоп](https://techtrain.ru/talks/20004151/) на тему TDD разработки на фронтенде. + +Мы решили реализовать что-то похожее, но для C++, чтобы в будущем не тратить время на рутину, такую как настройку сборки, конфигурацию окружения и т.п., а быстро стартовать проект, подобно create-react-app, когда одной команды в терминале достаточно, чтобы перед тобой появилась готовая структура проекта со всем необходимым. + +Наверное, каждый разработчик перед тем, как начать писать API backend на C++, задаётся вопросом - не делает ли он ошибку, выбирая такой язык, как C++, для реализации? Не будет ли правильнее взять что-то более современное и устоявшееся, как например .NET, NestJS или FastAPI? И если говорить начистоту, то в большинстве случаев сменить язык будет правильным выбором, но, несмотря на это, С++ имеет свои сильные стороны. + +Так, благодаря своей специфике, C++ предоставляет разработчику полный контроль над ресурсами и производительностью приложения, что открывает возможности для создания высоконагруженных API, где важны скорость и эффективность работы. Но не стоит забывать, что при этом приходится жертвовать скоростью разработки, поскольку сложность и вероятность совершить ошибку становятся существенно выше. + +Поэтому, если цель - это простой API с несложными выборками из базы данных, то C++, скорее всего, не ваш вариант. В этом случае можно присмотреться к готовым шаблонам на [.NET](https://github.com/TourmalineCore/to-dos-api-dot-net) или [NestJS](https://github.com/TourmalineCore/to-dos-api). Но если речь о проекте со сложной архитектурой, множеством интеграций и высокими требованиями к производительности, где важна тонкая настройка под конкретные сценарии, - это именно то, для чего C++ подходит лучше всего. + +В отличие от большинства технологий для задач бекенда, плюсом в пользу C++ можно считать результат компиляции. На выходе вы получаете самостоятельный бинарник, а не приложение, которому для запуска нужен установленный рантайм (вроде .NET runtime или Node.js) и набор зависимостей вокруг него. Благодаря этому не нужно следить, установлена на сервере нужная версия рантайма или нет, что уменьшает окно потенциальных ошибок и неопределенного поведения. Бинарник требует минимальное количество настройки, а иногда вовсе не требует. + +Дальше расскажем, как шаблон устроен и почему он выглядит именно так, а также через какие подводные камни нам пришлось пройти, чтобы каждый, кто его возьмет, мог уверенно стартовать свой проект, не уделяя время настройке необходимой инфраструктуры и не наступая на те же грабли. + +## Выбор инструментов + +Возникает вопрос выбора инструментов, которые будут использоваться для реализации, а здесь всё начинается с поиска и изучения того, что предлагает экосистема. + +Прежде всего мы решили посмотреть, что уже есть на эту тему - статьи, доклады, возможно, готовые проекты, которые могли бы упростить выбор и заранее подсветить подводные камни. Но довольно быстро стало понятно, что материалов на поверхности крайне мало. Статей и докладов о разработке бекенда на C++ оказалось меньше, чем мы рассчитывали, а open-source шаблоны подобного рода и вовсе редкость. + +Это, впрочем, не выглядело поводом для расстройства, а скорее стало дополнительным аргументом в пользу того, что тема слабо освещена, и наш шаблон, а также опыт его разработки могли оказаться действительно полезны, тем более что мы изначально планировали делать проект открытым и делиться этим опытом с другими. И опыт этот, прежде всего, об одной идее: нам, как разработчикам, хочется, чтобы работа была устроена легко и просто - это именно то, к чему мы стремились с самого начала. + +### Система сборки + +Но озвученная идея разбивается о реальность, как только речь заходит о сборке проекта, а в C++ это важная часть любого проекта. В отличие от JavaScript, Python и многих других языков, в комьюнити C/C++ так и не сложилось единого подхода, но из большинства CMake стал ближе всего к негласному стандарту, поскольку он кроссплатформенный, поддерживается практически всеми IDE и компиляторами, а большинство современных библиотек уже поставляются с готовой CMake-конфигурацией. Выбор здесь напрашивался сам собой. + +### Пакетный менеджер + +С пакетным менеджером история повторилась. В первую очередь мы выбирали между vcpkg и Conan. Остановились на Conan, поскольку он, в отличие от vcpkg, не ограничен связкой CMake и MSBuild, а одинаково хорошо работает с разными системами сборки и позволяет точно описывать окружение (компилятор, архитектуру, ОС, тип сборки и т.п.) через профили. Эта гибкость является для нас важным аспектом, ведь шаблон должен был одинаково собираться под разные архитектуры и операционные системы, а не быть заточен под одну конкретную конфигурацию. + +### Компилятор + +С компилятором повторилась похожая логика. MSVC отпал почти сразу, поскольку, как и в случае с vcpkg, не хотелось привязываться к конкретной платформе или IDE, а MSVC по сути жёстко связан с экосистемой Windows и Visual Studio. + +В выборе между GCC и Clang решающую роль сыграла экосистема. Clang поставляется в составе LLVM, который включает в себя и другие полезные инструменты, такие как clang-tidy и clang-format для статического анализа и форматирования кода, которые развиваются параллельно компилятору Clang и благодаря этому имеют хорошую совместимость, поэтому мы решили использовать его. + +### Веб-фреймворк + +В выборе веб-фремворка мы остановились на Drogon, поскольку, как нам показалось, у него низкий порог входа и понятный синтаксис. Из минусов можем отметить документацию, поскольку некоторые разделы содержат битые ссылки, что порой усложняет повествование и попытки найти нужный материал. + +Помимо Drogon, рассматривали Oat++ и userver. Oat++ показался нам более сложным и многословным по сравнению с Drogon. С userver ситуация другая, фреймворк отсутствует в `conan-center-index`, а его установка подразумевает самостоятельную сборку из репозитория разработчиков по их собственному шаблону, что не подходит нам. + +### ORM + +В качестве ORM системы изначально мы рассматривали те, что встроенны в фреймворки Drogon и Oat++, но в последствии отказались от этой идеи, поскольку они ориентированы на работу в формате SQL first, а не следуя паттерну Data Mapper, как нам хотелось. Кроме того, Drogon подразумевает, что база данных создаётся и наполняется таблицами вручную через SQL, и только после этого утилита drogon_ctl генерирует C++ классы моделей по уже существующей схеме, подключившись к базе. + +Также, в определенный момент рассматривали использование TinyORM, но быстро отказались из-за этой идеи, поскольку, как нам показалось, проект перестал поддерживаться, ведь последний коммит в репозитории был почти 2 года назад. + +В итоге мы остановились на ODB, поскольку, на наш взгляд, это более продвинутый инструмент, который позволяет работать с базой данных с помощью объектов и их методов, а не через SQL напрямую. + +Стоит отдельно упомянуть про лицензию. ODB распространяется под GPLv2, что подразумевает открытый исходный код. В случаях, когда проект имеет лицензию более свободную, чем GPL (например, Apache или MIT), есть возможность по запросу получить персональную и более свободную версию лицензии. Code Synthesis, разработчик ODB, также предлагает бесплатную проприетарную лицензию (FPL) для небольших проектов, её можно получить, если объём сгенерированного кода поддержки базы данных в рамках одного релиза приложения не превышает 10 000 строк (~10-20 классов моделей). Для более крупных закрытых проектов потребуется приобрести коммерческую проприетарную лицензию (CPL). + +### Миграции + +В момент реализации механизма применения миграций к базе данных мы еще не были знакомы с возможностями ODB в этой области. Как оказалось, с версии 2.3.0 ODB добавил поддержку ревизий схемы базы данных, включая генерацию миграций на основе изменений в моделях. Тогда же мы решили использовать отдельный инструмент - Alembic. Кроме того, мы увидели возможность показать в шаблоне пример того, как C++ API может взаимодействовать с инструментами из другого окружения, в нашем случае Python. + +Alembic показался нам хорошим выбором, поскольку реализовать логику применения миграций и описать модели с его помощью оказалось довольно просто, но пришлось завести отдельное представление модели на Python, которое повторяло структуру моделей ODB. + +## Внутреннее устройство шаблона + +Как всё это выглядит на практике? Начнём с общей структуры, а дальше подробнее остановимся на отдельных её частях. + +### Структура проекта + +```ini +.devcontainer/ # Конфигурация VSCode DevContainer +.github/ # Конфигурация пайплайна GitHub Actions +.vscode/ # Настройки редактора и сниппеты кода для VSCode +alembic/ # Конфигурация Alembic и список миграций +ci/ # Конфигурация для запуска API в кластере k8s +deps/ # Собственные Conan-рецепты зависимостей +docs/ # Документация проекта +profiles/ # Conan-профили под отдельные конфигурации сборки +scripts/ # Вспомогательные скрипты +src/ # Код приложения +unit/ # Unit тесты +e2e/ # E2E тесты +test_package/ # Служебный conanfile для проверки собранного Conan-пакета +.clang-format # Конфигурация автоформатирования кода +.clang-tidy # Конфигурация статического анализа кода +.env.example # Пример файла переменных окружения +.gitattributes # Настройки Git +.gitignore # Список файлов и директорий, игнорируемых Git +CMakeLists.txt # Корневой конфигурационный файл сборки +CMakeUserPresets.json # Пресеты CMake, генерируемые Conan +conanfile.py # Рецепт Conan-пакета приложения +docker-compose.yml # Конфигурация для запуска базы данных и других сервисов +Dockerfile # Многоступенчатая сборка продуктового образа +LICENSE # Лицензия проекта +Makefile # Таргеты Makefile для быстрых команд +README.md # Основная информация о проекте, инструкции +``` + +### Dev Container + +Директория `.devcontainer` содержит конфигурацию для разработки внутри Dev Container в VS Code. Сборка самого контейнера идёт на основе `Dockerfile`, который лежит в той же директории и устанавливает весь набор инструментов, таких как Clang, CMake, Conan и остальные. Там же, в `Dockerfile` производится копирование в `/root/.conan2/profiles/default` лежащего рядом Conan-профиля `to-dos-conan-profile.conf`, тем самым содержимое профиля устанавливается в значение по умолчанию, в результате Conan подхватывает его автоматически при любой сборке, без необходимости указывать профиль вручную через флаг. + +Чтобы не собирать зависимости и проект с нуля при каждом пересоздании контейнера, в `devcontainer.json` настроено кэширование через именованные Docker volumes. Отдельно кэшируется директория `.conan2` со скачанными и собранными Conan-пакетами и директория сборки `build`, где хранятся сгенерированные Conan файлы toolchain'а для CMake. Без этого кэширования пересборка контейнера с нуля означала и пересборку всех зависимостей заново, а это существенно увеличивает время сборки (в нашем случае более 20 минут). + +В том же `devcontainer.json` через хук `postCreateCommand` сразу после создания контейнера автоматически подключается локальное хранилище рецептов Conan, чтобы не приходилось выполнять эту команду вручную. + +Дополнительно мы использовали [Dev Container Feature](https://containers.dev/features) - `docker-outside-of-docker`, которая позволяет работать с Docker на хост-машине изнутри devcontainer, а не поднимать вложенный Docker внутри контейнера. Кроме того, её использование позволяет наблюдать запущенные контейнеры через Docker Desktop, что достаточно удобно. + +Чтобы контейнеры, поднятые на хосте, были доступны и по сети из самого devcontainer, в `runArgs` явно указан флаг `--network=host`, который подключает devcontainer к сети хост-машины напрямую, а не изолирует его в собственной Docker-сети. + +### Пакетные рецепты + +По умолчанию, устанавливая зависимость, Conan ищет её рецепт в собственном публичном индексе, который называется `conan-center-index`. Бывает так, что это хранилище не содержит нужного пакета, поэтому в Conan предусмотрена возможность подключения собственных источников рецептов, в дополнение к официальному индексу. + +Чтобы Conan смог увидеть локальные рецепты, директория, в которой находятся рецепты (в нашем случае `deps`), подключается как дополнительный локальный remote командой `conan remote add local-recipes ./deps --type=local-recipes-index`. После этого при установке зависимостей Conan берёт рецепты уже не из публичного индекса, а из локальной папки, собирает исходники под нужную конфигурацию и кладёт готовые бинарники в кэш, то есть точно так же, как если бы эти пакеты были частью `conan-center-index`. + +### Тесты + +В проекте реализовано два вида тестов: unit и E2E. + +Unit-тесты используют библиотеку [GTest](https://google.github.io/googletest/) и вынесены в отдельную директорию `unit` со своим `CMakeLists.txt`, в котором описана вся логика сборки тестового бинарника, он собирается как отдельный исполняемый файл, независимый от основного. Сборка тестов подключается в корневом `CMakeLists.txt` и может быть отключена переменной окружения `EXCLUDE_UNIT_TESTS_FROM_BUILD`, установленной в значение `true`. Также, сборка тестов не происходит при кросс-платформенной сборке, поскольку в этом случае не получится запустить сам исполняемый файл тестов на машине, где происходила сборка. + +> На данный момент в директории лежит только один unit-тест, который служит примером возможного теста. + +E2E-тесты реализованны с использованием библиотеки [Karate](https://docs.karatelabs.io/) и вынесены в отдельную директорию `e2e`. В случае E2E, вместо изолированного бинарника с тестами Karate выполняет HTTP-запросы к уже поднятому и работающему экземпляру API. Сценарий (тест) лежит в директории `e2e`, в файле `to-dos-happy-path.feature`, и последовательно проходит через все эндпоинты приложения, начиная от создания задачи, и заканчивая удалением и финальной проверкой, что задачи больше нет в базе. + +Поскольку Karate запускается не как часть сборки приложения, а как самостоятельный процесс, для него в той же директории `e2e` заведён отдельный `KarateDockerfile`, который использует за основу лёгкий образ с Java и скачанным `karate.jar`, не связанный с основным `Dockerfile` приложения. Такое разделение позволяет запускать e2e-тесты в двух разных окружениях: в docker-compose и в кластере k8s, где собранный образ приложения разворачивается через Helm. + +### CI-пайплайн + +Пайплайн состоит из двух воркфлоу, расположенных в директории `.github/workflows`. + +Первый воркфлоу, `.reusable-docker-build-and-push.yml`, отвечает за сборку и публикацию Docker-образа. Он выполняется в двух случаях: при пуше в master-ветку, тогда собирается и публикуется актуальный образ с тегом `latest`, либо по вызову из другого воркфлоу. Сама сборка происходит для двух архитектур, arm64 и amd64, затем оба образа объединяются в единый мультиархитектурный образ с помощью docker buildx. + +Второй воркфлоу, `e2e-tests-on-pull-request.yml`, запускается на каждый pull request и выполняет E2E-тесты. Выполнение тестов происходит в двух джобах, каждая из которых отвечает за собственное окружение. Первая джоба вызывает воркфлоу сборки образа API, дожидается публикации образа под конкретным коммитом и разворачивает его через Helm в тестовый кластер k8s, поднятый прямо в CI. Вторая джоба поднимает `docker-compose`, который собирает актуальный образ приложения и запускает его вместе с базой данных и контейнером Karate в общей Docker-сети. + +### Alembic и миграции + +Вся логика, связанная с миграциями, вынесена в отдельную директорию `alembic` в корне проекта, это было сделано осознанно, чтобы не смешивать ее с кодом самого приложения, то есть в стороне от `src`. + +Внутри лежит основной конфигурационный файл Alembic - `alembic.ini`, который в целом остался близким к стандартному, сгенерированному самим Alembic при инициализации. Внутри этого файла можно найти параметр `script_location`, который содержит путь к директории `migrations`, где Alembic будет искать сценарий окружения и сами файлы миграций. + +Внутри директории `migrations` находится `env.py`, который предназначен для настройки окружения при каждом запуске, также здесь происходит чтение переменных среды (`POSTGRES_HOST`, `POSTGRES_PORT` и так далее), содержащих данные необходимые для подключения к базе данных, само подключение, и, если базы ещё не существует, логика автоматического создания базы перед применением миграций. Там же лежит папка `versions` с самими миграциями (на данный момент там всего одна, начальная, создающая таблицу `todo`). + +Отдельно существует директория `models` с файлом `to_do.py`, который представляет собой Python-класс, основанный на SQLAlchemy и повторяющий структуру ODB-модели `ToDo` из C++ представления модели. Именно его `env.py` использует как `target_metadata`, когда при вызове `alembic revision --autogenerate` производит сравнение метаданных с текущим состоянием базы данных, на основе разницы которых Alembic генерирует новую миграцию. + +### Код приложения + +Весь код приложения находится в директории `src`. + +Если говорить об архитектуре, то мы выбрали для себя 3-х уровневую архитектуру, которая состоит из: +- `src/api` - Уровень представления +- `src/application` - Уровень бизнес-логики +- `src/core` - Уровень данных + +Такой подход: +- Помогает эффективно работать с обширным набором кода, поскольку разработчик понимает, где он должен искать ту или иную логику +- Разделяет приложение на отдельные части, каждая из которых выполняет свои функции, что позволяет вести разработку каждой из частей отдельно от других +- Ограничивает распространение изменений в коде, препятствуя возникновению ошибок, упрощая тестирование и поддержку кода + +Каждый из слоев собирается как отдельная статическая библиотека и линкуется в единый исполняемый файл в корневом `CMakeLists.txt`. Такая изоляция позволяет предотвратить построение взаимосвязей между слоями на уровне компиляции, поскольку обратиться к функционалу из другого слоя становится затруднительно. + +#### Уровень представления + +Представляет собой логический уровень взаимодействия API с пользователем. Этот слой не содержит бизнес-логики и не взаимодействует с уровнем данных напрямую, а только лишь через уровень бизнес-логики. Включает в себя контроллеры, выполняя функцию обработчика HTTP-запросов. + +#### Уровень бизнес-логики + +Представляет собой логический уровень выполнения бизнес-логики приложения. Код внутри сгруппирован по функционалу, где каждая директория в `application/features` описывает один конкретный сценарий использования (создание задачи, удаление и т.п.). + +Каждый такой сценарий имеет классы с чётко разделённой ответственностью. Command или Query отвечает непосредственно за обращение к базе данных, а Handler принимает Request, вызывает нужный Command или Query и формирует из результата Response. + +Также в `application` находится `db_connection`, представляющий собой обертку для получения общего подключения к базе данных, которое в последствии передаётся во все Command и Query классы, `shared-dtos` являются DTO, используемые сразу в нескольких сценариях, и `odb-gen`, который содержит сгенерированный ODB-компилятором код, обеспечивающий саму работу с базой на основе моделей, описанных в уровне данных. + +#### Уровень данных + +Представляет собой логический уровень, описывающий структуру данных приложения. Здесь находится класс, описывающий сущность `ToDo`, размеченную ODB-прагмами, которые определяют, как объект существует таблице в базы данных (поля, их типы и т.п.). На основе этих классов моделей ODB-компилятор генерирует код для уровня бизнес-логики. + +### Docker Compose + +В проекте есть `docker-compose.yml`, который описывает конфигурацию для запуска 4-х сервисов. Все они объединены в одну общую сеть, что позволяет сервисам обращаться друг к другу по имени. + +Сервис `to-dos-api-cpp-db` - это локальный PostgreSQL, который используется приложением в качестве базы данных. Для контейнера определен healthcheck, на статус которого ориентируются остальные сервисы, зависящие от базы. + +Сервис `to-dos-api-cpp-pgadmin` добавляет встроенное решение для просмотра базы данных. При старте pgAdmin автоматически подключается к базе данных, используя смонтированный файл `servers.json`, в котором заранее прописаны параметры подключения. + +Сервис `to-dos-api-cpp` использует в качестве основы тот же `Dockerfile`, что применяется и для сборки продуктового образа приложения, но с флагом `EXCLUDE_UNIT_TESTS_FROM_BUILD=true`, который исключает unit-тесты из сборки. У сервиса также переопределена точка входа, перед запуском самого приложения сначала применяются миграции через Alembic, и лишь после этого запускается API. Этот контейнер выступает целью для обращений сервиса `to-dos-api-cpp-karate-tests`. + +Сервис `to-dos-api-cpp-karate-tests` собирается на основе образа с JRE, необходимого для запуска библиотеки тестирования Karate, которая распространяется в виде обычного `.jar`-файла. Сам `karate.jar` скачивается при сборке образа. После старта контейнера запускается выполнение e2e-сценария `to-dos-happy-path.feature`, который делает запросы к контейнеру `to-dos-api-cpp` и завершает работу с кодом, соответствующим результату выполнения тестов. + +Конфигурация также содержит два профиля, позволяющих поднимать разный набор сервисов в зависимости от задачи: +- `DbOnly` - используется при локальной разработке и запускает только `to-dos-api-cpp-db` и `to-dos-api-cpp-pgadmin`; +- `MockForPullRequest` - используется для запуска e2e-тестов в среде docker compose в CI-пайплайне, в воркфлоу `e2e-tests-on-pull-request.yml`. + + +### Makefile + +С целью улучшения опыта разработки, в шаблон был добавлен Makefile. + +Makefile группирует в себе наборы команд, необходимых для выполнения различных сценариев, таких как применение миграций или запуск приложения. Такие сценарии именуются targets и могут быть вызваны как локально, так и из CI-пайплайна. Также, чаще всего утилита make, которая исполняет правила, описанные в Makefile, уже предустановлена в UNIX-подобные системы. + +Если возвращаться к проекту, то итоговы Makefile у нас выглядит так: +```makefile +# This is necessary so that environment variables from .env +# are visible when Makefile commands are executed +include .env +export + +# Generate a new Alembic migration with autogenerate +create-migration: + @cd ./alembic && \ + alembic revision --autogenerate -m $(name) + +# Apply all pending Alembic migrations +apply-migrations: + @cd ./alembic && \ + alembic upgrade head + +# Build the project, apply migrations and start the application +run: apply-migrations + @conan build . --build=missig && \ + ./build/Debug/to-dos-api + +# Run clang-tidy static analysis +run-tidy: + @run-clang-tidy -p build/Debug +``` + +### Профили и скрипты + +Хочется обратить внимание на некоторые тонкости, которые были внутри шаблона. + +Одной из улучшений было заимстованно с conan. Начнем с профилей. У Conan есть понятие [профилей](https://docs.conan.io/2/reference/config_files/profiles.html). Это по сути краткий свод правил в которые входит: какой компилятор использовать, флаги компилятора, версия C++ и многое другое. В Conan в документации почти каждый раз перед сборкой проекта рекомендует запустить `conan profile detect`, что определит профиль по вашей системе. Но так как мы хотели использовать clang в качестве компилятора с 20 версией C++, то появилась мысль написать свой профиль, так как по умолчанию Conan всегда находил gcc компилятор с какой-нибудь старой версией C++. + +```ini +[settings] +# Определяет систему автоматически через detect_api (например Linux, Windows, Macos) +os={{detect_api.detect_os()}} +# Определяет архитектуру автоматически через detect_api (например x86_64, armv8) +arch={{detect_api.detect_arch()}} +# Тип сборки +build_type=Debug +# Компилятор который будет использоваться +compiler=clang +# Версия компилятора Clang +compiler.version=14 +# Реализация стандартной библиотеки C++ +compiler.libcxx=libstdc++11 +# Стандарт C++ +compiler.cppstd=gnu20 + +[conf] +# Явно указывает, какие исполняемые файлы использовать для C и C++ компиляции +tools.build:compiler_executables={"c":"clang","cpp":"clang++"} +# Задаёт количество параллельных job равное числу доступных ядер процессора +tools.build:jobs={{os.cpu_count()}} +# Разрешает системному пакетному менеджеру (apt, brew и т.д.) автоматически устанавливать недостающие системные зависимости +tools.system.package_manager:mode=install +``` + +Тем самым мы жестко зафиксировали некоторые параметры сборки, но так же оставили гибкость профиля для работы на других системахю. Это потом помогло нам переиспользовать этот профиль так же в Github Actions, а не определять его там атоматически. + +Единственное, что необходимо сделать с профилем, это положить его в папку `/root/.conan2/profiles/default`. + +Далее во время использования ODB должны были каждый раз генерировать вспомогательные файлы моделей, для дальнейшей работы ODB runtime. + +Так как это действие происходило достаточно редко, только во время создания модели или ее редактирование, постоянно держать эту команду в уме становилось тяжко, а так же это буквально единственное применение CLI инструмента ODB, то мы вынесли ее использование в скрипт `scripts/generate_odb_files.sh`. + + + +### Первый камень + +Наверное один из самых насущных подводных камней оказалось отсутствие необходимых пакетов в `conan-center-index` (реестр пакетов Conan). В ходе поэтапного выбора необходимых инструментов внутри шаблона мы наконец-то подошли к выбору ORM. И самой, на наш взгляд, перспективной библиотекой ODB для работы с базой данных не оказалось в `conan-center-index`. + +И как бы мы не хотели, но просто нажать и запустить шаблон не вышло бы на любой машине. Да вы можете найти готовый бинарник для вашей системы и положить его рядом или внутрь проекта, но это бы означало уже достаточное колличество ручной подгонки, чего нам очень не хотелось. + +Самым правильным вариантом оказался, это добавить необходимы рецепт (так называют файл для сборки) в `conan-center-index`. Мы подготовили его и отправили на ревью мейнтейнерам конан индекса. + +Если хотите, чтобы опыт использования шаблона был мягким и приятным, то побалуйте нас своим лайком на issue [libodb](https://github.com/conan-io/conan-center-index/pull/30476) и [libodb-pgsql](https://github.com/conan-io/conan-center-index/pull/30477) тем самым вы приблизите публикацию пакета в `conan-center-index`. + +### Второй камень + +Как вы помните, нам важны были миграции в используемой ORM. Вышло что многие ORM не поддерживают ее из коробки. Даже если и поддерживают, то делают это как-то наоборот, сначала должна быть ревизия в базе данных, после которой ORM составляют миграции на ее основе (очень странная логика). Так как `libodb` нам по большим параметрам подходил, оставалось только закрыть потребность в миграциях. + +Именно поэтому миграции пришлось выносить в отдельный инструмент. В итоге для этой задачи был внедрён `Alembic` - официальный инструмент для `SQLAlchemy`, предназначенный именно для управления версиями схемы и применения миграций в базу данных. + +Порядок действий такой: +- Ставим Python и пакеты alembic, psycopg2-binary и sqlalchemy-utils +- Инициализируем alembic командой `alembic init migrations` +- Описываем модель, только уже на питоне +- И запускаем генерацию миграции `alembic revision --autogenerate -m "new migration"` +- И накатываем ее в базу при необходимости `alembic upgrade head` + + +И в целом такой опыт встраивания инструментов абсолютно другого стека может означать, что C++ шаблон можно подружить с любым инструментом или фреймворком. Хотя, конечно, в данном случае, это не совсем прямая интеграция. + +### Третий камень + +В этом шаблоне хотелось, не использовать CMake вовсе, либо свести его использование к минимуму, поэтому и был выбран conan. К сожалению, чем больше мы работали с Conan тем больше мы понимали что это заблуждение. Conan это именно пакетный менеджер, но никак не система сборки. В этом он все так же полагается на CMake/Meson и т.д. + +Так что было принято решение свести к минимуму работу с CMake. Для этого у Conan в функции generate есть возможность подготовить всё необходимое для дальнейшей работы CMake. В нашем случае мы использовали CMakeDeps и CMakeToolchain. Первый отвечает за генерацию файлов зависимостей, чтобы CMake мог корректно находить подключённые библиотеки, а второй за генерацию toolchain-файла с нужными параметрами сборки. + +```python +# Использование генераторов +def generate(self): + deps = CMakeDeps(self) + deps.generate() + + tc = CMakeToolchain(self, generator="Ninja") + tc.generate() +``` + +Оставшаяся ответственность для `CMakeLists.txt` оставалось только подключение библиотек из Conan. Для этого просто необходимо добавить несколько строк в `CMakeLists.txt`, которые можно взять со страницы любого пакета из [`conan-center-index`](https://conan.io/center/recipes/drogon?version=1.9.13) + +![Подключение пакетов в CMakeLists](conan_lib_connection.png) + +### Четвертый камень + +Когда мы принялись за сборку проекта на несколько платформ с разными архитектурами, то столкнулись с проблемой запуска тестов. Да тестов шаблоне можно сказать, что нету. Но они настроены и готовы к запуску, осталось их только написать. + +И при разработке с хостовой машины для хостового таргета, проблем нету. Проект собирается и тесты запускаются. А как только дело касается сборки проекта кросс-платформенно, то проект может и соберется через кросс-платформенные компиляторы, но тесты уже не запустятся, так как *сюрприз* они собраны для запуска на другой платформе. + +Думаем многие уже знают как решить эту проблему: запустить сборку в Github Actions без кросс-платформы, а сразу использовать таргетную машину и запускать тесты. + +В целом так оно и есть и оно работает. Но с какой скоростью это уже другой вопрос. + +### Пятый камень + +Скорость сборки при это существенно падала. При сборке локально, было приемлемо подождать один раз, пока Conan соберет исходники всех подключаемых библиотек и соберет их конкретно под машину, а потом использовать эти артефакты собрки дальше, без необходимости их пересобирать. А вот при сборке в Github Actions это происходит каждый запуск. И на сборку хотя бы двух архитектур (arm64 и x86-64) только на linux уходило около 30 минут. + +Немного подумав, мы пришли к решению, которое нас устраивало в локальной разработке — хранить артефакты сборки библиотек от Conan. Поскольку сборка идёт внутри Docker-образа, кешировать стали не саму папку `conan2/` на раннере, а Docker-слои через `cache-from/cache-to` с `mode=max`, отдельно для каждой архитектуры: + +``` +... +cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:cache-amd64-${{ env.BRANCH_NAME }} +cache-to: type=registry,ref=${{ env.REGISTRY_IMAGE }}:cache-amd64-${{ env.BRANCH_NAME }},mode=max +... +``` + +Для arm64-джобы используется отдельный тег `cache-arm64-${{ env.BRANCH_NAME }}`, чтобы кеши Conan-пакетов разных архитектур не смешивались. `mode=max` кеширует каждый слой Dockerfile отдельно, поэтому слой со сборкой Conan-зависимостей переиспользуется, пока не менялись conanfile и версии библиотек. + +Кеш при этом ещё и привязан к ветке через BRANCH_NAME. Благодаря этому разные ветки не перетирают чужой кеш друг у друга, а PR-сборки могут переиспользовать Conan-артефакты, собранные ранее в рамках той же ветки. + +## Вывод + +Мы начали с идеи о том, что запуск нового C++ API-проекта не должен превращаться в рутину, состоящей из настройки сборки, поиска подходящих библиотек, конфигурирование окружения с нуля и т.п.. Пройдя путь от выбора инструментов до столкновения с реальными подводными камнями, мы получили рабочий шаблон, который решает именно эту задачу, когда после нажатия пары кнопок у вас уже есть настроенная инфраструктура, тесты, CI и миграции. + +Надеемся, что опыт, который мы получили в процессе, может быть полезным не только нам, поэтому намеренно сделали проект открытым, так что если вы задумываетесь о старте C++ API-проекта, то попробуйте наш шаблон, а если найдёте, что улучшить, то будем рады issue или PR'у. \ No newline at end of file diff --git a/docs/conan_lib_connection.png b/docs/conan_lib_connection.png new file mode 100644 index 0000000..81e1d57 Binary files /dev/null and b/docs/conan_lib_connection.png differ