En esta página, se abordan los beneficios y el uso básico de la configuración de Starlark, la API de Bazel para personalizar la forma en que se compila tu proyecto. Incluye cómo definir la configuración de compilación y proporciona ejemplos.
Esto permite hacer lo siguiente:
- definir marcas personalizadas para tu proyecto, lo que deja obsoleta la necesidad de
--define
- escribir transiciones para configurar dependencias en configuraciones diferentes a las de sus superiores (como
--compilation_mode=opt
o--cpu=arm
) - Generar mejores valores predeterminados en reglas (como compilar automáticamente
//my:android_app
con un SDK especificado)
y mucho más, todo desde archivos .bzl (no se requiere la versión de Bazel). Consulta el repositorio de bazelbuild/examples
para ver ejemplos.
Configuración de compilación definida por el usuario
Una configuración de compilación es una información de configuración única. Piensa en una configuración como un mapa de clave-valor. Cuando se establecen --cpu=ppc
y --copt="-DFoo"
, se produce una configuración similar a {cpu: ppc, copt: "-DFoo"}
. Cada entrada es una configuración de compilación.
Las marcas tradicionales, como cpu
y copt
, son opciones de configuración nativas; sus claves están definidas y sus valores se establecen dentro del código Java de Bazel nativo.
Los usuarios de Bazel solo pueden leerlos y escribirlos a través de la línea de comandos
y otras APIs que se mantienen de forma nativa. Para cambiar las marcas nativas y las APIs que las exponen, se requiere una versión de Bazel. La configuración de compilación definida por el usuario se define en archivos .bzl
(por lo tanto, no se necesita una versión de Bazel para registrar los cambios). También se pueden configurar mediante la línea de comandos (si están designados como flags
, consulta más información a continuación), pero también se pueden configurar mediante transiciones definidas por el usuario.
Cómo definir configuraciones de compilación
El parámetro build_setting
rule()
Las configuraciones de compilación son reglas como cualquier otra regla y se diferencian mediante el atributo build_setting
de la función rule()
de Starlark.
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
El atributo build_setting
toma una función que designa el tipo de configuración de la compilación. El tipo se limita a un conjunto de tipos básicos de Starlark, como bool
y string
. Consulta la documentación del módulo config
para obtener más detalles. Se puede realizar una escritura más complicada en la función de implementación de la regla. Sigue leyendo para obtener más información.
Las funciones del módulo config
toman un parámetro booleano opcional, flag
, que se establece en falso de forma predeterminada. Si flag
se establece en verdadero, los usuarios pueden establecer la configuración de la compilación en la línea de comandos, así como internamente por los escritores de reglas mediante valores predeterminados y transiciones.
Los usuarios no deben establecer todos los parámetros de configuración. Por ejemplo, si tú, como escritor de reglas, tienes algún modo de depuración que deseas activar dentro de las reglas de prueba, no querrás darles a los usuarios la capacidad de activar indiscriminadamente esa función dentro de otras reglas que no son de prueba.
Usa ctx.build_setting_value
Como todas las reglas, las reglas de configuración de compilación tienen funciones de implementación.
Se puede acceder al valor básico de tipo Starlark de la configuración de compilación a través del método ctx.build_setting_value
. Este método solo está disponible para objetos ctx
de reglas de configuración de compilación. Estos métodos de implementación pueden reenviar directamente el valor de configuración de compilación o realizar trabajos adicionales en él, como la verificación de tipo o la creación de structs más complejos. A continuación, se muestra cómo implementar una configuración de compilación de tipo enum
:
# example/buildsettings/build_settings.bzl
TemperatureProvider = provider(fields = ['type'])
temperatures = ["HOT", "LUKEWARM", "ICED"]
def _impl(ctx):
raw_temperature = ctx.build_setting_value
if raw_temperature not in temperatures:
fail(str(ctx.label) + " build setting allowed to take values {"
+ ", ".join(temperatures) + "} but was set to unallowed value "
+ raw_temperature)
return TemperatureProvider(type = raw_temperature)
temperature = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
Define marcas de cadena de varios conjuntos
La configuración de cadenas tiene un parámetro allow_multiple
adicional que permite que la
marca se establezca varias veces en la línea de comandos o en Bazelrcs. Su valor predeterminado todavía se establece con un atributo de tipo de cadena:
# example/buildsettings/build_settings.bzl
allow_multiple_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "allow_multiple_flag")
allow_multiple_flag(
name = "roasts",
build_setting_default = "medium"
)
Cada configuración de la marca se trata como un valor único:
$ bazel build //my/target --//example:roasts=blonde \
--//example:roasts=medium,dark
Lo anterior se analiza como {"//example:roasts": ["blonde", "medium,dark"]}
y ctx.build_setting_value
muestra la lista ["blonde", "medium,dark"]
.
Cómo crear instancias de la configuración de compilación
Las reglas definidas con el parámetro build_setting
tienen un atributo build_setting_default
obligatorio implícito. Este atributo toma el mismo tipo que se declaró por el parámetro build_setting
.
# example/buildsettings/build_settings.bzl
FlavorProvider = provider(fields = ['type'])
def _impl(ctx):
return FlavorProvider(type = ctx.build_setting_value)
flavor = rule(
implementation = _impl,
build_setting = config.string(flag = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
name = "favorite_flavor",
build_setting_default = "APPLE"
)
Configuración predefinida
La biblioteca Skylib incluye un conjunto de parámetros de configuración predefinidos de los que puedes crear instancias sin tener que escribir Starlark personalizado.
Por ejemplo, para definir una configuración que acepte un conjunto limitado de valores de cadena:
# example/BUILD
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
string_flag(
name = "myflag",
values = ["a", "b", "c"],
build_setting_default = "a",
)
Para obtener una lista completa, consulta Reglas comunes de configuración de compilación.
Cómo usar la configuración de compilación
Según la configuración de la compilación
Si un destino desea leer información de configuración, puede depender directamente de la configuración de compilación mediante una dependencia de atributo normal.
# example/rules.bzl
load("//example/buildsettings:build_settings.bzl", "FlavorProvider")
def _rule_impl(ctx):
if ctx.attr.flavor[FlavorProvider].type == "ORANGE":
...
drink_rule = rule(
implementation = _rule_impl,
attrs = {
"flavor": attr.label()
}
)
# example/BUILD
load("//example:rules.bzl", "drink_rule")
load("//example/buildsettings:build_settings.bzl", "flavor")
flavor(
name = "favorite_flavor",
build_setting_default = "APPLE"
)
drink_rule(
name = "my_drink",
flavor = ":favorite_flavor",
)
Es posible que los idiomas deseen crear un conjunto canónico de configuraciones de compilación del que dependan todas las reglas para ese idioma. Aunque el concepto nativo de fragments
ya no existe como un objeto codificado en el mundo de configuración de Starlark, una forma de traducirlo sería usar conjuntos de atributos implícitos comunes. Por ejemplo:
# kotlin/rules.bzl
_KOTLIN_CONFIG = {
"_compiler": attr.label(default = "//kotlin/config:compiler-flag"),
"_mode": attr.label(default = "//kotlin/config:mode-flag"),
...
}
...
kotlin_library = rule(
implementation = _rule_impl,
attrs = dicts.add({
"library-attr": attr.string()
}, _KOTLIN_CONFIG)
)
kotlin_binary = rule(
implementation = _binary_impl,
attrs = dicts.add({
"binary-attr": attr.label()
}, _KOTLIN_CONFIG)
Cómo usar la configuración de compilación en la línea de comandos
Al igual que en la mayoría de las marcas nativas, puedes usar la línea de comandos para establecer configuraciones de compilación que están marcadas como marcas. El nombre de la configuración de compilación es la ruta de destino completa que usa la sintaxis name=value
:
$ bazel build //my/target --//example:string_flag=some-value # allowed
$ bazel build //my/target --//example:string_flag some-value # not allowed
Se admite la sintaxis booleana especial:
$ bazel build //my/target --//example:boolean_flag
$ bazel build //my/target --no//example:boolean_flag
Usa alias de configuración de compilación
Puedes establecer un alias para la ruta de acceso de destino de tu configuración de compilación a fin de que sea más fácil de leer en la línea de comandos. Los alias funcionan de manera similar a las marcas nativas y también usan la sintaxis de opción de doble guion.
Para establecer un alias, agrega --flag_alias=ALIAS_NAME=TARGET_PATH
a tu .bazelrc
. Por ejemplo, para establecer un alias en coffee
, haz lo siguiente:
# .bazelrc
build --flag_alias=coffee=//experimental/user/starlark_configurations/basic_build_setting:coffee-temp
Práctica recomendada: Si configuras un alias varias veces, hará que el más reciente tenga prioridad. Usa nombres de alias únicos para evitar resultados de análisis no deseados.
Para usar el alias, escríbelo en lugar de la ruta de acceso de destino de la configuración de la compilación.
Con el ejemplo anterior de coffee
configurado en el .bazelrc
del usuario, ocurre lo siguiente:
$ bazel build //my/target --coffee=ICED
en lugar de
$ bazel build //my/target --//experimental/user/starlark_configurations/basic_build_setting:coffee-temp=ICED
Práctica recomendada: Si bien es posible configurar alias en la línea de comandos, dejarlos en un .bazelrc
reduce el desorden en la línea de comandos.
Configuración de compilación con tipo de etiqueta
A diferencia de otros parámetros de configuración de compilación, la configuración de tipo etiqueta no se puede definir con el parámetro de regla build_setting
. En cambio, Bazel tiene dos reglas integradas: label_flag
y label_setting
. Estas reglas reenvían a los proveedores del destino real al que se estableció la configuración de compilación. Las transiciones pueden leer o escribir label_flag
y label_setting
, y el usuario puede establecer label_flag
, al igual que otras reglas build_setting
. Su única diferencia es que
no pueden definirse de forma personalizada.
Con el tiempo, la configuración de tipo de etiqueta reemplazará la funcionalidad de los valores predeterminados de límite tardío. Los atributos predeterminados con límite tardío son atributos de tipo etiqueta cuyos valores finales pueden verse afectados por la configuración. En Starlark, esto reemplazará a la API de configuration_field
.
# example/rules.bzl
MyProvider = provider(fields = ["my_field"])
def _dep_impl(ctx):
return MyProvider(my_field = "yeehaw")
dep_rule = rule(
implementation = _dep_impl
)
def _parent_impl(ctx):
if ctx.attr.my_field_provider[MyProvider].my_field == "cowabunga":
...
parent_rule = rule(
implementation = _parent_impl,
attrs = { "my_field_provider": attr.label() }
)
# example/BUILD
load("//example:rules.bzl", "dep_rule", "parent_rule")
dep_rule(name = "dep")
parent_rule(name = "parent", my_field_provider = ":my_field_provider")
label_flag(
name = "my_field_provider",
build_setting_default = ":dep"
)
Configuración de compilación y select()
Los usuarios pueden configurar atributos en los parámetros de configuración de la compilación con select()
. Los objetivos de configuración de la compilación se pueden pasar al atributo flag_values
de config_setting
. El valor que debe coincidir con la configuración se pasa como un String
y, luego, se analiza según el tipo de configuración de compilación para la coincidencia.
config_setting(
name = "my_config",
flag_values = {
"//example:favorite_flavor": "MANGO"
}
)
Transiciones definidas por el usuario
Una transición de configuración asigna la transformación de un destino configurado a otro dentro del gráfico de compilación.
Definición
Las transiciones definen los cambios de configuración entre reglas. Por ejemplo, una solicitud como "compilar mi dependencia para una CPU diferente a la de su superior" se maneja mediante una transición.
De manera formal, una transición es una función de una configuración de entrada a una o más configuraciones de salida. La mayoría de las transiciones son 1:1, como "anular la configuración de entrada con --cpu=ppc
". También pueden existir transiciones 1:2+, pero tienen restricciones especiales.
En Starlark, las transiciones se definen de forma muy parecida a las reglas, con una función transition()
y una de implementación.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {"//example:favorite_flavor" : "MINT"}
hot_chocolate_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
La función transition()
toma una función de implementación, un conjunto de configuraciones de compilación para leer(inputs
) y un conjunto de configuraciones de compilación para escribir (outputs
). La función de implementación tiene dos parámetros: settings
y attr
. settings
es un diccionario {String
:Object
} de todas las opciones de configuración declaradas en el parámetro inputs
de transition()
.
attr
es un diccionario de atributos y valores de la regla a la que se adjunta la transición. Cuando se adjuntan como una transición perimetral saliente, todos los valores de estos atributos son todos una resolución post-select() configurada. Cuando se adjunta como una transición perimetral entrante, attr
no incluye ningún atributo que use un selector para resolver su valor. Si una transición perimetral entrante en --foo
lee el atributo bar
y, luego, también selecciona --foo
para establecer el atributo bar
, existe la posibilidad de que la transición perimetral entrante lea el valor incorrecto de bar
en la transición.
La función de implementación debe mostrar un diccionario (o una lista de diccionarios, en el caso de transiciones con varias configuraciones de salida) de valores de configuración de compilación nuevos que se aplicarán. Los conjuntos de claves del diccionario que se muestran deben contener exactamente el conjunto de configuraciones de compilación que se pasan al parámetro outputs
de la función de transición. Esto es así incluso si la configuración de una compilación no se cambia durante la transición; su valor original se debe pasar de forma explícita en el diccionario que se muestra.
Cómo definir transiciones de 1:2 o más
La transición perimetral de salida puede asignar una configuración de entrada única a dos o más configuraciones de salida. Esto es útil para definir reglas que agrupan código multiarquitectónica.
Las transiciones 1:2+ se definen cuando se muestra una lista de diccionarios en la función de implementación de transición.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return [
{"//example:favorite_flavor" : "LATTE"},
{"//example:favorite_flavor" : "MOCHA"},
]
coffee_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
También pueden configurar claves personalizadas que la función de implementación de reglas puede usar para leer dependencias individuales:
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {
"Apple deps": {"//command_line_option:cpu": "ppc"},
"Linux deps": {"//command_line_option:cpu": "x86"},
}
multi_arch_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
)
Cómo adjuntar transiciones
Las transiciones se pueden adjuntar en dos lugares: bordes entrantes y bordes salientes. Efectivamente, esto significa que las reglas pueden cambiar su propia configuración (transición perimetral entrante) y cambiar la configuración de sus dependencias (transición perimetral saliente).
NOTA: Actualmente, no hay forma de adjuntar transiciones de Starlark a las reglas nativas. Si necesitas hacer esto, comunícate con bazel-discuss@googlegroups.com para obtener ayuda para encontrar soluciones alternativas.
Transiciones perimetrales entrantes
Las transiciones perimetrales entrantes se activan cuando se adjunta un objeto transition
(creado por transition()
) al parámetro cfg
de rule()
:
# example/rules.bzl
load("example/transitions:transitions.bzl", "hot_chocolate_transition")
drink_rule = rule(
implementation = _impl,
cfg = hot_chocolate_transition,
...
Las transiciones perimetrales entrantes deben ser transiciones 1:1.
Transiciones de borde salientes
Las transiciones perimetrales salientes se activan adjuntando un objeto transition
(creado por transition()
) al parámetro cfg
de un atributo:
# example/rules.bzl
load("example/transitions:transitions.bzl", "coffee_transition")
drink_rule = rule(
implementation = _impl,
attrs = { "dep": attr.label(cfg = coffee_transition)}
...
Las transiciones de borde salientes pueden ser 1:1 o 1:2+.
Consulta Cómo acceder a los atributos con transiciones para obtener información sobre cómo leer estas claves.
Transiciones en opciones nativas
Las transiciones de Starlark también pueden declarar operaciones de lectura y escritura en opciones de configuración de compilación nativas mediante un prefijo especial en el nombre de la opción.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {"//command_line_option:cpu": "k8"}
cpu_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
Opciones nativas no compatibles
Bazel no admite la transición en --define
con "//command_line_option:define"
. En su lugar, usa una configuración de compilación personalizada. En general, no se recomienda el uso nuevo de --define
para dar lugar a las configuraciones de compilación.
Bazel no admite la transición en --config
. Esto se debe a que --config
es una marca de "expansión" que se expande a otras marcas.
Cabe destacar que --config
puede incluir marcas que no afectan la configuración de la compilación, como --spawn_strategy
. Por su diseño, Bazel no puede vincular esas marcas a destinos individuales. Esto significa que no hay una manera coherente de aplicarlos en las transiciones.
Como solución alternativa, puedes detallar explícitamente las marcas que forman parte de la configuración en tu transición. Esto requiere mantener la expansión de --config
en dos lugares, lo cual es una falla conocida de la IU.
Transiciones para permitir múltiples configuraciones de compilación
Cuando se establecen configuraciones de compilación que permiten varios valores, el valor de la configuración debe establecerse con una lista.
# example/buildsettings/build_settings.bzl
string_flag = rule(
implementation = _impl,
build_setting = config.string(flag = True, allow_multiple = True)
)
# example/BUILD
load("//example/buildsettings:build_settings.bzl", "string_flag")
string_flag(name = "roasts", build_setting_default = "medium")
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
# Using a value of just "dark" here will throw an error
return {"//example:roasts" : ["dark"]},
coffee_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//example:roasts"]
)
Transiciones no-ops
Si una transición muestra {}
, []
o None
, esto es una abreviatura para mantener toda la configuración en sus valores originales. Esto puede ser más conveniente que configurar explícitamente cada resultado para sí mismo.
# example/transitions/transitions.bzl
def _impl(settings, attr):
_ignore = (attr)
if settings["//example:already_chosen"] is True:
return {}
return {
"//example:favorite_flavor": "dark chocolate",
"//example:include_marshmallows": "yes",
"//example:desired_temperature": "38C",
}
hot_chocolate_transition = transition(
implementation = _impl,
inputs = ["//example:already_chosen"],
outputs = [
"//example:favorite_flavor",
"//example:include_marshmallows",
"//example:desired_temperature",
]
)
Cómo acceder a los atributos con transiciones
Cuando adjuntas una transición a un perímetro saliente (independientemente de si la transición es de 1:1 o de 1:2 o más), ctx.attr
se fuerza a ser una lista si aún no lo es. No se especifica el orden de los elementos en esta lista.
# example/transitions/rules.bzl
def _transition_impl(settings, attr):
return {"//example:favorite_flavor" : "LATTE"},
coffee_transition = transition(
implementation = _transition_impl,
inputs = [],
outputs = ["//example:favorite_flavor"]
)
def _rule_impl(ctx):
# Note: List access even though "dep" is not declared as list
transitioned_dep = ctx.attr.dep[0]
# Note: Access doesn't change, other_deps was already a list
for other_dep in ctx.attr.other_deps:
# ...
coffee_rule = rule(
implementation = _rule_impl,
attrs = {
"dep": attr.label(cfg = coffee_transition)
"other_deps": attr.label_list(cfg = coffee_transition)
})
Si la transición es 1:2+
y establece claves personalizadas, se puede usar ctx.split_attr
para leer dependencias individuales para cada clave:
# example/transitions/rules.bzl
def _impl(settings, attr):
_ignore = (settings, attr)
return {
"Apple deps": {"//command_line_option:cpu": "ppc"},
"Linux deps": {"//command_line_option:cpu": "x86"},
}
multi_arch_transition = transition(
implementation = _impl,
inputs = [],
outputs = ["//command_line_option:cpu"]
)
def _rule_impl(ctx):
apple_dep = ctx.split_attr.dep["Apple deps"]
linux_dep = ctx.split_attr.dep["Linux deps"]
# ctx.attr has a list of all deps for all keys. Order is not guaranteed.
all_deps = ctx.attr.dep
multi_arch_rule = rule(
implementation = _rule_impl,
attrs = {
"dep": attr.label(cfg = multi_arch_transition)
})
Consulta el ejemplo completo aquí.
Integración con plataformas y cadenas de herramientas
En la actualidad, muchas marcas nativas, como --cpu
y --crosstool_top
, están relacionadas con la resolución de la cadena de herramientas. En el futuro, es probable que las transiciones explícitas en estos tipos de marcas se reemplacen por transiciones en la plataforma de destino.
Consideraciones de memoria y rendimiento
Agregar transiciones (y, por lo tanto, configuraciones nuevas) a tu compilación tiene un costo: gráficos de compilación más grandes, gráficos de compilación menos comprensibles y compilaciones más lentas. Vale la pena tener en cuenta estos costos cuando consideres el uso de transiciones en tus reglas de compilación. A continuación, se muestra un ejemplo de cómo una transición podría crear un crecimiento exponencial de tu gráfico de compilación.
Compilaciones con comportamiento inadecuado: un caso de éxito
Figura 1: Grafo de escalabilidad que muestra un objetivo de nivel superior y sus dependencias.
En este gráfico, se muestra un objetivo de nivel superior, //pkg:app
, que depende de dos objetivos, //pkg:1_0
y //pkg:1_1
. Ambos objetivos dependen de dos objetivos, //pkg:2_0
y //pkg:2_1
. Ambos objetivos dependen de dos objetivos, //pkg:3_0
y //pkg:3_1
.
Esto continúa hasta //pkg:n_0
y //pkg:n_1
, que dependen de un solo destino, //pkg:dep
.
La compilación de //pkg:app
requiere \(2n+2\) objetivos:
//pkg:app
//pkg:dep
//pkg:i_0
y//pkg:i_1
para \(i\) en \([1..n]\)
Imagina que implement una marca --//foo:owner=<STRING>
y //pkg:i_b
se aplica
depConfig = myConfig + depConfig.owner="$(myConfig.owner)$(b)"
En otras palabras, //pkg:i_b
agrega b
al valor anterior de --owner
para todas sus dependencias.
Esto produce los siguientes destinos configurados:
//pkg:app //foo:owner=""
//pkg:1_0 //foo:owner=""
//pkg:1_1 //foo:owner=""
//pkg:2_0 (via //pkg:1_0) //foo:owner="0"
//pkg:2_0 (via //pkg:1_1) //foo:owner="1"
//pkg:2_1 (via //pkg:1_0) //foo:owner="0"
//pkg:2_1 (via //pkg:1_1) //foo:owner="1"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_0) //foo:owner="00"
//pkg:3_0 (via //pkg:1_0 → //pkg:2_1) //foo:owner="01"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_0) //foo:owner="10"
//pkg:3_0 (via //pkg:1_1 → //pkg:2_1) //foo:owner="11"
...
//pkg:dep
produce \(2^n\) objetivos configurados: config.owner=
“\(b_0b_1...b_n\)” para todo \(b_i\) en \(\{0,1\}\).
Esto hace que el gráfico de compilación sea exponencialmente más grande que el grafo de destino, con las consecuencias correspondientes de memoria y rendimiento.
PENDIENTE: Agrega estrategias para medir y mitigar estos problemas.
Lecturas adicionales
Para obtener más detalles sobre cómo modificar configuraciones de compilación, consulta:
- Configuración de compilación de Starlark
- Hoja de ruta de configuración de Bazel
- Conjunto completo de ejemplos de extremo a extremo