Bzlmod é o codinome do novo sistema de dependência externa introduzido no Bazel 5.0. Ele foi apresentado 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 de problema" do documento de design original para saber mais.
No Bazel 5.0, o Bzlmod não é ativado por padrão. A flag
--experimental_enable_bzlmod
precisa ser especificada para que o seguinte
entre em vigor. Como o nome da sinalização sugere, esse recurso atualmente é experimental.
APIs e comportamentos podem mudar até que o recurso seja lançado oficialmente.
Para migrar seu projeto para o Bzlmod, siga o Guia de migração do Bzlmod. Você também pode encontrar exemplos de usos do Bzlmod no repositório de exemplos (em inglês).
Módulos do 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 regras de repositório).
Embora os repositórios ainda sejam um conceito importante no novo sistema, os módulos são as
unidades principais de dependência.
Um módulo é essencialmente um projeto do Bazel que pode ter várias versões, cada uma publicando metadados sobre outros módulos de que depende. Isso é análogo a conceitos conhecidos em outros sistemas de gerenciamento de dependências: um artefato Maven, um pacote npm, uma caixa Cargo, um módulo Go etc.
Um módulo simplesmente especifica as próprias dependências usando pares name
e version
,
em vez de URLs específicos em WORKSPACE
. As dependências são então procuradas em
um registro do Bazel. Por padrão, no
Bazel Central Registry. No seu espaço de trabalho, cada módulo é transformado em um repositório.
MODULE.bazel
Cada versão de cada módulo tem um arquivo MODULE.bazel
que declara as
dependências e outros metadados. Este é 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
precisa estar localizado na raiz do diretório do espaço de trabalho, ao lado do arquivo WORKSPACE
. Ao contrário do arquivo WORKSPACE
, não é necessário especificar as dependências transitivas. Em vez disso, especifique apenas dependências diretas, e os arquivos MODULE.bazel
das dependências são processados para descobrir dependências transitivas automaticamente.
O arquivo MODULE.bazel
é semelhante aos arquivos BUILD
, porque não oferece suporte a nenhuma
forma de fluxo de controle. Ele também proíbe instruções load
. As diretivas
compatíveis com os arquivos MODULE.bazel
são:
module
, para especificar metadados sobre o módulo atual, incluindo nome, versão etc.bazel_dep
, para especificar dependências diretas em outros módulos do Bazel.- Substituições, que só podem ser usadas pelo módulo raiz (ou seja, não por um módulo que está sendo usado como dependência) para personalizar o comportamento de uma determinada dependência direta ou transitiva:
- Diretivas relacionadas a extensões de módulo:
Formato da versão
O Bazel tem um ecossistema diverso, e os projetos usam vários esquemas de controle de versões. O mais conhecido, de longe, é o SemVer, mas também há projetos proeminentes que usam esquemas diferentes, como o Abseil, com versões baseadas em data, por exemplo, 20210324.2
.
Por esse motivo, o Bzlmod adota uma versão mais flexível da especificação SemVer. As diferenças incluem:
- O SemVer determina que a parte de "lançamento" da versão precisa consistir em três
segmentos:
MAJOR.MINOR.PATCH
. No Bazel, esse requisito é reduzido para que qualquer número de segmentos seja permitido. - No SemVer, cada um dos segmentos na parte "lançamento" precisa ter apenas dígitos. No Bazel, isso é diminuído para permitir letras também, e a semântica de comparação corresponde aos "identificadores" na parte "pré-lançamento".
- Além disso, a semântica de aumentos de versões principais, secundárias e de patch não são aplicadas. No entanto, consulte o nível de compatibilidade para ver detalhes sobre como indicamos a compatibilidade com versões anteriores.
Qualquer versão válida do SemVer é uma versão válida do módulo do Bazel. Além disso, duas versões do SemVer a
e b
comparam a < b
se as mesmas forem mantidas quando forem comparadas às versões do módulo Bazel.
Resolução da versão
O problema de dependência diamante é um item básico no espaço de gerenciamento de dependências com controle de versões. 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 de seleção mínima de versão (MVS, na sigla em inglês) introduzido no sistema de módulos Go. O MVS presume 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 no nosso exemplo). Ela é chamada de "mínima", porque a versão D 1.1 é a versão mínima que pode atender aos nossos requisitos. mesmo que exista D 1.2 ou mais recente, não vamos selecioná-la. Isso tem a vantagem extra de a seleção da versão ser de alta fidelidade e reprodutível.
A resolução de versão é realizada localmente na sua máquina, não pelo registro.
Nível de compatibilidade
A suposição do MVS sobre a compatibilidade com versões anteriores é viável porque ela simplesmente trata as versões incompatíveis com versões anteriores de um módulo como um módulo separado. Em termos do SemVer, isso significa que A 1.x e A 2.x são considerados módulos distintos e podem coexistir no gráfico de dependências resolvida. Isso, por sua vez, é possibilitado 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 vinculação.
No Bazel, não temos essas garantias. Portanto, precisamos de uma maneira de indicar 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 do módulo na
diretiva module()
. Com essas informações em mãos, podemos gerar um erro
quando detectarmos 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 com 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 dependências diferentes em projetos distintos.
No Bzlmod, os repositórios podem ser gerados por módulos do Bazel e extensões de módulo (link em inglês). Para resolver conflitos de nome de repositório, adotamos 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 de cada repositório. Esse será o nome do diretório em que o repositório está.
Ele é criado da seguinte maneira. Aviso: o formato de nome canônico não é uma API de que você precisa depender e está sujeito a mudanças a qualquer momento:- Para repositórios de módulo do 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
)
- Para repositórios de módulo do Bazel:
Nome do repositório aparente: o nome do repositório a ser usado nos arquivos
BUILD
e.bzl
em um repositório. A mesma dependência pode ter nomes aparentes diferentes em repositórios distintos.
É determinado da seguinte maneira:
Cada repositório tem um dicionário de mapeamento de repositório das dependências diretas,
que é um mapa do nome do repositório aparente para o nome do repositório canônico.
Usamos o mapeamento do repositório para resolver o nome do repositório ao criar um
rótulo. Não há conflito de nomes de repositório canônicos, e os
usos de nomes de repositórios aparentes podem ser descobertos analisando o arquivo
MODULE.bazel
. Portanto, os conflitos podem ser facilmente detectados e resolvidos sem afetar
outras dependências.
Dependências estritas
O novo formato de especificação de dependência nos permite realizar verificações mais rigorosas. Em particular, agora aplicamos que um módulo só pode usar repositórios criados a partir das dependências diretas. Isso ajuda a evitar falhas acidentais e difíceis de depurar quando algo no gráfico de dependência transitivo muda.
As dependências restritas são implementadas com base no mapeamento de repositório. Basicamente, o mapeamento de repositório de cada repo 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 maneira:
- Um repositório de módulo do Bazel pode ver todos os repositórios introduzidos no arquivo
MODULE.bazel
usandobazel_dep
euse_repo
. - Um repositório de extensões 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 as informações deles dos registros do Bazel. Um registro do Bazel é simplesmente um banco de dados de módulos do Bazel. A única forma de registros aceita é 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 a registros de módulo único, que são simplesmente repositórios Git que contêm a origem e o histórico de um projeto.
Registro do índice
Um registro de índice é um diretório local ou um servidor HTTP estático que contém
informações sobre uma lista de módulos, incluindo a página inicial, os mantenedores, o
arquivo MODULE.bazel
de cada versão e como buscar a origem de cada
versão. Ele não precisa disponibilizar os arquivos de origem em si.
Um registro de índice precisa seguir o formato abaixo:
/bazel_registry.json
: um arquivo JSON contendo metadados do registro, como:mirrors
, especificando a lista de espelhos a serem usados para arquivos de origem.module_base_path
, especificando o caminho base para módulos com o tipolocal_repository
no arquivosource.json
.
/modules
: um diretório que contém um subdiretório para cada módulo nesse registro./modules/$MODULE
: um diretório que contém 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
: o URL da página inicial do projeto.maintainers
: uma lista de objetos JSON, cada um correspondendo às informações de um administrador do módulo no registro. Observe que ele não é necessariamente o mesmo que os authors do projeto.versions
: uma lista de todas as versões deste módulo encontradas nesse registro.yanked_versions
: uma lista de versões ianizadas deste módulo. No momento, esse é um ambiente autônomo, mas, no futuro, as versões puxadas serão ignoradas ou gerarão um erro.
/modules/$MODULE/$VERSION
: um diretório que contém os seguintes arquivos:MODULE.bazel
: o arquivoMODULE.bazel
dessa versão do módulo.source.json
: um arquivo JSON contendo informações sobre como buscar a origem dessa versão do módulo.- O tipo padrão é "archive" com os seguintes campos:
url
: o URL do arquivo de origem.integrity
: a soma de verificação de Integridade de recursos secundários do arquivo.strip_prefix
: um prefixo de diretório a ser removido ao extrair o arquivo de origem.patches
: uma lista de strings. Cada uma 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
: igual ao argumento--strip
do patch do Unix.
- É possível alterar o tipo para usar um caminho local com estes campos:
type
:local_path
path
: o caminho local para o repo, calculado da seguinte maneira:- Se o caminho for absoluto, será usado do jeito que está.
- Se o caminho for relativo e
module_base_path
for absoluto, o caminho será resolvido para<module_base_path>/<path>
. - Se o caminho e
module_base_path
forem caminhos relativos, o caminho será resolvido como<registry_path>/<module_base_path>/<path>
. O registro precisa ser hospedado localmente e usado por--registry=file://<registry_path>
. Caso contrário, o Bazel vai gerar um erro.
- O tipo padrão é "archive" com os seguintes campos:
patches/
: um diretório opcional que contém arquivos de patch, usado somente quandosource.json
tem o tipo "archive".
Registro central do Bazel
O Bazel Central Registry (BCR) é um registro de índice localizado em
bcr.bazel.build. O conteúdo dela
é respaldado pelo repositório do GitHub
bazelbuild/bazel-central-registry
(link em inglês).
O BCR é mantido pela comunidade do Bazel. Os colaboradores podem enviar solicitações de envio. Consulte as Políticas e procedimentos de registro Central do Bazel.
Além de seguir o formato de um registro de índice normal, o BCR exige
um arquivo presubmit.yml
para cada versão do módulo
(/modules/$MODULE/$VERSION/presubmit.yml
). Esse arquivo especifica alguns destinos essenciais
de build e teste que podem ser usados para verificar a validade dessa
versão de módulo e são usados pelos pipelines de CI do BCR para garantir a interoperabilidade
entre módulos no BCR.
Seleção de registros
A flag repetível --registry
do Bazel pode ser usada para especificar a lista de
registros para solicitar módulos. Assim, é possível 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 sinalizações --registry
no
arquivo .bazelrc
do seu projeto.
Extensões do módulo
As extensões de módulo permitem estender o sistema de módulos lendo dados de entrada
dos módulos no gráfico de dependência, executando a lógica necessária para resolver
dependências e, por fim, criando repositórios chamando as regras de repo. Elas são semelhantes
em função às macros WORKSPACE
atuais, mas são mais adequadas para o mundo dos
módulos e dependências transitivas.
As extensões do módulo são definidas em arquivos .bzl
, assim como as regras de repositório ou
as macros WORKSPACE
. Elas não são invocadas diretamente. Em vez disso, cada módulo pode especificar partes de dados chamadas tags para que as extensões leiam. 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 build realmente acontecer) e
recebe a leitura de 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
são módulos do Bazel.
Pense em cada um como um arquivo MODULE.bazel
. Cada módulo pode especificar algumas
tags para extensões de módulo. Aqui algumas são especificadas para a extensão "maven"
e outras para "cargo". Quando esse gráfico de dependência é finalizado (por
exemplo, talvez B 1.2
realmente tenha um bazel_dep
em D 1.3
, mas tenha sido atualizado para
D 1.4
devido a C
), as extensões "maven" são executadas e ele pode ler todas as tags
maven.*
, usando as informações contidas neles para decidir quais repositórios criar.
O mesmo vale 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 no
seu módulo, primeiro você precisa adicionar uma bazel_dep
a esse módulo e, em seguida, chamar
a função integrada use_extension
para incluí-la no escopo. Considere o exemplo a seguir, um snippet 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 colocar a extensão no escopo, você poderá usar a sintaxe de ponto para especificar tags para ela. As tags precisam seguir o esquema definido pelas classes de tag correspondentes. Consulte a definição de extensão abaixo. Veja um exemplo que especifica 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ê quer usar no seu módulo, use a
diretiva use_repo
para declará-los. Isso atende à condição de dependências estrita e evita o conflito de nome do repositório local.
use_repo(
maven,
"org_junit_junit",
guava="com_google_guava_guava",
)
Os repositórios gerados por uma extensão fazem parte da API dela. Portanto, a partir das tags especificadas, você saberá que a extensão "maven" gerará um repositório chamado "org_junit_junit" e outro chamado "com_google_guava_guava". Com
use_repo
, é possível renomeá-los no escopo do módulo, como em
"guava".
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
.
Ambas têm uma função de implementação. Embora as regras de repo tenham vários atributos, as extensões de módulo têm uma série de tag_class
s, cada uma com vários atributos. As classes de tag definem os esquemas das tags usadas por essa extensão. Continuando com nosso exemplo da extensão hipotética "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 pelo fato de receber um objeto module_ctx
, que concede acesso ao gráfico de dependências e a todas as tags pertinentes. A função de
implementação precisa 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, analisamos todos os módulos no gráfico de dependência
(ctx.modules
), cada um sendo um objeto
bazel_module
cujo campo tags
expõe todas as tags maven.*
no módulo. Em seguida, invocamos o utilitário da CLI
Coursier para entrar em contato com o Maven e realizar a resolução. Por fim, usamos o resultado
da resolução para criar vários repositórios, usando a regra de repo hipotética
maven_single_jar
.