Administra dependencias externas con Bzlmod

Bzlmod es el nombre en clave del nuevo sistema de dependencia externo introducido en Bazel 5.0. Se introdujo para abordar varios puntos problemáticos del antiguo sistema que no podían solucionarse de manera incremental; consulte 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 el indicador --experimental_enable_bzlmod para que surta efecto lo siguiente. Como sugiere el nombre de la bandera, esta característica es actualmente experimental ; Las API y los comportamientos pueden cambiar hasta que la función se lance oficialmente.

Módulos Bazel

El antiguo sistema de dependencia externo basado en WORKSPACE se centra en repositorios (o repositorios ), creados a través de reglas de repositorio (o reglas de repositorio). Si bien los repositorios siguen siendo un concepto importante en el nuevo sistema, los módulos son las unidades centrales de dependencia.

Un módulo es esencialmente 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 conceptos familiares en otros sistemas de administración de dependencias: un artefacto Maven, un paquete npm, una caja de carga, un módulo Go, etc.

Un módulo simplemente especifica sus dependencias usando pares de name y version , en lugar de direcciones URL específicas en WORKSPACE . Luego, las dependencias se buscan en un registro de Bazel ; por defecto, el Registro Central de Bazel . En su espacio de trabajo, cada módulo se convierte en un repositorio.

MODULO.bazel

Cada versión de cada módulo tiene un archivo MODULE.bazel que declara sus dependencias y otros metadatos. He aquí 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 estar ubicado en la raíz del directorio del espacio de trabajo (junto al archivo WORKSPACE ). A diferencia del archivo WORKSPACE , no necesita especificar sus dependencias transitivas ; en su lugar, solo debe especificar dependencias directas y los archivos MODULE.bazel de sus 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 declaraciones de load . Las directivas que soportan los archivos MODULE.bazel son:

Formato de versión

Bazel tiene un ecosistema diverso y los proyectos utilizan varios esquemas de versiones. El más popular con diferencia es SemVer , pero también hay proyectos destacados que utilizan diferentes esquemas como Abseil , cuyas versiones están basadas en fechas, por ejemplo 20210324.2 ).

Por esta razón, Bzlmod adopta una versión más relajada de la especificación SemVer, en particular permitiendo cualquier número de secuencias de dígitos en la parte de "lanzamiento" de la versión (en lugar de exactamente 3 como prescribe SemVer: MAJOR.MINOR.PATCH ). Además, no se aplica la semántica de los aumentos de versiones principales, secundarias y parches. (Sin embargo, consulte el nivel de compatibilidad para obtener detalles sobre cómo denotamos la compatibilidad con versiones anteriores). Otras partes de la especificación de SemVer, como un guión que indica una versión preliminar, no se modifican.

Resolución de la versión

El problema de la dependencia de diamantes es un elemento básico en el espacio de administración de dependencias versionadas. Suponga que tiene el siguiente gráfico de dependencia:

       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 utiliza el algoritmo de selección de versión mínima (MVS) introducido en el sistema del módulo Go. MVS asume 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ínimo" porque D 1.1 aquí es la versión mínima que podría satisfacer nuestros requisitos; incluso si existe D 1.2 o posterior, no los seleccionamos. Esto tiene el beneficio adicional de que la selección de la versión es de alta fidelidad y reproducible .

La resolución de la versión se realiza localmente en su máquina, no por el registro.

Nivel de compatibilidad

Tenga en cuenta que la suposición de MVS sobre la compatibilidad con versiones anteriores es factible porque simplemente trata las versiones incompatibles con versiones anteriores de un módulo como un módulo separado. 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 dependencia resuelta. Esto, a su vez, es posible gracias al hecho de que la versión principal está codificada en la ruta del paquete en Go, por lo que no hay conflictos de tiempo de compilación o tiempo de vinculación.

En Bazel, no tenemos tales garantías. Por lo tanto, necesitamos una forma de indicar el número de "versión principal" para detectar versiones incompatibles con versiones anteriores. Este número se denomina nivel de compatibilidad y lo especifica cada versión del módulo en su directiva module() . Con esta información en la mano, podemos arrojar un error cuando detectamos que existen versiones de un mismo módulo con diferentes niveles de compatibilidad en el gráfico de dependencias resueltas.

Nombres de repositorio

En Bazel, cada dependencia externa tiene un nombre de repositorio. A veces, se puede usar la misma dependencia a través de diferentes nombres de repositorio (por ejemplo, tanto @io_bazel_skylib como @bazel_skylib significan Bazel skylib ), o se puede usar el mismo nombre de repositorio para diferentes dependencias en diferentes proyectos.

