Привет, Хабр. В этой статье я расскажу о практическом опыте работы с Bazel, утилитой для автоматизации сборки и тестирования софта от Google. Мы, компания NtechLab, разрабатываем платформу видеоаналитики FindFace. Продукт большой и сложный, используется много разных языков программирования и библиотек, соответственно процесс сборки у нас громоздкий. В поисках инструмента, способного упростить и ускорить сборку, мы остановились на Bazel. Bazel - это утилита для автоматизации сборки и тестирования софта от Google. Первый релиз случился в далеком 2015 году, как opensource часть Blaze - внутренней системы сборки Google. Акцент сделан на скорость, корректность и воспроизводимость всех процессов - для этого поддерживается централизованное кэширование, удаленная сборка и изолирование каждой части. Для описания сборки используется Python-подобный язык skylark и Workspace правила, нативно поддерживаются C, C++, Go, Python, Java, Objective-C и Bourne-shell, но есть возможность сделать расширения и для других языков. Чтобы понять, какие у нас были проблемы и как мы их решили с переходом на Bazel, я расскажу про наши основные сервисы. Упрощенная схема их зависимостей выглядит так: extraction-api и video-worker - микросервисы, sdk - пользовательский продукт (shared library), facenkit - внутренняя библиотека для инференса нейронных сетей, opencv/openvino/tensorrt/cuda - внешние зависимости. На самом деле как микросервисов, так и их зависимостей намного больше, но в этой статье будем рассматривать упрощенный случай. Мы используем статическую линковку в финальные продукты. Во-первых, это позволяет не зависеть от особенностей конкретной версии при запуске. Во-вторых, это позволяет клиенту в своем приложении, которое обращается к нашем sdk , использовать ту же самую зависимость, но другой версии. Это стало бы проблемой при поставке в виде динамических библиотек. И, в-третьих, это необходимо для лучшей лицензионной защиты. Для каждой из внешних зависимостей у нас был отдельный репозиторий во внутреннем gitlab: зеркало фиксированной версии апстрима, наши патчи и .gitlab-ci.yml для воспроизводимой сборки и упаковки в пакет. Библиотека facenkit тоже находилась в отдельном репозитории, перед сборкой она  скачивались и устанавливались необходимые зависимости (opencv, tensorrt) фиксированной версии. И, наконец, при сборке “финального” микросервиса, например video-worker, нужно было скачать не только артефакт библиотеки facenkit, но и артефакты сборок всех ее зависимостей. Когда разработчик обновлял или вносил изменения в какую-либо зависимость, например в openvino, ему требовалось проделать следующее: Внести изменения в код, собрать библиотеку. Уже на этом этапе были трудности: размер некоторых зависимостей сотни мегабайт, и упаковка в пакет с последующей установкой занимают далеко не секунду; делать же костыли в следующих этапах сборки вида “бери сейчас зависимость из этой build-директории, а не из /usr/…” или симлинкать /usr/… на артефакт из текущей сборки - во-первых, не очень удобно, а во-вторых, высок шанс что-то упустить. Собрать facenkit с обновленной версией библиотеки. Поправить код, если изменилось API, убедиться что все тесты проходят. Упаковать в пакет и установить его, либо воспользоваться костылем из п.1. И только теперь можно собирать video-worker, надеясь что нигде не была допущена ошибка и используются именно обновленные версии всех зависимостей. В случае проблем - исправить и повторить с п.1. Когда доволен результатом локально - обновить три репозитория (зависимость, facenkit, video-worker), прописав везде нужные (и одинаковые) версии. Согласитесь, не слишком быстро и надежно для проектов, куда мы довольно часто вносим изменения, а когда сборка делается под разные ОС/платформы - все становится еще сложнее, ведь у каждой есть свои нюансы. После перехода на сборку с помощью bazel мы расширили наш монорепозиторий (который стал теперь по настоящему моно-), и выглядеть он стал так: В файле WORKSPACE описываются внешние зависимости: указываются URL архива (можно использовать несколько зеркал) и его sha256 хэш-сумма, набор патчей и build_file, в котором описываются правила сборки: В компонентах, для которых opencv является зависимостью, правило сборки может выглядеть так: Основной сложностью было разобраться с новым инструментом и написать BUILD-файлы для наших сервисов и их зависимостей. Рассмотрим это на примере простой библиотеки с нативной bazel-сборкой и более сложного примера, где проще использовать cmake-wrapper. Для библиотеки {fmt} все получилось очень просто: WORKSPACE: Указываем, откуда скачивать исходники (в т.ч. локальное зеркало), sha256hash и где брать правило для сборки. third_party/com_github_fmtlib_fmt/BUILD.bazel: Создаем библиотеку из двух .cc файлов и экспортируем наружу публичный интерфейс через заголовки hdrs. Для libjpeg-turbo написать нативный BUILD-файл хоть и возможно, но вариант с cmake-сборкой выглядит проще. Для более сложных библиотек, типа OpenCV или OpenVino cmake-сборка практически не имеет альтернативы, если у вас нет нескольких свободных недель. WORKSPACE: Тут все аналогично примеру с библиотекой {fmt}. third_party/jpeg/BUILD.bazel: Здесь используется rules_foreign_cc, который помогает делать сборку cmake и “configure && make” проектов. Для полного понимания всех параметров стоит обратиться к документации, но большинство из них понятны по имени. Пересборка только того, что нужно Благодаря тому, что Bazel точно знает полный граф всех зависимостей, он пересобирает только то, что действительно нужно. Например, если A зависит от B и C, C зависит от E, то при изменении кода/параметров сборки E при сборке A будут пересобраны E, C, A, а B будет взят из кэша. Быстрые тесты Предыдущий пункт применим и к тестам - даже если тесты ваших компонентов занимают 1 час, при их последующем запуске будет произведен анализ графа зависимостей, и будут перезапущены только заафекченные этим изменением тесты. Общие параметры сборки Хотите debug-сборку или сборку с санитайзерами? Всего одним флагом можно пересобрать все с нужными вам параметрами. Единая сборка под все системы Не важно, под какой дистрибутив Linux или Windows мы делаем сборку, x86_64 это или arm64 - правила сборки под всё описаны в одном месте, а платформо-специфичные вещи удобно интегрируются с помощью select(). Единая версия зависимостей Все зависимости всегда одной и той же версии - во всех компонентах и под все платформы, больше не нужно подстраиваться под libXXX-1.0 на старой LTS системе, когда нужные нам функции есть в свежем релизе libXXX-3.0. tensorflow/third_party - Bazel-рецепты сборок зависимостей TensorFlow. C++ Bazel Tutorial - раздел в официальной документации по Bazel с простым C++ туториалом. DL4AGX - проект от NVIDIA, который собирается с помощью Bazel, и в котором им собираются CUDA-kernels. Software Engineering at Google: Lessons Learned from Programming Over Time - как устроена разработка в Google, есть главы про Blaze (из которого родился Bazel) и монорепу - почему они к этому пришли, какие проблему решали и решили, кому это подходит, а кому нет. .