Gerencie dependências externas com Bzlmod

Bzlmod é o codinome do novo sistema de dependência externa introduzido no Bazel 5.0. Ele foi introduzido para resolver vários pontos problemáticos do sistema antigo que não podiam ser corrigidos de forma incremental; consulte a seção Declaração do Problema do documento de design original para obter mais detalhes.

No Bazel 5.0, o Bzlmod não está ativado por padrão; o sinalizador --experimental_enable_bzlmod precisa ser especificado para que o seguinte tenha efeito. Como o nome do sinalizador sugere, esse recurso é atualmente experimental ; APIs e comportamentos podem mudar até que o recurso seja lançado oficialmente.

Módulos Bazel

O antigo sistema de dependência externa baseado em WORKSPACE é centrado em repositórios (ou repos ), criados por meio de regras de repositório (ou repo rules ). Embora os repositórios ainda sejam um conceito importante no novo sistema, os módulos são as principais unidades de dependência.

Um módulo é essencialmente um projeto Bazel que pode ter várias versões, cada uma das quais publica metadados sobre outros módulos dos quais depende. Isso é análogo a conceitos familiares em outros sistemas de gerenciamento de dependência: um artefato Maven , um pacote npm , uma caixa Cargo , um módulo Go , etc.

Um módulo simplesmente especifica suas dependências usando pares de name e version , em vez de URLs específicos em WORKSPACE . As dependências são então pesquisadas em um registro Bazel ; por padrão, o Registro Central do Bazel . Em seu espaço de trabalho, cada módulo é transformado em um repositório.

MÓDULO.bazel

Cada versão de cada módulo tem um arquivo MODULE.bazel declarando suas dependências e outros metadados. Aqui está um exemplo 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")

O arquivo MODULE.bazel deve estar localizado na raiz do diretório do espaço de trabalho (próximo ao arquivo WORKSPACE ). Ao contrário do arquivo WORKSPACE , você não precisa especificar suas dependências transitivas ; em vez disso, você deve especificar apenas dependências diretas e os arquivos MODULE.bazel de suas dependências são processados ​​para descobrir dependências transitivas automaticamente.

O arquivo MODULE.bazel é semelhante aos arquivos BUILD , pois não oferece suporte a nenhuma forma de fluxo de controle; ele também proíbe declarações de load . As diretivas suportadas pelos arquivos MODULE.bazel são:

Formato da versão

O Bazel tem um ecossistema diversificado e os projetos usam vários esquemas de versão. O mais popular de longe é SemVer , mas também existem projetos proeminentes usando esquemas diferentes, como Abseil , cujas versões são baseadas em data, por exemplo 20210324.2 ).

Por esta razão, Bzlmod adota uma versão mais relaxada da especificação SemVer, em particular permitindo qualquer número de sequências de dígitos na parte "release" da versão (em vez de exatamente 3 como o SemVer prescreve: MAJOR.MINOR.PATCH ). Além disso, a semântica dos aumentos de versão principal, secundária e de patch não é aplicada. (No entanto, consulte o nível de compatibilidade para obter detalhes sobre como denotamos compatibilidade com versões anteriores.) Outras partes da especificação SemVer, como um hífen indicando uma versão de pré-lançamento, não são modificadas.

Resolução da versão

O problema de dependência de diamante é um grampo no espaço de gerenciamento de dependência com versão. Suponha que você tenha o seguinte gráfico de dependência:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

Qual versão de D deve ser usada? Para resolver essa questão, o Bzlmod usa o algoritmo MVS ( Minimal Version Selection ) introduzido no sistema do módulo Go. O MVS assume que todas as novas versões de um módulo são compatíveis com versões anteriores e, portanto, simplesmente escolhe a versão mais alta especificada por qualquer dependente (D 1.1 em nosso exemplo). É chamado de "mínimo" porque D 1.1 aqui é a versão mínima que pode satisfazer nossos requisitos; mesmo que exista D 1.2 ou mais recente, não os selecionamos. Isso tem o benefício adicional de que a seleção de versão é de alta fidelidade e reproduzível .

