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

Типичный проект Bazel представляет собой дерево каталогов с файлом с именем WORKSPACE в корне дерева, который определяет внешние зависимости, и одним или несколькими файлами с именем BUILD (или BUILD.bazel), которые содержат объявления целей сборки. Больше информации и примеров можно найти здесь.

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

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
    name = "bazel_skylib",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.2.0/bazel-skylib-1.2.0.tar.gz",
        "https://github.com/bazelbuild/bazel-skylib/releases/download/1.2.0/bazel-skylib-1.2.0.tar.gz",
    ],
)

При этом используется правило http_archive из встроенной библиотеки bazel_tools, которую мы используем для импорта репозитория bazel_skylib.

Теперь мы можем добавить настройку сборки в наш проект в файле BUILD:

load("@bazel_skylib//rules:common_settings.bzl", "string_flag")

string_flag(
    name = "foo",
    build_setting_default = "42",
)

В моем проекте мне нужен был способ использовать значение этого параметра сборки в некотором коде C++. Здесь все начинает становиться немного сложнее. Во-первых, добавим код программы:

// test.cpp
#include <iostream>
using namespace std;
int main() {
    cout << FLAG << endl;
    return 0;
}

В настоящее время это не скомпилируется, если мы не добавим что-то в командную строку компилятора C, чтобы определить значение для FLAG. Мы могли бы сделать это в нашем файле BUILD, используя атрибут defines правила cc_binary:

cc_binary(
    name = "test",
    srcs = ["test.cpp"],
    defines = ["FLAG=99"],
)

Теперь нам нужно выяснить, как связать нашу настройку сборки со значением FLAG в нашем коде. Для этого мы собираемся определить пользовательское правило, используя язык bazel Starlark. Добавляем новый файл defs.bzl, содержащий реализацию правила:

# defs.bzl
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
def _define_from_flag_impl(ctx):
    return CcInfo(
        compilation_context = cc_common.create_compilation_context(
            defines = depset([
                "FLAG=\"{}\"".format(
                    ctx.attr.value[BuildSettingInfo].value,
                ),
            ]),
        ),
    )
define_from_flag = rule(
    implementation = _define_from_flag_impl,
    attrs = {
        "value": attr.label(),
    },
)

Разбивая вещи, мы определяем новое правило с именем define_from_flag, которое имеет единственный атрибут с именем value, который получает метку. Этот атрибут будет использоваться с меткой, ссылающейся на нашу настройку сборки. Для дальнейшего чтения взгляните на Учебник по правилам, в котором эта тема рассматривается более подробно.

В Starlark метки содержат набор поставщиков, которые описывают детали объекта с этой меткой. Для правила string_flag для bazel_skylib это включает BuildSettingInfo, который содержит значение флага — мы извлекаем его из метки и используем для форматирования параметра определения для FLAG. Последняя часть оборачивает это определение в поставщика CcInfo, который ведет себя как пустой cc_library, содержащий наше значение определения.

Теперь мы можем подключить это в нашем файле BUILD:

load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
load("defs.bzl", "define_from_flag")
cc_binary(
    name = "test",
    srcs = ["test.cpp"],
    deps = [":flag"],
)
define_from_flag(
    name = "flag",
    value = ":foo",
)
string_flag(
    name = "foo",
    build_setting_default = "42",
)

Это добавляет экземпляр define_from_flag в качестве зависимости от нашего cc_binary. Поскольку определения распространяются от библиотек к целям в зависимости от этих библиотек, это автоматически добавит -DFLAG=”42" в командную строку компилятора.

Наконец, нам нужно выяснить, как изменить значение параметра сборки. К счастью, это легко — значения флагов можно изменить с помощью командной строки bazel:

$ bazel run:test --//foo=hello
...
hello

Мы можем сделать это немного более удобным для пользователя, определив псевдоним в .bazelrc:

# .bazelrc
build --flag_alias=foo=//:foo

Весь код этого примера находится на гитхабе здесь.