En Bzlmod, los módulos Bazel y las extensiones de módulos pueden generar repositorios. Para resolver conflictos de nombres de repositorios, estamos adoptando el mecanismo de mapeo de repositorios en el nuevo sistema. Aquí hay dos conceptos importantes:

  • Nombre de repositorio canónico: el nombre de repositorio global único para cada repositorio. Este será el nombre del directorio en el que vive el repositorio.
    Está construido de la siguiente manera ( Advertencia : el formato de nombre canónico no es una API de la que deba depender, está sujeto a cambios en cualquier momento):

    • Para repositorios de módulos de Bazel: module_name . version
      ( Ejemplo . @bazel_skylib.1.0.3 )
    • Para repositorios de extensión de módulo: module_name . version . extension_name . repo_name
      ( Ejemplo . @rules_cc.0.0.1.cc_configure.local_config_cc )
  • Nombre del repositorio local : el nombre del repositorio que se usará en los archivos BUILD y .bzl dentro de un repositorio. La misma dependencia podría tener diferentes nombres locales para diferentes repositorios.
    Se determina de la siguiente manera:

    • Para los repositorios del módulo Bazel: module_name de forma predeterminada, o el nombre especificado por el atributo repo_name en bazel_dep .
    • Para repositorios de extensión de módulo: nombre del repositorio introducido a través use_repo .

Cada repositorio tiene un diccionario de mapeo de repositorios de sus dependencias directas, que es un mapa del nombre del repositorio local al nombre del repositorio canónico. Usamos el mapeo del repositorio para resolver el nombre del repositorio al construir una etiqueta. Tenga en cuenta que no hay conflicto de nombres de repositorios canónicos, y los usos de los nombres de repositorios locales se pueden descubrir analizando el archivo MODULE.bazel , por lo tanto, 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 comprobaciones más estrictas. En particular, ahora hacemos cumplir que un módulo solo puede usar repositorios creados a partir de sus dependencias directas. Esto ayuda a evitar roturas accidentales y difíciles de depurar cuando cambia algo en el gráfico de dependencia transitiva.

Las dependencias estrictas se implementan en función de la asignación de repositorios . Básicamente, el mapeo del repositorio para cada repositorio contiene todas sus dependencias directas , cualquier otro repositorio no es visible. Las dependencias visibles para cada repositorio se determinan de la siguiente manera:

  • Un repositorio del módulo Bazel puede ver todos los repositorios introducidos en el archivo MODULE.bazel a través de bazel_dep y use_repo .
  • Un repositorio de extensión de módulo 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 Bazel es simplemente una base de datos de módulos 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 soporte para 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 de inicio, los mantenedores, el archivo MODULE.bazel de cada versión y cómo obtener la fuente de cada versión. En particular, no necesita servir los archivos fuente en sí.

Un registro de índice debe seguir el siguiente formato:

  • /bazel_registry.json : un archivo JSON que contiene metadatos para el registro. Actualmente, solo tiene una clave, mirrors , que especifica la lista de espejos que se usarán para los archivos fuente.
  • /modules : un directorio que contiene un subdirectorio para cada módulo en 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 de inicio: la URL de la página de inicio 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 . Tenga en cuenta que esto no es necesariamente 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 extraídas de este módulo. Actualmente, esto no es operativo, pero en el futuro, las versiones extraídas se omitirán o generarán un error.
  • /modules/$MODULE/$VERSION : Un directorio que contiene los siguientes archivos:
    • MODULE.bazel : El archivo MODULE.bazel de esta versión del módulo.
    • source.json : un archivo JSON que contiene información sobre cómo obtener la fuente de esta versión del módulo, con los siguientes campos:
      • url : La URL del archivo fuente.
      • integrity : La suma de comprobación de integridad del subrecurso del archivo.
      • strip_prefix : un prefijo de directorio para eliminar al extraer el archivo fuente.
      • patches : una lista de cadenas, cada una de las cuales nombra un archivo de parche para aplicar al archivo extraído. Los archivos de parches se encuentran en el directorio /modules/$MODULE/$VERSION/patches .
      • patch_strip : Igual que el argumento --strip del parche de Unix.
    • patches/ : un directorio opcional que contiene archivos de parches.

Registro Central de Bazel

Bazel Central Registry (BCR) es un registro de índice ubicado en registration.bazel.build . Su contenido está respaldado por el repositorio de GitHub bazelbuild/bazel-central-registry .