A resolução de versão é executada localmente em sua máquina, não pelo registro.

Nível de compatibilidade

Observe que a suposição do MVS sobre compatibilidade com versões anteriores é viável porque simplesmente trata versões incompatíveis com versões anteriores de um módulo como um módulo separado. Em termos de SemVer, isso significa que A 1.xe A 2.x são considerados módulos distintos e podem coexistir no gráfico de dependência resolvido. Isso, por sua vez, é possível pelo fato de que a versão principal é codificada no caminho do pacote em Go, portanto, não há conflitos de tempo de compilação ou de tempo de vinculação.

Em Bazel, não temos essas garantias. Assim, precisamos de uma maneira de denotar o número da "versão principal" para detectar versões incompatíveis com versões anteriores. Esse número é chamado de nível de compatibilidade e é especificado por cada versão de módulo em sua diretiva module() . Com essas informações em mãos, podemos gerar um erro quando detectamos que existem versões do mesmo módulo com diferentes níveis de compatibilidade no gráfico de dependência resolvido.

Nomes de repositório

No Bazel, cada dependência externa tem um nome de repositório. Às vezes, a mesma dependência pode ser usada por meio de nomes de repositório diferentes (por exemplo, @io_bazel_skylib e @bazel_skylib significam Bazel skylib ), ou o mesmo nome de repositório pode ser usado para diferentes dependências em projetos diferentes.

No Bzlmod, os repositórios podem ser gerados por módulos Bazel e extensões de módulo . Para resolver conflitos de nome de repositório, estamos adotando o mecanismo de mapeamento de repositório no novo sistema. Aqui estão dois conceitos importantes:

  • Nome do repositório canônico : o nome do repositório globalmente exclusivo para cada repositório. Este será o nome do diretório em que o repositório mora.
    Ele é construído da seguinte forma ( Aviso : o formato do nome canônico não é uma API da qual você deve depender, está sujeito a alterações a qualquer momento):

    • Para repositórios do módulo Bazel: module_name . version
      ( Exemplo . @bazel_skylib.1.0.3 )
    • Para repositórios de extensão de módulo: module_name . version . extension_name . repo_name
      ( Exemplo . @rules_cc.0.0.1.cc_configure.local_config_cc )
  • Nome do repositório local : O nome do repositório a ser usado nos arquivos BUILD e .bzl em um repositório. A mesma dependência pode ter nomes locais diferentes para repositórios diferentes.
    É determinado da seguinte forma:

    • Para repositórios do módulo Bazel: module_name por padrão ou o nome especificado pelo atributo repo_name em bazel_dep .
    • Para repositórios de extensão de módulo: nome do repositório introduzido via use_repo .

Cada repositório tem um dicionário de mapeamento de repositório de suas dependências diretas, que é um mapa do nome do repositório local para o nome do repositório canônico. Usamos o mapeamento do repositório para resolver o nome do repositório ao construir um rótulo. Observe que não há conflito de nomes de repositórios canônicos e os usos de nomes de repositórios locais podem ser descobertos analisando o arquivo MODULE.bazel , portanto, os conflitos podem ser facilmente capturados e resolvidos sem afetar outras dependências.

Deps estritos

O novo formato de especificação de dependência nos permite realizar verificações mais rigorosas. Em particular, agora reforçamos que um módulo só pode usar repositórios criados a partir de suas dependências diretas. Isso ajuda a evitar interrupções acidentais e difíceis de depurar quando algo no gráfico de dependência transitiva é alterado.

Deps estritos é implementado com base no mapeamento de repositório . Basicamente, o mapeamento de repositório para cada repositório contém todas as suas dependências diretas , qualquer outro repositório não é visível. As dependências visíveis para cada repositório são determinadas da seguinte forma:

  • Um repositório do módulo Bazel pode ver todos os repositórios introduzidos no arquivo MODULE.bazel via bazel_dep e use_repo .
  • Um repositório de extensão de módulo pode ver todas as dependências visíveis do módulo que fornece a extensão, além de todos os outros repositórios gerados pela mesma extensão de módulo.

