Bzlmod es el nombre en clave del nuevo sistema de dependencias externas que se introdujo en Bazel 5.0. Se introdujo para abordar varios puntos débiles del sistema anterior que no se podían corregir de forma incremental; consulta la sección Declaración del problema del documento de diseño original para obtener más detalles.
En Bazel 5.0, Bzlmod no está activado de forma predeterminada; se debe especificar la marca
--experimental_enable_bzlmod para que surta
efecto lo siguiente. Como sugiere el nombre de la marca, esta función se encuentra actualmente en fase experimental;
las APIs y los comportamientos pueden cambiar hasta que se lance oficialmente la función.
Para migrar tu proyecto a Bzlmod, sigue la Guía de migración de Bzlmod. También puedes encontrar ejemplos de usos de Bzlmod en el repositorio de ejemplos.
Módulos de Bazel
El antiguo sistema de dependencias externas basado en WORKSPACE se centra en
repositorios (o repos), creados a través de reglas de repositorio (o reglas de repo).
Si bien los repositorios siguen siendo un concepto importante en el nuevo sistema, módulos son las
unidades principales de dependencia.
Un módulo es, básicamente, un proyecto de Bazel que puede tener varias versiones, cada una de las cuales publica metadatos sobre otros módulos de los que depende. Esto es análogo a los conceptos familiares de otros sistemas de administración de dependencias: un artefacto de Maven, un paquete de npm, un crate de Cargo, un módulo de Go, etcétera.
Un módulo simplemente especifica sus dependencias con pares name y version,
en lugar de URLs específicas en WORKSPACE. Luego, las dependencias se buscan en
un registro de Bazel; de forma predeterminada, el
registro central de Bazel. En tu espacio de trabajo, cada
módulo se convierte en un repositorio.
MODULE.bazel
Cada versión de cada módulo tiene un archivo MODULE.bazel que declara sus
dependencias y otros metadatos. Este es un ejemplo básico:
module(
name = "my-module",
version = "1.0",
)
bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")
El archivo MODULE.bazel debe ubicarse en la raíz del directorio del espacio de trabajo
(junto al archivo WORKSPACE). A diferencia del archivo WORKSPACE, no es necesario que especifiques tus dependencias transitivas. En su lugar, solo debes especificar las dependencias directas, y los archivos MODULE.bazel de tus dependencias se procesan para descubrir dependencias transitivas automáticamente.
El archivo MODULE.bazel es similar a los archivos BUILD, ya que no admite ninguna
forma de flujo de control; además, prohíbe las instrucciones load. Las directivas
MODULE.bazel que admiten los archivos son las siguientes:
module, para especificar metadatos sobre el módulo actual, incluidos su nombre, versión, etcétera.bazel_dep, para especificar dependencias directas en otros módulos de Bazel.- Anulaciones, que solo puede usar el módulo raíz (es decir, no un módulo que se usa como dependencia) para personalizar el comportamiento de una dependencia directa o transitiva determinada:
- Directivas relacionadas con las extensiones de módulo:
Formato de versión
Bazel tiene un ecosistema diverso y los proyectos usan varios esquemas de control de versiones. El
más popular es SemVer, pero también hay
proyectos destacados que usan esquemas diferentes, como
Abseil, cuyas
versiones se basan en la fecha, por ejemplo, 20210324.2).
Por este motivo, Bzlmod adopta una versión más relajada de la especificación de SemVer. Las diferencias incluyen lo siguiente:
- SemVer prescribe que la parte "release" de la versión debe constar de 3
segmentos:
MAJOR.MINOR.PATCH. En Bazel, este requisito se relaja para que se permita cualquier cantidad de segmentos. - En SemVer, cada uno de los segmentos de la parte "release" debe ser solo dígitos. En Bazel, esto se relaja para permitir también letras, y la semántica de comparación coincide con los "identificadores" de la parte "prerelease".
- Además, no se aplican las semánticas de los aumentos de versión principal, secundaria y de parche. (Sin embargo, consulta el nivel de compatibilidad para obtener detalles sobre cómo denotamos la compatibilidad con versiones anteriores).
Cualquier versión válida de SemVer es una versión válida del módulo de Bazel. Además, dos
versiones de SemVer a y b comparan a < b si se cumple lo mismo cuando se
comparan como versiones del módulo de Bazel.
Resolución de versiones
El problema de la dependencia de diamante es un elemento básico en el espacio de administración de dependencias con versiones. Supongamos que tienes el siguiente gráfico de dependencias:
A 1.0
/ \
B 1.0 C 1.1
| |
D 1.0 D 1.1
¿Qué versión de D se debe usar? Para resolver esta pregunta, Bzlmod usa el algoritmo de selección de versión mínima (MVS) que se introdujo en el sistema de módulos de Go. MVS supone que todas las versiones nuevas de un módulo son compatibles con versiones anteriores y, por lo tanto, simplemente elige la versión más alta especificada por cualquier dependiente (D 1.1 en nuestro ejemplo). Se llama "mínima" porque D 1.1 aquí es la versión mínima que podría satisfacer nuestros requisitos; incluso si existe D 1.2 o una versión más reciente, no las seleccionamos. Esto tiene el beneficio adicional de que la selección de versiones es de alta fidelidad y reproducible.
La resolución de versiones se realiza de forma local en tu máquina, no en el registro.
Nivel de compatibilidad
Ten en cuenta que la suposición de MVS sobre la compatibilidad con versiones anteriores es factible porque la simplemente trata las versiones incompatibles con versiones anteriores de un módulo como un módulo independiente. En términos de SemVer, eso significa que A 1.x y A 2.x se consideran módulos distintos, y pueden coexistir en el gráfico de dependencias resuelto. A su vez, esto es posible gracias a que la versión principal está codificada en la ruta de acceso del paquete en Go, por lo que no hay conflictos en el tiempo de compilación ni en el tiempo de vinculación.
En Bazel, no tenemos esas garantías. Por lo tanto, necesitamos una forma de denotar el número de "versión
principal" para detectar versiones incompatibles con versiones anteriores. Este número
se denomina nivel de compatibilidad y se especifica en cada versión del módulo en
su module() directiva. Con esta información, podemos generar un error
cuando detectamos que existen versiones del mismo módulo con diferentes niveles de compatibilidad
en el gráfico de dependencias resuelto.
Nombres de repositorios
En Bazel, cada dependencia externa tiene un nombre de repositorio. A veces, la misma
dependencia se puede usar a través de diferentes nombres de repositorio (por ejemplo, tanto
@io_bazel_skylib como @bazel_skylib significan
Bazel skylib), o el mismo
nombre de repositorio se puede usar para diferentes dependencias en diferentes proyectos.
En Bzlmod, los repositorios pueden generarse con módulos de Bazel y extensiones de módulos. Para resolver conflictos de nombres de repositorios, adoptamos el mecanismo de asignación de repositorios en el nuevo sistema. Estos son dos conceptos importantes:
Nombre de repositorio canónico: Es el nombre de repositorio único a nivel global para cada repositorio. Este será el nombre del directorio en el que reside el repositorio.
Se construye de la siguiente manera (Advertencia: El formato de nombre canónico no es una API de la que debas depender, ya que está sujeto a cambios en cualquier momento):- Para los repositorios de módulos de Bazel:
module_name~version
(Ejemplo.@bazel_skylib~1.0.3) - Para los repositorios de extensiones de módulos:
module_name~version~extension_name~repo_name
(Ejemplo.@rules_cc~0.0.1~cc_configure~local_config_cc)
- Para los repositorios de módulos de Bazel:
Nombre de repositorio aparente: Es el nombre de repositorio que se usará en los archivos
BUILDy.bzldentro de un repositorio. La misma dependencia podría tener diferentes nombres aparentes en diferentes repositorios.
Se determina de la siguiente manera:
Cada repositorio tiene un diccionario de asignación de repositorios de sus dependencias directas,
que es un mapa del nombre de repositorio aparente al nombre de repositorio canónico.
Usamos la asignación de repositorios para resolver el nombre del repositorio cuando construimos una
etiqueta. Ten en cuenta que no hay conflicto de nombres de repositorios canónicos, y los
usos de nombres de repositorios aparentes se pueden descubrir analizando el MODULE.bazel
archivo, por lo que los conflictos se pueden detectar y resolver fácilmente sin afectar
otras dependencias.
Dependencias estrictas
El nuevo formato de especificación de dependencias nos permite realizar verificaciones más estrictas. En particular, ahora aplicamos que un módulo solo puede usar repositorios creados a partir de sus dependencias directas. Esto ayuda a evitar interrupciones accidentales y difíciles de depurar cuando cambia algo en el gráfico de dependencias transitivas.
Las dependencias estrictas se implementan en función de la asignación de repositorios. Básicamente, la asignación de repositorios para cada repositorio contiene todas sus dependencias directas, y no se puede ver ningún otro repositorio. Las dependencias visibles para cada repositorio se determinan de la siguiente manera:
- Un repositorio de módulos de Bazel puede ver todos los repositorios introducidos en el
MODULE.bazelarchivo a través debazel_depyuse_repo. - Un repositorio de extensiones de módulos puede ver todas las dependencias visibles del módulo que proporciona la extensión, además de todos los demás repositorios generados por la misma extensión de módulo.
Registros
Bzlmod descubre dependencias solicitando su información de los registros de Bazel. Un registro de Bazel es simplemente una base de datos de módulos de Bazel. La única forma admitida de registros es un registro de índice, que es un directorio local o un servidor HTTP estático que sigue un formato específico. En el futuro, planeamos agregar compatibilidad con registros de un solo módulo, que son simplemente repositorios de Git que contienen la fuente y el historial de un proyecto.
Registro de índice
Un registro de índice es un directorio local o un servidor HTTP estático que contiene
información sobre una lista de módulos, incluida su página principal, los mantenedores, el
MODULE.bazel archivo de cada versión y cómo recuperar la fuente de cada
versión. En particular, no necesita entregar los archivos fuente.
Un registro de índice debe seguir el siguiente formato:
/bazel_registry.json: Un archivo JSON que contiene metadatos para el registro, como los siguientes:mirrors, que especifica la lista de servidores proxy que se usarán para los archivos fuente.module_base_path, que especifica la ruta de acceso base para los módulos conlocal_repositorytipo en elsource.jsonarchivo.
/modules: Un directorio que contiene un subdirectorio para cada módulo de este registro./modules/$MODULE: Un directorio que contiene un subdirectorio para cada versión de este módulo, así como el siguiente archivo:metadata.json: Un archivo JSON que contiene información sobre el módulo, con los siguientes campos:homepage: La URL de la página principal del proyecto.maintainers: Una lista de objetos JSON, cada uno de los cuales corresponde a la información de un mantenedor del módulo en el registro. Ten en cuenta que no siempre es lo mismo que los autores del proyecto.versions: Una lista de todas las versiones de este módulo que se encuentran en este registro.yanked_versions: Una lista de versiones retiradas de este módulo. Actualmente, no se realiza ninguna operación, pero, en el futuro, se omitirán las versiones retiradas o se mostrará un error.
/modules/$MODULE/$VERSION: Un directorio que contiene los siguientes archivos:MODULE.bazel: El archivoMODULE.bazelde esta versión del módulo.source.json: Un archivo JSON que contiene información sobre cómo recuperar la fuente de esta versión del módulo.- El tipo predeterminado es "archive" con los siguientes campos:
url: La URL del archivo fuente.integrity: La suma de verificación de integridad de subrecursos del archivo.strip_prefix: Un prefijo de directorio que se quitará cuando se extraiga el archivo fuente.patches: Una lista de cadenas, cada una de las cuales nombra un archivo de parche que se aplicará al archivo extraído. Los archivos de parche se encuentran en el/modules/$MODULE/$VERSION/patchesdirectorio.patch_strip: Es igual que el argumento--stripdel parche de Unix.
- El tipo se puede cambiar para usar una ruta de acceso local con estos campos:
type:local_pathpath: La ruta de acceso local al repositorio, que se calcula de la siguiente manera:- Si la ruta de acceso es una ruta de acceso absoluta, se usará tal como está.
- Si la ruta de acceso es una ruta de acceso relativa y
module_base_pathes una ruta de acceso absoluta, la ruta de acceso se resuelve en<module_base_path>/<path> - Si la ruta de acceso y
module_base_pathson rutas de acceso relativas, la ruta de acceso se resuelve en<registry_path>/<module_base_path>/<path>. El registro debe alojarse de forma local y usarse con--registry=file://<registry_path>. De lo contrario, Bazel generará un error.
- El tipo predeterminado es "archive" con los siguientes campos:
patches/: Un directorio opcional que contiene archivos de parche, que solo se usa cuandosource.jsontiene el tipo "archive".
Registro central de Bazel
El registro central de Bazel (BCR) es un registro de índice ubicado en
bcr.bazel.build. Su contenido
está respaldado por el repositorio de GitHub
bazelbuild/bazel-central-registry.
La comunidad de Bazel mantiene el BCR; los colaboradores pueden enviar solicitudes de extracción. Consulta las Políticas y procedimientos del registro central de Bazel.
Además de seguir el formato de un registro de índice normal, el BCR requiere
un archivo presubmit.yml para cada versión del módulo
(/modules/$MODULE/$VERSION/presubmit.yml). Este archivo especifica algunos objetivos esenciales
de compilación y prueba que se pueden usar para verificar la validez de esta
versión del módulo y las canalizaciones de CI del BCR lo usan para garantizar la interoperabilidad
entre los módulos en el BCR.
Selección de registros
Se puede usar la marca repetible de Bazel --registry para especificar la lista de
registros de los que se solicitarán módulos, de modo que puedas configurar tu proyecto para recuperar
dependencias de un registro interno o de terceros. Los registros anteriores tienen
prioridad. Para mayor comodidad, puedes colocar una lista de marcas --registry en el
.bazelrc archivo de tu proyecto.
Extensiones de módulos
Las extensiones de módulos te permiten extender el sistema de módulos leyendo datos de entrada
de módulos en el gráfico de dependencias, realizando la lógica necesaria para resolver
dependencias y, por último, creando repositorios llamando a reglas de repositorios. Son similares
en función a las macros WORKSPACE actuales, pero son más adecuadas en el mundo de
los módulos y las dependencias transitivas.
Las extensiones de módulos se definen en archivos .bzl, al igual que las reglas de repositorios o
WORKSPACE macros. No se invocan directamente; en cambio, cada módulo puede
especificar fragmentos de datos llamados etiquetas para que las extensiones los lean. Luego, después de que se realiza la resolución de la versión del módulo, se ejecutan las extensiones de módulos. Cada extensión se ejecuta
una vez después de la resolución del módulo (antes de que se produzca cualquier compilación) y
puede leer todas las etiquetas que le pertenecen en todo el gráfico de dependencias.
[ A 1.1 ]
[ * maven.dep(X 2.1) ]
[ * maven.pom(...) ]
/ \
bazel_dep / \ bazel_dep
/ \
[ B 1.2 ] [ C 1.0 ]
[ * maven.dep(X 1.2) ] [ * maven.dep(X 2.1) ]
[ * maven.dep(Y 1.3) ] [ * cargo.dep(P 1.1) ]
\ /
bazel_dep \ / bazel_dep
\ /
[ D 1.4 ]
[ * maven.dep(Z 1.4) ]
[ * cargo.dep(Q 1.1) ]
En el gráfico de dependencias de ejemplo anterior, A 1.1 y B 1.2, etc., son módulos de Bazel;
puedes pensar en cada uno como un archivo MODULE.bazel. Cada módulo puede especificar algunas
etiquetas para las extensiones de módulos; aquí se especifican algunas para la extensión "maven",
y otras para "cargo". Cuando se finaliza este gráfico de dependencias (por
ejemplo, es posible que B 1.2 tenga un bazel_dep en D 1.3, pero se actualizó a
D 1.4 debido a C), se ejecuta la extensión "maven" y puede leer todas las
maven.* etiquetas, usando la información que contiene para decidir qué repositorios crear.
Lo mismo sucede con la extensión "cargo".
Uso de extensiones
Las extensiones se alojan en los módulos de Bazel, por lo que, para usar una extensión en
tu módulo, primero debes agregar un bazel_dep en ese módulo y, luego, llamar a
la función integrada use_extension para incluirla en el alcance. Considera el siguiente ejemplo, un fragmento de
un archivo MODULE.bazel para usar una extensión hipotética "maven" definida en el
rules_jvm_external módulo:
bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
Después de incluir la extensión en el alcance, puedes usar la sintaxis de puntos para
especificar etiquetas. Ten en cuenta que las etiquetas deben seguir el esquema definido por las
clases de etiquetas correspondientes (consulta la definición de extensión
a continuación). Este es un ejemplo en el que se especifican algunas etiquetas maven.dep y maven.pom.
maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")
Si la extensión genera repositorios que deseas usar en tu módulo, usa la
use_repo directiva para declarar
los. Esto es para satisfacer la condición de dependencias estrictas y evitar el conflicto de nombres de repositorios locales.
use_repo(
maven,
"org_junit_junit",
guava="com_google_guava_guava",
)
Los repositorios generados por una extensión forman parte de su API, por lo que, a partir de las etiquetas que
especificaste, debes saber que la extensión "maven" generará un
repositorio llamado "org_junit_junit" y otro llamado "com_google_guava_guava". Con
use_repo, puedes cambiarles el nombre de forma opcional en el alcance de tu módulo, como a
"guava" aquí.
Definición de extensión
Las extensiones de módulos se definen de manera similar a las reglas de repositorios, con la
module_extension función.
Ambas tienen una función de implementación; sin embargo, mientras que las reglas de repositorios tienen varios
atributos, las extensiones de módulos tienen varios
tag_classes, cada uno de los cuales tiene varios
atributos. Las clases de etiquetas definen esquemas para las etiquetas que usa esta
extensión. Continuando con nuestro ejemplo de la extensión hipotética "maven" anterior:
# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
implementation=_maven_impl,
tag_classes={"dep": maven_dep, "pom": maven_pom},
)
Estas declaraciones dejan en claro que se pueden especificar etiquetas maven.dep y maven.pom con el esquema de atributos definido anteriormente.
La función de implementación es similar a una macro WORKSPACE, excepto que obtiene un objeto module_ctx, que otorga acceso al gráfico de dependencias y a todas las etiquetas pertinentes. Luego, la función de implementación
debe llamar a las reglas de repositorios para generar repositorios:
# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
coords = []
for mod in ctx.modules:
coords += [dep.coord for dep in mod.tags.dep]
output = ctx.execute(["coursier", "resolve", coords]) # hypothetical call
repo_attrs = process_coursier(output)
[maven_single_jar(**attrs) for attrs in repo_attrs]
En el ejemplo anterior, recorremos todos los módulos del gráfico de dependencias
(ctx.modules), cada uno de los cuales es un
bazel_module objeto cuyo tags campo
expone todas las etiquetas maven.* en el módulo. Luego, invocamos la utilidad de CLI
Coursier para comunicarnos con Maven y realizar la resolución. Por último, usamos el resultado de la resolución
para crear varios repositorios con la regla de repositorio hipotética maven_single_jar.