Bazel — это инструмент для сборки и тестирования с открытым исходным кодом, разработанный Google. Он предназначен для быстрого, надежного и эффективного создания программного обеспечения на разных платформах и языках. Он поддерживает многие языки программирования, включая C++, Java, Python, и использует уникальную систему сборки, использующую граф зависимостей для сборки и тестирования программных проектов, что помогает гарантировать, что сборки являются воспроизводимыми, инкрементальными и надежными. оптимизирован для параллельного выполнения.

Для меня ключевым преимуществом здесь является воспроизводимость слова, поскольку Bazel можно использовать для автоматической компиляции и использования зависимостей вашего проекта без необходимости их предварительной установки/настройки на вашем компьютере.

В этой статье будут рассмотрены основы использования Bazel для сборки и тестирования проектов C++. Далее последуют другие статьи, объясняющие более сложные сценарии с внешними зависимостями, которые мы также предварительно скомпилируем в Bazel, что избавит от необходимости их предварительной установки в вашей ОС.

Установка Базеля

Первым шагом к началу работы с Bazel является его установка в вашей системе. Bazel поддерживает различные платформы, включая Windows, macOS и Linux. С этого момента я предполагаю, что вы работаете в Linux (или WSL).

Веб-сайт Bazel предоставляет инструкции по установке для нескольких систем. Здесь я буду использовать Bazelisk и npm, так как считаю их самым простым методом. Сначала установите npm через apt, а затем bazel с помощью npm:

sudo apt update && sudo apt install npm
sudo npm install -g @bazel/bazelisk

Среда разработки

Чтобы использовать Bazel, вам нужно настроить среду разработки, которая включает два файла: WORKSPACE и BUILD, оба написаны на Starlark, специальном языке программирования, разработанном для Bazel, который очень похож на Python. В целях обучения наш репозиторий кодов будет выглядеть следующим образом, также доступен на GitHub:

bazel_tutorial/
   cc/
      my_lib/
         my_lib.cpp
         my_lib.hpp
      main.cpp
   BUILD
   WORKSPACE

Исходный код С++

Давайте приступим к созданию некоторых необходимых файлов C++, которые мы будем использовать для тестирования Bazel. Мы напишем простой класс (my_lib.hpp и my_lib.cpp) и main.cpp, который его использует.

//main.cpp

#include <iostream>
#include "cc/my_lib/my_lib.hpp"

int main() {
  MyClass obj;
  obj.setValue(5);
  std::cout << "Value: " << obj.getValue() << std::endl;
  return 0;
}
//my_lib.hpp

#ifndef MY_LIB_H
#define MY_LIB_H

class MyClass {
public:
  MyClass();
  void setValue(int val);
  int getValue();

private:
  int value;

};

#endif
//my_lib.cpp

#include "cc/my_lib/my_lib.hpp"

MyClass::MyClass() {
  value = 0;
}

void MyClass::setValue(int val) {
  value = val;
}

int MyClass::getValue() {
  return value;
}

Базель файлы

Файл WORKSPACE устанавливает среду и должен быть помещен в корень вашего проекта, среди прочего, он сообщает Bazel, где найти внешние зависимости и как с ними обращаться. Он также делает доступными правила сборки, своего рода API-интерфейсы компилятора, вам понадобятся некоторые правила для сборки C++, другие для Rust, еще некоторые для создания пакета DEB с вашей программой и т. д. Базовый файл WORKSPACE выглядит следующим образом:

workspace(name = "main")

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

# Setup rules_foreign_cc (CMake integration)
http_archive(
    name = "rules_foreign_cc",
    strip_prefix = "rules_foreign_cc-8d540605805fb69e24c6bf5dc885b0403d74746a", # 0.9.0
    url = "https://github.com/bazelbuild/rules_foreign_cc/archive/8d540605805fb69e24c6bf5dc885b0403d74746a.tar.gz",
)

load("@rules_foreign_cc//foreign_cc:repositories.bzl", "rules_foreign_cc_dependencies")

rules_foreign_cc_dependencies()

Файлы BUILD используются для указания того, как Bazel должен создавать и тестировать ваш проект. Они содержат информацию о целях (например, исполняемых файлах, библиотеках и тестах), которые вы хотите создать, а также о необходимых зависимостях и правилах. Для каждой цели в файле BUILD указаны исходные файлы, параметры компилятора и любая другая важная информация, необходимая Bazel для сборки программы.

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

cc_library(
    name = "my_lib",
    srcs = ["cc/my_lib/my_lib.cpp"],
    hdrs = ["cc/my_lib/my_lib.hpp"],
    visibility = ["//visibility:public"],
)

cc_binary(
    name = "main",
    srcs = ["cc/main.cpp"],
    deps = [
        "//:my_lib",
    ],
    visibility = ["//visibility:public"],
)

Сборка и тестирование

С приведенной выше настройкой (WORKSPACE + BUILD + Sources) вы можете собрать свой проект, выполнив следующую инструкцию в корневой папке:

bazel build //<relative-path-to-target>:<target>

<relative-path-to-target> — это путь к файлу BUILD, в котором определена ваша цель, относительно того, где находится файл WORKSPACE. В нашем примере один файл BUILD находится в той же папке, что и файл WORSPACE; поэтому <relative-path-to-target> пусто.