Registros

O Bzlmod descobre dependências solicitando suas informações aos registros do Bazel. Um registro Bazel é simplesmente um banco de dados de módulos Bazel. A única forma de registros com suporte é um registro de índice , que é um diretório local ou um servidor HTTP estático seguindo um formato específico. No futuro, planejamos adicionar suporte para registros de módulo único , que são simplesmente repositórios git contendo a origem e o histórico de um projeto.

Registro de índice

Um registro de índice é um diretório local ou um servidor HTTP estático contendo informações sobre uma lista de módulos, incluindo sua página inicial, mantenedores, o arquivo MODULE.bazel de cada versão e como buscar a fonte de cada versão. Notavelmente, ele não precisa servir os próprios arquivos de origem.

Um registro de índice deve seguir o formato abaixo:

  • /bazel_registry.json : um arquivo JSON contendo metadados para o registro. Atualmente, ele possui apenas uma chave, mirrors , especificando a lista de espelhos a serem usados ​​para arquivos de origem.
  • /modules : Um diretório contendo um subdiretório para cada módulo neste registro.
  • /modules/$MODULE : Um diretório contendo um subdiretório para cada versão deste módulo, bem como o seguinte arquivo:
    • metadata.json : um arquivo JSON contendo informações sobre o módulo, com os seguintes campos:
      • homepage : A URL da página inicial do projeto.
      • maintainers : Uma lista de objetos JSON, cada um dos quais corresponde às informações de um mantenedor do módulo no registro . Observe que isso não é necessariamente o mesmo que os autores do projeto.
      • versions : Uma lista de todas as versões deste módulo que podem ser encontradas neste registro.
      • yanked_versions : Uma lista de versões arrancadas deste módulo. No momento, isso não é operacional, mas no futuro, as versões removidas serão ignoradas ou gerarão um erro.
  • /modules/$MODULE/$VERSION : Um diretório contendo os seguintes arquivos:
    • MODULE.bazel : O arquivo MODULE.bazel desta versão do módulo.
    • source.json : um arquivo JSON contendo informações sobre como buscar a fonte desta versão do módulo, com os seguintes campos:
      • url : O URL do arquivo de origem.
      • integrity : A soma de verificação de integridade do sub -recurso do arquivo.
      • strip_prefix : Um prefixo de diretório para remover ao extrair o arquivo de origem.
      • patches : Uma lista de strings, cada uma das quais nomeia um arquivo de patch a ser aplicado ao arquivo extraído. Os arquivos de patch estão localizados no diretório /modules/$MODULE/$VERSION/patches .
      • patch_strip : O mesmo que o argumento --strip do patch do Unix.
    • patches/ : Um diretório opcional contendo arquivos de patch.

Registo Central de Bazel

O Bazel Central Registry (BCR) é um registro de índice localizado em registry.bazel.build . Seu conteúdo é apoiado pelo repositório GitHub bazelbuild/bazel-central-registry .

O BCR é mantido pela comunidade Bazel; os contribuidores são bem-vindos para enviar solicitações de pull. Consulte Políticas e Procedimentos do Registro Central do Bazel .

Além de seguir o formato de um registro de índice normal, o BCR requer um arquivo presubmit.yml para cada versão do módulo ( /modules/$MODULE/$VERSION/presubmit.yml ). Esse arquivo especifica alguns alvos essenciais de compilação e teste que podem ser usados ​​para verificar a validade desta versão do módulo e é usado pelos pipelines de CI do BCR para garantir a interoperabilidade entre os módulos no BCR.

Selecionando registros

O sinalizador repetível do Bazel --registry pode ser usado para especificar a lista de registros dos quais solicitar módulos, para que você possa configurar seu projeto para buscar dependências de um registro interno ou de terceiros. Registros anteriores têm precedência. Por conveniência, você pode colocar uma lista de sinalizadores --registry no arquivo .bazelrc do seu projeto.

Extensões do módulo

