Bzlmod é o nome de código do novo sistema de dependência externa introduzido no Bazel 5.0. Ele foi introduzido para resolver vários problemas 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 mais detalhes.
No Bazel 5.0, o Bzlmod não está ativado por padrão. A flag
--experimental_enable_bzlmod precisa ser especificada para que o seguinte entre em
vigor. Como o nome da flag sugere, esse recurso está experimental no momento;
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. Também é possível encontrar exemplos de uso do Bzlmod no repositório de exemplos.
Módulos do Bazel
O antigo sistema de dependência externa baseado em WORKSPACE é centralizado em
repositórios (ou repos), criados por regras de repositório (ou regras de repo).
Embora os repositórios ainda sejam um conceito importante no novo sistema, 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 delas publicando metadados sobre outros módulos dos quais depende. Isso é análogo a conceitos familiares em outros sistemas de gerenciamento de dependências: um artefato do Maven, um pacote do npm, uma caixa do Cargo, um módulo do Go etc.
Um módulo simplesmente especifica as dependências usando name e version pares,
em vez de URLs específicos em WORKSPACE. As dependências são pesquisadas em
um registro do Bazel. Por padrão, o
Registro central do Bazel. 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. Confira 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 as 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
MODULE.bazel oferecem suporte 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 de 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
mais popular é o SemVer, mas também há
projetos importantes que usam esquemas diferentes, como o
Abseil, cujas
versões são baseadas em datas, 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 prescreve que a parte "lançamento" da versão precisa consistir em três
segmentos:
MAJOR.MINOR.PATCH. No Bazel, esse requisito é flexibilizado para que qualquer número de segmentos seja permitido. - No SemVer, cada um dos segmentos na parte "lançamento" precisa ser apenas dígitos. No Bazel, isso é flexibilizado 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ão principal, secundária e de patch não é aplicada. No entanto, consulte o nível de compatibilidade para detalhes sobre como denotamos a compatibilidade com versões anteriores.
Qualquer versão SemVer válida é uma versão de módulo do Bazel válida. Além disso, duas
versões SemVer a e b comparam a < b se o mesmo for válido quando forem
comparadas como versões de módulo do Bazel.
Resolução de versão
O problema de dependência de diamante é um elemento básico no espaço de gerenciamento de dependência com 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 de versão mínima (MVS, na sigla em inglês) introduzido no sistema de módulos do Go. O MVS pressupõe que todas as novas versões de um módulo sejam compatíveis com versões anteriores e, portanto, escolhe a versão mais alta especificada por qualquer dependente (D 1.1 no nosso exemplo). Ele é chamado de "mínimo" porque D 1.1 aqui é a versão mínima que pode atender aos nossos requisitos. Mesmo que D 1.2 ou mais recente exista, não as 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 é 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 ele simplesmente trata versões incompatíveis de um módulo como um módulo separado. Em termos de 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ência resolvido. Isso, por sua vez, é possível devido ao fato de que a versão principal é codificada no caminho do pacote no 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 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 do módulo em
sua diretiva module(). Com essas informações em mãos, podemos gerar um erro
quando detectamos que versões do mesmo módulo com níveis de compatibilidade
diferentes existem 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, tanto
@io_bazel_skylib quanto @bazel_skylib significam
Bazel skylib), ou o mesmo
nome de repositório pode ser usado para dependências diferentes em projetos diferentes.
No Bzlmod, os repositórios podem ser gerados por módulos do Bazel e extensões de módulo. Para resolver conflitos de nomes de repositório, estamos adotando o mecanismo de mapeamento de repositório no novo sistema. Dois conceitos importantes:
Nome canônico do repositório: o nome do repositório globalmente exclusivo para cada repositório. Esse será o nome do diretório em que o repositório reside.
Ele é construído da seguinte maneira (Aviso: o formato de nome canônico não é uma API da qual você deve depender. Ele 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ódulo:
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
BUILDe.bzlem 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ó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 de repositório para resolver o nome do repositório ao construir 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 MODULE.bazel
arquivo. 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 permite que façamos verificações mais rigorosas. Em particular, agora aplicamos que um módulo só pode usar repositórios criados a partir das 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 muda.
As dependências estritas são implementadas com base no mapeamento de repositório. Basicamente, o mapeamento de repositório para cada repositório contém todas as dependências diretasdele. 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.bazelviabazel_depeuse_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 informações delas nos registros do Bazel. Um registro do Bazel é simplesmente um banco de dados de módulos do Bazel. A única forma de registros com suporte é 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 do Git que contêm 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 responsáveis pela manutenção, o arquivo
MODULE.bazel de cada versão e como buscar a origem de cada
versão. É importante ressaltar que ele não precisa veicular os arquivos de origem.
Um registro de índice precisa seguir o formato abaixo:
/bazel_registry.json: um arquivo JSON que contém metadados para o registro, como:mirrors, que especifica a lista de espelhos a serem usados para arquivos de origem.module_base_path, especificando o caminho base para módulos comlocal_repositorytipo nosource.jsonarquivo.
/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 desse módulo, bem como o seguinte arquivo:metadata.json: um arquivo JSON que contém 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 deles correspondendo a informações de um responsável pela manutenção do módulo no registro. Isso não é necessariamente o mesmo que os autores do projeto.versions: uma lista de todas as versões desse módulo a serem encontradas em esse registro.yanked_versions: uma lista de versões retiradas desse módulo. No momento, essa é uma operação nula, mas, no futuro, as versões retiradas serão ignoradas ou gerarão um erro.
/modules/$MODULE/$VERSION: um diretório que contém os seguintes arquivos:MODULE.bazel: o arquivoMODULE.bazeldessa versão do módulo.source.json: um arquivo JSON que contém 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 sub-recursos 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 delas nomeando um arquivo de patch a ser aplicado ao arquivo extraído. Os arquivos de patch estão localizados em o/modules/$MODULE/$VERSION/patchesdiretório.patch_strip: igual ao argumento--stripdo patch Unix.
- O tipo pode ser alterado para usar um caminho local com estes campos:
type:local_pathpath: o caminho local para o repositório, calculado da seguinte maneira:- Se o caminho for um caminho absoluto, ele será usado como está.
- Se o caminho for um caminho relativo e
module_base_pathfor um caminho absoluto, o caminho será resolvido para<module_base_path>/<path> - Se o caminho e
module_base_pathforem caminhos 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 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 apenas quandosource.jsontem o tipo "archive".
Registro central do Bazel
O Registro central do Bazel (BCR, na sigla em inglês) é um registro de índice localizado em
bcr.bazel.build. O conteúdo dele
é apoiado pelo repositório do GitHub
bazelbuild/bazel-central-registry.
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 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 do módulo e é usado pelos pipelines de CI do BCR para garantir a interoperabilidade
entre os módulos no BCR.
Como selecionar registros
A flag repetível do Bazel --registry pode ser usada para especificar a lista de
registros dos quais solicitar módulos. Assim, você pode configurar seu projeto para buscar
dependências de um registro interno ou de terceiros. Os registros anteriores têm
precedência. Para sua conveniência, você pode colocar uma lista de --registry flags no
.bazelrc arquivo do seu projeto.
Extensões de 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ência, executando a lógica necessária para resolver
dependências e, por fim, criando repositórios chamando regras de repositório. Elas têm uma função semelhante
às macros WORKSPACE atuais, mas são mais adequadas no mundo dos
módulos e dependências transitivas.
As extensões de módulo são definidas em arquivos .bzl, assim como regras de repositório ou
WORKSPACE macros. 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 de módulo são executadas. Cada extensão é executada
uma vez após a resolução do módulo (ainda antes de qualquer build acontecer) e
pode 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 do Bazel;
você pode pensar em cada um deles 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 são especificadas para "cargo". Quando esse gráfico de dependência é finalizado (por
exemplo, talvez B 1.2 tenha um bazel_dep em D 1.3, mas tenha sido atualizado para
D 1.4 devido a C), a extensão "maven" é executada e pode ler todas as
maven.* tags, usando as informações contidas nelas 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 em
seu módulo, primeiro adicione um bazel_dep nesse módulo e chame
a função integrada use_extension para colocá-la no escopo. Considere o exemplo a seguir, um snippet de
um arquivo MODULE.bazel para usar uma extensão hipotética "maven" definida no
rules_jvm_external módulo:
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 de extensão
abaixo). Confira um exemplo que especifica algumas maven.dep e maven.pom tags.
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
use_repo diretiva para declarar
eles. Isso é para atender à condição de dependências estritas e evitar conflitos de nomes de 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, nas tags que você
especificou, você precisa saber que a extensão "maven" vai gerar um
repositório chamado "org_junit_junit" e outro chamado "com_google_guava_guava". Com
use_repo, você pode renomeá-los opcionalmente 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
module_extension função.
Ambas 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_classes, cada um com vários
atributos. As classes de tag definem esquemas para tags usadas por essa
extensão. Continuando 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 atributo definido acima.
A função de implementação é semelhante a uma macro WORKSPACE, exceto pelo fato de que ela
recebe um objeto module_ctx, que concede
acesso ao gráfico de dependência e a todas as tags relevantes. 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, passamos por todos os módulos no gráfico de dependência
(ctx.modules), cada um deles sendo um
bazel_module objeto cujo tags campo
expõe todas as tags maven.* no módulo. Em seguida, invocamos o utilitário de 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 repositório hipotética maven_single_jar.
Links externos
- Revisão das dependências externas do Bazel (documento de design original do Bzlmod, link em inglês)
- Políticas e procedimentos do Registro central do Bazel (link em inglês)
- Repositório do GitHub do Registro central do Bazel (link em inglês)
- Palestra do BazelCon 2021 sobre o Bzlmod