Administra dependencias externas con Bzlmod

Informar un problema Ver fuente . Por la noche · 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Bzlmod es el nombre interno del nuevo sistema de dependencias externas. que se introdujo en Bazel 5.0. Se introdujo para abordar varios puntos débiles del un sistema antiguo que no se podía arreglar de forma incremental; consulta la Sección Planteamiento del problema del documento de diseño original para obtener más información.

En Bazel 5.0, Bzlmod no está activado de forma predeterminada. la bandera Debes especificar --experimental_enable_bzlmod para que se complete lo siguiente efecto. Tal como lo sugiere el nombre de la marca, esta función es actualmente experimental. Las APIs y los comportamientos pueden cambiar hasta el lanzamiento oficial de 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 dependencia externo basado en WORKSPACE se centra en repositories (o repos), creados mediante reglas de repositorio (o reglas de repositorio) Si bien los repositorios siguen siendo un concepto importante en el sistema nuevo, los módulos son las unidades centrales de dependencia.

En esencia, un módulo es un proyecto de Bazel que puede tener varias versiones, cada una que publica metadatos sobre otros módulos de los que depende. Este es análogo a los conceptos conocidos en otros sistemas de administración de dependencias: un artifact, un paquete de npm, un cajón de Cargo, un módulo de Go, etcétera.

Un módulo simplemente especifica sus dependencias con los pares name y version, en lugar de URLs específicas en WORKSPACE. Luego, se buscan las dependencias un registro de Bazel de forma predeterminada, Registro Central de Bazel. En tu lugar de trabajo, cada uno y luego se convierte en un repositorio.

MODULE.bazel

Cada versión de cada módulo tiene un archivo MODULE.bazel que declara su las dependencias y otros metadatos. A continuación, te mostramos 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 necesitas para especificar tus dependencias transitivas; en cambio, solo debes especificar dependencias directas, y los archivos MODULE.bazel de tus dependencias se se procesan para descubrir dependencias transitivas automáticamente.

El archivo MODULE.bazel es similar a los archivos BUILD, ya que no admite ninguno una forma de flujo de control; además prohíbe las sentencias load. Las directivas Características de los archivos MODULE.bazel:

Formato de la versión

Bazel tiene un ecosistema diverso y los proyectos usan varios esquemas de control de versiones. El el más popular por mucho es SemVer, pero hay también destaca proyectos con diferentes esquemas, Abseil, cuyo se basan en fechas, por ejemplo, 20210324.2).

Por este motivo, Bzlmod adopta una versión más relajada de la especificación de SemVer. El Estas son algunas de las diferencias:

  • SemVer prescribe que la "liberación" parte de la versión debe constar de 3 segmentos: MAJOR.MINOR.PATCH. En Bazel, este requisito se flexibiliza, por lo que que se permite cualquier cantidad de segmentos.
  • En SemVer, cada uno de los segmentos del “lanzamiento” debe contener solo dígitos. En Bazel, esto se flexibiliza para permitir letras también, y la comparación la semántica coinciden con los “identificadores” en la sección “Versión preliminar” de una parte del proyecto.
  • Además, la semántica de los aumentos mayores, secundarios y de versiones de parche son sin aplicar de manera forzosa. (Sin embargo, consulta el nivel de compatibilidad para detalles sobre cómo denotamos la retrocompatibilidad).

Cualquier versión válida de SemVer es una versión válida del módulo de Bazel. Además, dos Las versiones a y b de SemVer comparan a < b si son iguales en comparación con las versiones de módulos de Bazel.

Resolución de la versión

El problema de la dependencia de diamante es un elemento básico de la dependencia con control de versiones de una organización. 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 debería usar? Para resolver esta pregunta, Bzlmod usa la Selección de versión mínima (MVS) en el sistema de módulos de Go. MVS asume que todos los datos de un módulo son retrocompatibles y, por lo tanto, simplemente elige la más alta especificada por cualquier dependiente (D 1.1 en nuestro ejemplo). Se llama “mínimo” porque D 1.1 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 que la selección de versión sea de alta fidelidad y reproducible.

La resolución de la versión 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 retrocompatibilidad es factible porque 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 dependencia resuelto. Esto, a su vez, posible por el hecho de que la versión principal está codificada en la ruta del paquete en Así que no haya conflictos en el tiempo de compilación ni de vinculación.

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

Nombres de repositorios

En Bazel, cada dependencia externa tiene un nombre de repositorio. A veces, el mismo por medio de diferentes nombres de repositorio (por ejemplo, tanto @io_bazel_skylib y @bazel_skylib significan Bazel skylib), o el mismo el nombre del repositorio podría usarse para diferentes dependencias en proyectos distintos.

