Как стать автором
Обновить
0

Сборка с Bazel в реальном проекте

Время на прочтение6 мин
Количество просмотров7.8K

Привет, Хабр.

В этой статье я расскажу о практическом опыте работы с Bazel, утилитой для автоматизации сборки и тестирования софта от Google. Мы, компания NtechLab, разрабатываем платформу видеоаналитики FindFace. Продукт большой и сложный, используется много разных языков программирования и библиотек, соответственно процесс сборки у нас громоздкий. В поисках инструмента, способного упростить и ускорить сборку, мы остановились на Bazel.

Что такое 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, ему требовалось проделать следующее:

  1. Внести изменения в код, собрать библиотеку. Уже на этом этапе были трудности: размер некоторых зависимостей сотни мегабайт, и упаковка в пакет с последующей установкой занимают далеко не секунду; делать же костыли в следующих этапах сборки вида “бери сейчас зависимость из этой build-директории, а не из /usr/…” или симлинкать /usr/… на артефакт из текущей сборки - во-первых, не очень удобно, а во-вторых, высок шанс что-то упустить.

  1. Собрать facenkit с обновленной версией библиотеки. Поправить код, если изменилось API, убедиться что все тесты проходят. Упаковать в пакет и установить его, либо воспользоваться костылем из п.1.

  2. И только теперь можно собирать video-worker, надеясь что нигде не была допущена ошибка и используются именно обновленные версии всех зависимостей.

  3. В случае проблем - исправить и повторить с п.1.

  4. Когда доволен результатом локально - обновить три репозитория (зависимость, facenkit, video-worker), прописав везде нужные (и одинаковые) версии.

Согласитесь, не слишком быстро и надежно для проектов, куда мы довольно часто вносим изменения, а когда сборка делается под разные ОС/платформы - все становится еще сложнее, ведь у каждой есть свои нюансы.

Как стало с Bazel

После перехода на сборку с помощью bazel мы расширили наш монорепозиторий (который стал теперь по настоящему моно-), и выглядеть он стал так:

.
├── WORKSPACE
├── facenkit/
├── sdk/
├── video-worker/
├── extraction-api/
└── third_party
    ├── opencv/
    ├── openvino/
    ├── tensorrt/
    ├── cuda/
    ├── jpeg/
    ├── png/
    └── ...

В файле WORKSPACE описываются внешние зависимости: указываются URL архива (можно использовать несколько зеркал) и его sha256 хэш-сумма, набор патчей и build_file, в котором описываются правила сборки:

...
http_archive(
    name = "opencv",
    build_file = "@//:third_party/opencv/BUILD.bazel",
    patch_args = ["-p1"],
    patches = [
        "@//:third_party/opencv/winbuild.patch",
    ],
    sha256 = "bb95acd849e458be7f7024d17968568d1ccd2f0681d47fd60d34ffb4b8c52563",
    strip_prefix = "opencv-4.4.0",
    urls = [
        "http://bazel-cache.int.ntl/cache/opencv-4.4.0.tar.gz",
        "https://github.com/opencv/opencv/archive/4.4.0.tar.gz",
    ],
)
...

В компонентах, для которых opencv является зависимостью, правило сборки может выглядеть так:

# создадим библиотеку для работы с изображениями, которая требует OpenCV
# в hdrs указываются экспортируемые заголовки
cc_library(
   name = "imgproc",
   srcs = ["lib/imgproc.cpp"],
   hdrs = ["lib/imgproc.h"],
   deps = [
     "@opencv//:opencv",
   ],
)

# тест для нее
cc_test(
    name = "imgproc-test",
    srcs = ["lib/imgproc_test.cpp"],
    deps = [
      ":imgproc",
    ],
)

# и бинарник, использующий эту библиотеку
cc_binary(
   name = "demo",
   srcs = ["src/demo.cpp"],
   deps = [
     ":imgproc",
   ],
)

С какими сложностями столкнулись