<target> — это имя цели (а не исходных файлов .h/.cpp), как определено в файле BUILD. В настоящее время у нашего проекта есть две цели: main и my_lib.

Если мы хотим скомпилировать нашу цель main, мы просто запускаем следующее:

bazel build //:main

Поскольку my_lib является зависимостью от main, он был автоматически создан и связан. Это сгенерирует несколько папок и, по сути, скомпилирует программу. Если все пойдет хорошо, вы можете запустить его со следующим:

bazel run //:main

Все файлы в рабочем состоянии доступны в этой папке GitHub.

Относительные пути

Теперь давайте внесем простое изменение в нашу структуру папок, чтобы продемонстрировать использование <relative-path-to-target>. Теперь я помещаю файл BUILD в папку cc, где находятся мои исходники:

bazel_tutorial/
   cc/
      my_lib/
         my_lib.cpp
         my_lib.hpp
      main.cpp
      BUILD
   WORKSPACE

Мы также должны обновить содержимое файла BUILD, чтобы отразить новые пути:

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

cc_library(
    name = "my_lib",
    srcs = ["my_lib/my_lib.cpp"],
    hdrs = ["my_lib/my_lib.hpp"],
    visibility = ["//visibility:public"],
)

cc_binary(
    name = "main",
    srcs = ["main.cpp"],
    deps = [
        "//cc:my_lib",
    ],
    visibility = ["//visibility:public"],
)

Обратите внимание, что я сделал два изменения:

  • При определении библиотеки я удалил cc из путей, так как теперь файлы библиотеки находятся всего в одной папке от того места, где находится файл BUILD.
  • Я добавил cc в раздел deps в cc_binary, так как теперь файл BUILD, определяющий цель my_lib, находится внутри папки cc относительно файла WORKSPACE.

Если мы выполним сборку bazel //:main, она завершится ошибкой, так как не сможет найти цель. Вот где появляется относительный путь, новые и правильные инструкции:

bazel build //cc:main
bazel run //cc:main

Вы можете проверить файловую структуру в этой папке GitHub.

Построение зависимостей

Как мы видели, наш проект имеет 2 цели: main и my_lib, обе определены в нашем единственном файле BUILD. Запустив bazel build //cc:main, мы автоматически создадим my_lib, так как он зависит от main. Однако, возможно, в некоторых сценариях нас интересует только создание зависимости или библиотеки. Мы также можем сделать это, вызвав его:

bazel build //cc:my_lib

И последнее замечание: со следующей опцией вы можете построить все в рабочей области, даже если цели не зависят друг от друга:

bazel build ...

Сборка с несколькими файлами BUILD

Сделаем последний поворот. Так как у нашей библиотеки есть своя папка, давайте зададим ей и свой файл BUILD. Наша структура папок теперь будет выглядеть следующим образом:

bazel_tutorial/
   cc/
      my_lib/
         my_lib.cpp
         my_lib.hpp
         BUILD
      main.cpp
      BUILD
   WORKSPACE

Новый файл BUILD будет выглядеть следующим образом:

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

cc_library(
    name = "my_lib",
    srcs = ["my_lib.cpp"],
    hdrs = ["my_lib.hpp"],
    visibility = ["//visibility:public"],
)

Нам нужно только один раз определить цель my_lib, поэтому давайте удалим ее из исходного файла BUILD, который теперь выглядит следующим образом:

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

cc_binary(
    name = "main",
    srcs = ["main.cpp"],
    deps = [
        "//cc/my_lib:my_lib",
    ],
    visibility = ["//visibility:public"],
)

Опять же, обратите внимание, как я также добавил относительный путь к библиотеке (где находится ее файл BUILD) в разделе «deps».

Теперь мы можем скомпилировать каждый из компонентов независимо или вместе со следующими инструкциями (с обновленными относительными путями):

bazel build //cc/my_lib:my_lib
bazel build //cc:main

Хотя сейчас добавление дополнительных файлов BUILD может показаться ненужным, оно станет полезным, когда ваши программы станут больше. При выполнении проекта с внесением изменений легче понять небольшие и автономные файлы BUILD, чем просто пройтись по одному файлу BUILD из 1000 строк.

Обновленные файлы в этой окончательной конфигурации также доступны на GitHub.

Заключение

Bazel — это мощный и гибкий инструмент, который хорошо подходит для проектов C++ любого размера. Выполнив шаги, описанные в этой статье, вы сможете приступить к работе с ним и приступить к созданию своих проектов C++. Независимо от того, работаете ли вы над небольшим проектом или обширным сложным программным обеспечением, Bazel может помочь вам в достижении ваших целей.

Это был всего лишь простой пример для закладки основ. Я намерен опираться на это, используя bazel для следующего:

  • Внешние зависимости в проекте Bazel: protobuf (скомпилированный с помощью Bazel) и ArrayFire (скомпилированный с помощью CMake в Bazel).
  • Пример использования расширенных внешних зависимостей: сборка OpenCV с поддержкой Contrib и VTK.
  • Создавайте выпуски: пакеты ZIP, TAR и DEB.
  • Расширение информации о процессе сборки и ценные инструкции по отладке.
  • Скомпилируйте программы на Rust с помощью Bazel.
  • Многоязычная поддержка: C++ с использованием библиотеки Rust.
  • Создайте расширения базы данных (PostgreSQL) из Bazel.