En Bzlmod, los repositorios se pueden generar con módulos de Bazel y extensiones del módulo. Para resolver conflictos de nombres de repositorios, Implementamos la asignación de repositorios un mecanismo de control en el nuevo sistema. Estos son dos conceptos importantes:

  • Nombre del repositorio canónico: Es el nombre del repositorio único a nivel global para cada uno. en un repositorio de confianza. Este será el nombre del directorio donde reside el repositorio.
    Está construida de la siguiente manera (Advertencia: El formato de nombre canónico es no es una API en la que deberías confiar, está sujeta a cambios en cualquier momento):

    • Para los repositorios del módulo de Bazel: module_name~version
      (Ejemplo. @bazel_skylib~1.0.3)
    • Para los repositorios de extensiones de módulo: module_name~version~extension_name~repo_name
      (Ejemplo: @rules_cc~0.0.1~cc_configure~local_config_cc)
  • Nombre del repositorio aparente: Es el nombre del repositorio que se usará en BUILD y .bzl archivos en un repositorio. La misma dependencia puede tener distintos comportamientos en diferentes repositorios.
    Se determina de la siguiente manera:

    • Para los repositorios del módulo de Bazel: module_name de predeterminado o el nombre especificado por el atributo repo_name en bazel_dep
    • Para los repositorios de extensiones de módulo: el nombre del repositorio ingresado a través de use_repo

Cada repositorio tiene un diccionario de asignación de repositorios de sus dependencias directas, que es un mapa desde el nombre aparente del repositorio hasta el nombre del repositorio canónico. Usamos la asignación del repositorio para resolver el nombre del repositorio cuando construimos un etiqueta. Ten en cuenta que no se generan conflictos con los nombres de repositorios canónicos. Los usos de nombres de repositorio aparentes se pueden descubrir analizando MODULE.bazel. Así, los conflictos se pueden detectar y resolver fácilmente sin afectarse otras dependencias.

Dependencias estrictas

El nuevo formato de especificación de dependencias nos permite realizar verificaciones más estrictas. En particular, ahora establecemos que un módulo solo puede usar repositorios creados a partir de su las dependencias directas. Esto ayuda a evitar fallas accidentales y difíciles de depurar cuando cambia algo en el gráfico de dependencia transitivo.

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

  • Un repositorio de módulos de Bazel puede ver todos los repositorios ingresados 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 el mismo módulo. .

Registros

Para descubrir dependencias, Bzlmod solicita su información a Bazel registros. Un registro de Bazel es simplemente una base de datos de módulos de Bazel. El único una forma de registros admitida es un registro de índices, que es un directorio local o un servidor HTTP estático según un formato específico. En la en el futuro, planeamos agregar compatibilidad con los registros de un solo módulo, que son Repositorios de Git que contienen la fuente y el historial de un proyecto

Registro de índices

Un registro de índice es un directorio local o un servidor HTTP estático que contiene información sobre una lista de módulos, como su página principal, los encargados de mantenimiento, los MODULE.bazel de cada versión y cómo recuperar la fuente de cada una versión. En particular, no necesita entregar los archivos de origen en sí.

Un registro de índices debe seguir el siguiente formato:

  • /bazel_registry.json: Es un archivo JSON que contiene metadatos para el registro, como el siguiente:
    • mirrors, que especifica la lista de duplicaciones que se usarán para los archivos de origen.
    • module_base_path, especificando la ruta base para los módulos con Escribe local_repository en el archivo source.json.
  • /modules: Es un directorio que contiene un subdirectorio para cada módulo de este. registro.
  • /modules/$MODULE: Es un directorio que contiene un subdirectorio para cada versión. de este módulo, así como del siguiente archivo:
    • metadata.json: Es 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 lo siguiente: la información de un encargado de mantenimiento del módulo en el registro Ten en cuenta que no es necesariamente lo mismo que los autores de la en un proyecto final.
      • versions: Es una lista de todas las versiones de este módulo que se encuentran en este registro.
      • yanked_versions: Es una lista de versiones ya realizadas de este módulo. Esta actualmente es una no-op, pero en el futuro, las versiones ya creadas se omitió o generará un error.
  • /modules/$MODULE/$VERSION: Un directorio que contiene los siguientes archivos:
    • MODULE.bazel: Es el archivo MODULE.bazel de la versión de este módulo.
    • source.json: Es un archivo JSON que contiene información para recuperar el de esta versión del módulo.
      • El tipo predeterminado es "archivar" con los siguientes campos:
        • url: Es la URL del archivo de origen.
        • integrity: El Integridad de los subrecursos de comprobación del archivo.
        • strip_prefix: Es un prefijo de directorio que se quitará cuando se extraiga el elemento. archivo fuente.
        • patches: Una lista de cadenas, cada una de las cuales nombra un archivo de parche al se aplica al archivo extraído. Los archivos de parche se encuentran en el directorio /modules/$MODULE/$VERSION/patches.
        • patch_strip: Igual que el argumento --strip del parche de Unix.
      • Se puede cambiar el tipo para usar una ruta local con estos campos:
        • type: local_path
        • path: Es la ruta local al repositorio, que se calcula de la siguiente manera:
          • Si la ruta es una ruta de acceso absoluta, se usará tal como está.
          • Si la ruta de acceso es relativa y module_base_path es una ruta absoluta, la ruta se resuelve a <module_base_path>/<path>
          • Si ruta y module_base_path son rutas de acceso relativas, la ruta es se resolvió a <registry_path>/<module_base_path>/<path>. El registro debe estar alojado de forma local, y --registry=file://<registry_path> debe usarlo. De lo contrario, Bazel mostrará un error.
    • patches/: Es un directorio opcional que contiene archivos de parche; solo se usa cuando source.json tiene "file". el tipo de letra.