Основной сложностью было разобраться с новым инструментом и написать BUILD-файлы для наших сервисов и их зависимостей. Рассмотрим это на примере простой библиотеки с нативной bazel-сборкой и более сложного примера, где проще использовать cmake-wrapper.

bazel-way сборка {fmt}

Для библиотеки {fmt} все получилось очень просто:

WORKSPACE:

# string format for C++
http_archive(
    name = "com_github_fmtlib_fmt",
    build_file = "@//:third_party/com_github_fmtlib_fmt/BUILD.bazel",
    sha256 = "decfdf9ad274070fa85f26407b816f5a4d82205ae86bac1990be658d0795ea4d",
    strip_prefix = "fmt-7.0.3",
    urls = [
        "http://bazel-cache.int.ntl/cache/fmt-7.0.3.zip",
        "https://github.com/fmtlib/fmt/releases/download/7.0.3/fmt-7.0.3.zip",
    ],
)

Указываем, откуда скачивать исходники (в т.ч. локальное зеркало), sha256hash и где брать правило для сборки.

third_party/com_github_fmtlib_fmt/BUILD.bazel:

load("@rules_cc//cc:defs.bzl", "cc_library")

cc_library(
    name = "fmt",
    srcs = [
        "src/format.cc",
        "src/os.cc",
    ],
    hdrs = glob(["include/fmt/*.h"]),
    linkstatic = 1,
    strip_include_prefix = "include",
    visibility = ["//visibility:public"],
)

Создаем библиотеку из двух .cc файлов и экспортируем наружу публичный интерфейс через заголовки hdrs.

cmake сборка libjpeg-turbo

Для libjpeg-turbo написать нативный BUILD-файл хоть и возможно, но вариант с cmake-сборкой выглядит проще. Для более сложных библиотек, типа OpenCV или OpenVino cmake-сборка практически не имеет альтернативы, если у вас нет нескольких свободных недель.

WORKSPACE:

http_archive(
    name = "jpeg",
    build_file = "@//:third_party/jpeg/BUILD.bazel",
    sha256 = "16f8f6f2715b3a38ab562a84357c793dd56ae9899ce130563c72cd93d8357b5d",
    strip_prefix = "libjpeg-turbo-2.0.5",
    urls = [
        "http://bazel-cache.int.ntl/cache/libjpeg-turbo-2.0.5.tar.gz",
        "https://download.sourceforge.net/libjpeg-turbo/libjpeg-turbo-2.0.5.tar.gz",
    ],
)

Тут все аналогично примеру с библиотекой {fmt}.

third_party/jpeg/BUILD.bazel:

load("@rules_foreign_cc//tools/build_defs:cmake.bzl", "cmake_external")

filegroup(
    name = "all",
    srcs = glob(["**"]),
)

common_cache_entries = {
    "ENABLE_SHARED": "FALSE",
    "CMAKE_POSITION_INDEPENDENT_CODE": "TRUE",
    "WITH_JPEG8": "TRUE",
    "REQUIRE_SIMD": "TRUE",
    "CMAKE_INSTALL_DEFAULT_LIBDIR": "lib64",
}

cmake_external(
    name = "jpeg",
    cache_entries = common_cache_entries,
    cmake_options = ["-GNinja"],
    generate_crosstool_file = True,
    lib_source = ":all",
    make_commands = [
        "ninja",
        "ninja install",
    ],
    out_lib_dir = "lib64",
    static_libraries = select({
        "@//:linux_any": [
            "libjpeg.a",
            "libturbojpeg.a",
        ],
        "@//:windows_x86_64": [
            "jpeg-static.lib",
            "turbojpeg-static.lib",
        ],
    }),
    tools_deps = [
        "@nasm//:nasm",
    ],
    visibility = ["//visibility:public"],
)

Здесь используется 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) и монорепу - почему они к этому пришли, какие проблему решали и решили, кому это подходит, а кому нет.

Теги:
Хабы:
+3
Комментарии7

Публикации

Изменить настройки темы

Информация

Сайт
ntechlab.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия

Истории