As extensões de módulo permitem estender o sistema de módulo lendo dados de entrada de módulos no gráfico de dependência, executando a lógica necessária para resolver dependências e, finalmente, criando repositórios chamando regras de repositório. Eles são semelhantes em função às macros WORKSPACE , mas são mais adequados no mundo dos módulos e dependências transitivas.

Extensões de módulo são definidas em arquivos .bzl , assim como regras de repositório ou macros WORKSPACE . Eles não são invocados diretamente; em vez disso, cada módulo pode especificar pedaços de dados chamados tags para as extensões lerem. Em seguida, após a resolução da versão do módulo, as extensões do módulo são executadas. Cada extensão é executada uma vez após a resolução do módulo (ainda antes de qualquer compilação realmente acontecer) e consegue ler todas as tags pertencentes a ela em todo o gráfico de dependência.

          [ 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) ]

No gráfico de dependência de exemplo acima, A 1.1 e B 1.2 etc são módulos Bazel; você pode pensar em cada um como um arquivo MODULE.bazel . Cada módulo pode especificar alguns tags para extensões de módulo; aqui alguns são especificados para a extensão "maven", e alguns são especificados para "cargo". Quando este gráfico de dependência é finalizado (por exemplo, talvez B 1.2 tenha um bazel_dep em D 1.3 , mas foi atualizado para D 1.4 devido a C ), as extensões "maven" são executadas e ele lê todas as tags maven.* , usando as informações nele contidas para decidir quais repositórios criar. Da mesma forma para a extensão "cargo".

Uso da extensão

As extensões são hospedadas nos próprios módulos do Bazel, portanto, para usar uma extensão em seu módulo, você precisa primeiro adicionar um bazel_dep nesse módulo e, em seguida, chamar a use_extension para trazê-lo para o escopo. Considere o exemplo a seguir, um trecho de um arquivo MODULE.bazel para usar uma extensão "maven" hipotética definida no módulo rules_jvm_external :

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

Depois de trazer a extensão para o escopo, você pode usar a sintaxe de ponto para especificar tags para ela. Observe que as tags precisam seguir o esquema definido pelas classes de tags correspondentes (consulte a definição de extensão abaixo). Aqui está um exemplo especificando algumas tags maven.dep e 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")

Se a extensão gerar repositórios que você deseja usar em seu módulo, use a diretiva use_repo para declará-los. Isso é para satisfazer a condição de deps estrita e evitar conflito de nomes de repositórios locais.

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

Os repositórios gerados por uma extensão fazem parte de sua API, portanto, a partir das tags especificadas, você deve saber que a extensão "maven" gerará um repositório chamado "org_junit_junit" e um chamado "com_google_guava_guava". Com use_repo , você pode, opcionalmente, renomeá-los no escopo do seu módulo, como "guava" aqui.

Definição de extensão

As extensões de módulo são definidas de maneira semelhante às regras de repositório, usando a função module_extension . Ambos têm uma função de implementação; mas enquanto as regras de repositório têm vários atributos, as extensões de módulo têm vários tag_class es , cada um com vários atributos. As classes de tags definem esquemas para tags usadas por esta extensão. Continuando nosso exemplo da hipotética extensão "maven" acima:

# @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},
)

Essas declarações deixam claro que as tags maven.dep e maven.pom podem ser especificadas, usando o esquema de atributos definido acima.

A função de implementação é semelhante a uma macro WORKSPACE , exceto que obtém um objeto module_ctx , que concede acesso ao gráfico de dependência e a todos os tags pertinentes. A função de implementação deve então chamar regras de repo para gerar repos:

# @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]

No exemplo acima, passamos por todos os módulos no gráfico de dependência ( ctx.modules ), cada um dos quais é um objeto bazel_module cujo campo tags expõe todas as tags maven.* no módulo. Em seguida, invocamos o utilitário CLI Coursier para entrar em contato com o Maven e executar a resolução. Por fim, usamos o resultado da resolução para criar vários repositórios, usando a hipotética regra de maven_single_jar .