Bzlmod é o codinome do novo sistema de dependência externa introduzido no Bazel 5.0. Ele foi introduzido para resolver vários aspectos 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 sinalização
--experimental_enable_bzlmod
precisa ser especificada para que o código a seguir
entre em vigor. Como o nome da flag sugere, esse recurso é experimental.
As APIs e os 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 (em inglês). Confira exemplos de usos do Bzlmod no repositório examples.
Módulos do Bazel
O antigo sistema de dependência externa baseado em WORKSPACE
é centrado em
repositórios (ou repositórios), criados por regras de repositório (ou regras de repo).
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 publica metadados sobre outros módulos de que depende. Isso é análogo a conceitos conhecidos em outros sistemas de gerenciamento de dependências: um artefato do Maven, um pacote npm, uma caixa Cargo, um módulo do Go etc.
Um módulo simplesmente especifica as próprias dependências usando os pares name
e version
,
em vez de URLs específicos em WORKSPACE
. As dependências são pesquisadas em
um registro do Bazel (link em inglês). Por padrão, o
Registro Central do Bazel (link em inglês). No 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
deve 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 serão processados para descobrir dependências transitivas automaticamente.
O arquivo MODULE.bazel
é semelhante aos arquivos BUILD
, já que não oferece suporte a nenhuma forma de fluxo de controle. Além disso, ele 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 e assim por diante;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 do módulo:
Formato da versão
O Bazel tem um ecossistema diversificado e os projetos usam vários esquemas de controle de versões. O
SemVer é o mais conhecido, mas também
há projetos em destaque que usam esquemas diferentes, como
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 "release" deve ter apenas dígitos. No Bazel, isso é reduzido 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 é aplicada. 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 ocorrerem quando forem
comparadas como versões de módulo do Bazel.
Resolução da versão
O problema de dependência diamante é fundamental 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 pressupõe que todas as novas versões de um módulo são compatíveis com versões anteriores e, portanto, simplesmente escolhe a maior versão especificada por qualquer dependente (D 1.1 no nosso exemplo). Ela é chamada de "mínima" porque, aqui, D 1.1 é a versão mínima que pode atender aos nossos requisitos. Mesmo que a D 1.2 ou uma versão mais recente exista, não as selecionamos. Isso tem a vantagem adicional de que a seleção da versão é de alta fidelidade e reproduzível.
A resolução da versão é executada localmente na 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 ele 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 resolvido. Isso é possível porque a versão principal é codificada no caminho do pacote em Go, de modo que não há conflitos de tempo de compilação ou 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 diferentes nomes de repositório (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, estamos adotando o mecanismo de mapeamento de repositórios no novo sistema. Aqui estão dois conceitos importantes:
Nome do repositório canônico: o nome exclusivo globalmente de cada repositório. Esse vai ser o nome do diretório em que o repositório vai ficar.
Ele é criado da seguinte maneira. Aviso: o formato de nome canônico não é uma API de que você precisa e está sujeito a mudanças a qualquer momento:- Para repositórios de módulos do Bazel:
module_name~version
(exemplo.@bazel_skylib~1.0.3
) - Para repositórios de extensão de módulos:
module_name~version~extension_name~repo_name
(exemplo.@rules_cc~0.0.1~cc_configure~local_config_cc
)
- Para repositórios de módulos 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 diferentes.
Ele é determinado da seguinte maneira:
Cada repositório tem um dicionário de mapeamento de repositórios das dependências diretas,
que é um mapa do nome do repositório aparente para o nome canônico do repositório.
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ório aparentes podem ser descobertos analisando o arquivo MODULE.bazel
.
Assim, os conflitos podem ser facilmente detectados e resolvidos sem afetar
outras dependências.
Dependências restritas
O novo formato de especificação de dependência permite realizar verificações mais rigorosas. Em particular, agora aplicamos que um módulo só pode usar repositórios criados a partir de dependências diretas. Isso ajuda a evitar interrupções acidentais e difíceis de depurar quando algo no gráfico de dependências transitivas muda.
As dependências restritas são implementadas com base no mapeamento do repositório. Basicamente, o mapeamento de cada repositório contém todas as dependências diretas. Nenhum outro repositório fica visível. As dependências visíveis para cada repositório são determinadas da seguinte maneira:
- Um repositório de módulos do Bazel pode ver todos os repositórios introduzidos no arquivo
MODULE.bazel
viabazel_dep
euse_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 do módulo.
Registros
O Bzlmod descobre dependências solicitando informações dos registros do Bazel. Um registro do Bazel é simplesmente um banco de dados de módulos do Bazel. A única forma de registro aceita é um registro de índice, que é um diretório local ou um servidor HTTP estático que segue um formato específico. No futuro, planejamos adicionar suporte a 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 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 por si só.
Um registro de índice precisa seguir o formato abaixo:
/bazel_registry.json
: um arquivo JSON que contém metadados para o 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, além do seguinte arquivo:metadata.json
: um arquivo JSON que contém informações sobre o módulo, com estes campos:homepage
: o URL da página inicial do projeto.maintainers
: uma lista de objetos JSON, cada um correspondendo às informações de um mantenedor do módulo no registro. Observe que não é necessariamente igual aos authors do projeto.versions
: uma lista de todas as versões do módulo disponíveis nesse registro.yanked_versions
: uma lista de versões yanked deste módulo. Atualmente, é 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
da versão do módulo.source.json
: um arquivo JSON com informações sobre como buscar a origem dessa versão do módulo.- O tipo padrão é "arquivo" com os seguintes campos:
url
: o URL do arquivo de origem.integrity
: a soma de verificação de integridade de sub-recursos do arquivo.strip_prefix
: um prefixo de diretório para remover ao extrair o arquivo de origem.patches
: uma lista de strings. Cada uma delas nomeia um arquivo de patch para aplicar 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 Unix.
- O tipo pode ser alterado para usar um caminho local com estes campos:
type
:local_path
path
: o caminho local para o repositório, calculado da seguinte maneira:- Se o caminho for absoluto, será usado como 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 relativos, o caminho será resolvido para<registry_path>/<module_base_path>/<path>
. O registro precisa ser hospedado localmente e usado por--registry=file://<registry_path>
. Caso contrário, o Bazel gera um erro.
- O tipo padrão é "arquivo" com os seguintes campos:
patches/
: um diretório opcional contendo arquivos de patch, usado apenas quandosource.json
tem o tipo "arquivo".
Registro central do Bazel
O Bazel Central Registry (BCR) é um registro de índice localizado em
bcr.bazel.build. O conteúdo
é respaldado pelo repositório do GitHub
bazelbuild/bazel-central-registry
(em inglês).
O BCR é mantido pela comunidade do Bazel. Os colaboradores podem enviar solicitações de envio. Consulte as 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 destinos
essenciais de build e teste que podem ser usados para verificar a integridade da
versão do módulo. Ele é usado pelos pipelines de CI do BCR para garantir a interoperabilidade
entre módulos no BCR.
Como selecionar registros
A sinalização repetível do Bazel --registry
pode ser usada para especificar a lista de
registros para solicitar módulos. Assim, você pode configurar seu projeto para buscar
dependências de um registro interno ou externo. Registros anteriores têm
prioridade. Por conveniência, coloque 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
de módulos no gráfico de dependências, executando a lógica necessária para resolver
dependências e, por fim, criando repositórios chamando regras de repositório. A função delas é semelhante
às macros WORKSPACE
atuais, mas são mais adequadas no mundo dos
módulos e das dependências transitivas.
As extensões do módulo são definidas em arquivos .bzl
, assim como as regras de repositório ou
macros WORKSPACE
. Eles não são invocados diretamente. Em vez disso, cada módulo pode
especificar dados, chamados de tags, para leitura das extensões. Em seguida, após a conclusão da
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 criação acontecer) e
lê 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ências 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
), o "maven" de extensões é executado e consegue ler todas as
tags maven.*
, usando as informações contidas nele para decidir quais repositórios criar.
O mesmo acontece com 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, você precisa primeiro adicionar um bazel_dep
no módulo e, em seguida, chamar
a função integrada use_extension
(link em inglês)
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ê pode 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 da extensão abaixo. Veja 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ê quer usar no seu módulo, use a
diretiva use_repo
para declará-los. Isso atende à condição de dependências rígidas e evita conflitos 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 da API dela. Portanto, a partir das tags especificadas, a extensão "maven" vai gerar um repositório chamado "org_junit_junit" e outro chamado "com_google_guava_guava". Com
use_repo
, você tem a opção de renomeá-los no escopo do módulo, como
"guava".
Definição de extensão
As extensões do 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. No entanto, 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 tag definem esquemas para as tags usadas por essa extensão. Continuação 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ência e a todas as tags pertinentes. A função
de implementação precisa chamar regras de repositório para gerar repositórios:
# @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, percorremos todos os módulos no gráfico de dependências
(ctx.modules
), cada um deles 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 uma série de repositórios com a regra hipotética de repo maven_single_jar
.