Registro central de Bazel

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

La comunidad de Bazel mantiene el BCR. los colaboradores pueden enviar las solicitudes de extracción. Consulta Políticas y procedimientos del Registro Central de Bazel.

Además de seguir el formato de un registro de índice normal, la BCR requiere un archivo presubmit.yml para cada versión del módulo (/modules/$MODULE/$VERSION/presubmit.yml) Este archivo especifica algunos elementos crear y probar objetivos que se pueden usar para comprobar la validez de este y la usan las canalizaciones de CI de BCR para garantizar la interoperabilidad entre los módulos en el BCR.

Selecciona registros

Se puede usar la marca repetible de Bazel --registry para especificar la lista de registros a los que solicitar módulos, de modo que puedas configurar tu proyecto para recuperar las dependencias de un registro interno o de terceros. Los registros anteriores toman prioridad. Para mayor comodidad, puedes incluir una lista de marcas --registry en el .bazelrc archivo de tu proyecto.

Extensiones de módulo

Las extensiones de módulo 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 las dependencias y, por último, la creación de repositorios llamando a las reglas de repositorio. Son similares en función a las macros WORKSPACE actuales, pero son más adecuadas en el mundo de módulos y dependencias transitivas.

Las extensiones de módulo se definen en archivos .bzl, al igual que las reglas del repositorio o Macros de WORKSPACE. No se invocan de forma directa. sino que cada módulo especificar fragmentos de datos llamados etiquetas para que lean las extensiones. Luego, después del módulo se completa la resolución de la versión, 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 se produzca una compilación) y lee todas las etiquetas que le pertenecen en todo el gráfico de la 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 dependencias de ejemplo anterior, A 1.1 y B 1.2, etc., son módulos de Bazel. puedes considerar a cada uno como un archivo MODULE.bazel. Cada módulo puede especificar algunos etiquetas para extensiones de módulo; aquí se especifican algunas para la extensión "maven", y otras están especificadas para “cargo”. Cuando se finaliza este gráfico de dependencia (para Por ejemplo, tal vez B 1.2 realmente tenga un bazel_dep en D 1.3, pero se actualizó a D 1.4 debido a C), la extensión "maven" se ejecuta y lee todos los Etiquetas maven.*, que usan su información para decidir qué repositorios crear Lo mismo ocurre con el "cargo" .

Uso de extensiones

Las extensiones se alojan en módulos de Bazel. Usar una extensión en tu módulo, primero debes agregar un bazel_dep en ese módulo y, luego, llamar el use_extension integrado para ponerlo dentro del alcance. Considera el siguiente ejemplo, un fragmento de un archivo MODULE.bazel para usar un “maven” hipotético de Compute Engine, como se define 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 definir el alcance de la extensión, puedes usar la sintaxis de puntos para especificar etiquetas para ella. Ten en cuenta que las etiquetas deben seguir el esquema definido por el clases de etiquetas correspondientes (consulta la definición de extensión) a continuación). A continuación, se muestra 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 quieres usar en tu módulo, usa el Directiva use_repo para declarar de ellos. Esto se hace para cumplir con la condición de dependencias estrictas y evitar el nombre del repositorio local conflicto.

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

Los repositorios que genera una extensión forman parte de su API, así que, a partir de las etiquetas especificado, debes saber que el término "maven" la extensión generará un llamado “org_junit_junit” y otro llamado “com_google_guava_guava”. Con use_repo. De manera opcional, puedes cambiarles el nombre dentro del alcance de tu módulo, por ejemplo: “guayaba” aquí.

Definición de la extensión

Las extensiones de módulo se definen de manera similar a las reglas de repo, con las función module_extension. Ambos tienen una función de implementación. pero, aunque las reglas de repo tienen atributos, las extensiones de módulo tienen varios tag_class, cada uno de los cuales tiene un la cantidad de atributos. Las clases de etiquetas definen esquemas para las etiquetas que esta usa . Continuando con nuestro ejemplo del hipotético “maven” extensión 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 claro que las etiquetas maven.dep y maven.pom se pueden especificado 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 la dependencia y a todas las etiquetas pertinentes. La implementación debería llamar a las reglas de repo 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 dependencias. (ctx.modules), cada uno de los cuales es un Un objeto bazel_module cuyo campo tags expone todas las etiquetas maven.* en el módulo. Luego, invocamos la utilidad de la CLI Solicita comunicarte con Maven y realizar la resolución. Por último, usamos la resolución para crear varios repositorios usando el método maven_single_jar repo.