El BCR es mantenido por la comunidad de Bazel; los colaboradores son bienvenidos a enviar solicitudes de incorporación de cambios. Consulte 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 de BCR lo usan para garantizar la interoperabilidad entre los módulos en BCR.

Selección de registros

El indicador repetible de Bazel --registry se puede usar para especificar la lista de registros desde los que solicitar módulos, por lo que puede configurar su proyecto para obtener dependencias de un registro externo o interno. Los registros anteriores tienen prioridad. Para mayor comodidad, puede colocar una lista de indicadores --registry en el archivo .bazelrc de su proyecto.

Extensiones de módulo

Las extensiones de módulos le permiten ampliar el sistema de módulos al leer los datos de entrada de los módulos en el gráfico de dependencias, realizar la lógica necesaria para resolver las dependencias y, finalmente, crear repositorios llamando a las reglas de repositorios. Son similares en función a las macros de WORKSPACE de hoy, pero son más adecuadas en el mundo de los módulos y las dependencias transitivas.

Las extensiones de módulo se definen en archivos .bzl , al igual que las reglas de repositorio o las macros WORKSPACE . No se invocan directamente; más bien, cada módulo puede especificar piezas de datos llamadas etiquetas para que las extensiones las lean. Luego, una vez finalizada la resolución de la versión del módulo, se ejecutan las extensiones del módulo. Cada extensión se ejecuta una vez después de la resolución del módulo (aún antes de que ocurra cualquier compilación) y puede leer todas las etiquetas que le pertenecen en todo el gráfico de dependencia.

          [ 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 dependencia de ejemplo anterior, A 1.1 y B 1.2 , etc. son módulos de Bazel; puede pensar en cada uno como un archivo MODULE.bazel . Cada módulo puede especificar algunas etiquetas para extensiones de módulo; aquí algunos se especifican para la extensión "maven", y algunos se especifican para "cargo". Cuando se finaliza este gráfico de dependencia (por ejemplo, tal vez B 1.2 en realidad tiene un bazel_dep en D 1.3 pero se actualizó a D 1.4 debido a C ), se ejecutan las extensiones "maven" y puede leer todas las etiquetas maven.* , utilizando la información que contiene para decidir qué repositorios crear. Del mismo modo para la extensión de "carga".

uso de extensiones

Las extensiones están alojadas en los propios módulos de Bazel, por lo que para usar una extensión en su módulo, primero debe agregar un bazel_dep en ese módulo y luego llamar a la función incorporada use_extension para incluirlo. Considere el siguiente ejemplo, un fragmento de un archivo MODULE.bazel para usar una extensión "maven" hipotética definida en el módulo rules_jvm_external :

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, puede usar la sintaxis de puntos para especificar etiquetas para ella. Tenga en cuenta que las etiquetas deben seguir el esquema definido por las clases de etiquetas correspondientes (consulte la definición de extensión a continuación). Aquí hay un ejemplo que especifica 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 desea usar en su módulo, use la directiva use_repo para declararlos. Esto es para satisfacer la condición estricta de dependencias y evitar conflictos de nombres de repositorios locales.

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

Los repositorios generados por una extensión son parte de su API, por lo que a partir de las etiquetas que especificó, debe saber que la extensión "maven" generará un repositorio llamado "org_junit_junit" y uno llamado "com_google_guava_guava". Con use_repo , puede cambiarles el nombre opcionalmente en el ámbito de su módulo, como "guayaba" aquí.

Definición de extensión

Las extensiones de módulo se definen de manera similar a las reglas de repositorio, usando la función module_extension . Ambos tienen una función de implementación; pero mientras que las reglas de repositorio tienen varios atributos, las extensiones de módulo tienen varios tag_class es , cada uno de los cuales tiene varios atributos. Las clases de etiquetas definen esquemas para las etiquetas utilizadas por esta extensión. Continuando con nuestro ejemplo de la hipotética extensión "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 las etiquetas maven.dep y maven.pom se pueden especificar utilizando 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 dependencia y todas las etiquetas pertinentes. La función de implementación luego debe llamar a las reglas de repositorio 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, revisamos todos los módulos en el gráfico de dependencia ( ctx.modules ), cada uno de los cuales es un objeto bazel_module cuyo campo de tags expone todas las etiquetas maven.* en el módulo. Luego invocamos la utilidad CLI Coursier para contactar a Maven y realizar la resolución. Finalmente, usamos el resultado de la resolución para crear varios repositorios, usando la regla de repositorio hipotética maven_single_jar .