Um módulo do Bazel é 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, como um artefato do Maven, um pacote do npm, um módulo do Go ou uma caixa do Cargo.
Um módulo precisa ter um arquivo MODULE.bazel
na raiz do repositório (ao lado do
arquivo WORKSPACE
). Esse arquivo é o manifesto do módulo, declarando o nome,
versão, lista de dependências diretas e outras informações. Por exemplo:
module(name = "my-module", version = "1.0")
bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")
Consulte a lista completa de diretivas disponíveis em arquivos MODULE.bazel
.
Para realizar a resolução de módulos, o Bazel começa lendo o arquivo MODULE.bazel
do módulo raiz e, em seguida, solicita repetidamente o arquivo MODULE.bazel
de qualquer dependência de um registro do Bazel até descobrir todo o gráfico de dependência.
Por padrão, o Bazel seleciona uma versão de cada módulo para usar. O Bazel representa cada módulo com um repositório e consulta o registro novamente para saber como definir cada um deles.
Formato da versão
O Bazel tem um ecossistema diversificado, e os projetos usam vários esquemas de controle de versões. A 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 isso, o Bzlmod adota uma versão mais flexível da especificação SemVer. As diferenças incluem:
- A SemVer determina que a parte "release" 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 "release" precisa ser composto apenas por dígitos. No Bazel, isso é flexibilizado para permitir também letras, e a semântica de comparação corresponde aos "identificadores" na parte "pré-lançamento".
- 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 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 do SemVer a
e b
comparam a < b
se e somente se o mesmo acontecer quando elas forem comparadas como versões do módulo do Bazel.
Seleção da versão
Considere o problema de dependência de diamante, um elemento básico no espaço de gerenciamento de dependências com controle de versão. Suponha que você tenha o gráfico de dependência:
A 1.0
/ \
B 1.0 C 1.1
| |
D 1.0 D 1.1
Qual versão do 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 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, escolhe a versão mais alta especificada por qualquer dependente (D 1.1
no nosso exemplo). Ela é chamada de "mínima" porque D 1.1
é a versão mais antiga que atende aos nossos requisitos. Mesmo que D 1.2
ou versões mais recentes existam, elas não serão selecionadas. O uso do MVS cria um processo de seleção de versão de alta fidelidade e reproduzível.
Versões removidas
O registro pode declarar determinadas versões como revogadas se elas precisarem ser evitadas (por exemplo, por vulnerabilidades de segurança). O Bazel gera um erro ao selecionar uma versão
revogada de um módulo. Para corrigir esse erro, faça upgrade para uma versão mais recente e
não removida ou use a flag
--allow_yanked_versions
para permitir explicitamente a versão removida.
Nível de compatibilidade
Em Go, a proposição do MVS sobre compatibilidade com versões anteriores funciona porque ele 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 é possível porque a versão principal é codificada no caminho do pacote em Go, evitando conflitos de compilação ou vinculação.
No entanto, o Bazel não pode oferecer essas garantias. Por isso, ele precisa do 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, o Bazel pode gerar um erro quando detecta que versões do mesmo módulo com diferentes níveis de compatibilidade existem no gráfico de dependência resolvido.
Modifica
Especifique substituições no arquivo MODULE.bazel
para alterar o comportamento da resolução de módulos do Bazel. Somente as substituições do módulo raiz têm efeito. Se um módulo for usado como uma dependência, as substituições dele serão ignoradas.
Cada substituição é especificada para um determinado nome de módulo, afetando todas as versões dele no gráfico de dependências. Embora apenas as substituições do módulo raiz entrem em vigor, elas podem ser para dependências transitivas de que o módulo raiz não depende diretamente.
Substituição de versão única
O single_version_override
tem várias finalidades:
- Com o atributo
version
, é possível fixar uma dependência em uma versão específica, independente de quais versões da dependência são solicitadas no gráfico de dependências. - Com o atributo
registry
, é possível forçar essa dependência a vir de um registro específico, em vez de seguir o processo normal de seleção de registro. - Com os atributos
patch*
, é possível especificar um conjunto de patches a serem aplicados ao módulo baixado.
Todos esses atributos são opcionais e podem ser combinados.
Substituição de várias versões
Um multiple_version_override
pode ser especificado para permitir que várias versões do mesmo módulo coexistam no
gráfico de dependência resolvido.
É possível especificar uma lista explícita de versões permitidas para o módulo, que precisam estar presentes no gráfico de dependências antes da resolução. É necessário que haja alguma dependência transitiva dependendo de cada versão permitida. Após a resolução, apenas as versões permitidas do módulo permanecem, enquanto o Bazel faz upgrade de outras versões para a versão permitida mais próxima no mesmo nível de compatibilidade. Se não houver uma versão permitida mais alta no mesmo nível de compatibilidade, o Bazel vai gerar um erro.
Por exemplo, se as versões 1.1
, 1.3
, 1.5
, 1.7
e 2.0
existirem no gráfico de dependências antes da resolução e a versão principal for o nível de compatibilidade:
- Uma substituição de várias versões que permite
1.3
,1.7
e2.0
resulta em1.1
sendo atualizado para1.3
,1.5
sendo atualizado para1.7
e outras versões permanecendo as mesmas. - Uma substituição de várias versões que permite
1.5
e2.0
resulta em um erro, já que1.7
não tem uma versão mais recente no mesmo nível de compatibilidade para fazer upgrade. - Uma substituição de várias versões que permite
1.9
e2.0
resulta em um erro, já que1.9
não está presente no gráfico de dependências antes da resolução.
Além disso, os usuários também podem substituir o registro usando o atributo registry
, assim como as substituições de versão única.
Substituições não registradas
As substituições fora do registro removem completamente um módulo da resolução de versão. O Bazel não solicita esses arquivos MODULE.bazel
de um registro, mas do próprio repositório.
O Bazel oferece suporte às seguintes substituições que não são de registro:
Definir repositórios que não representam módulos do Bazel
Com bazel_dep
, é possível definir repositórios que representam outros módulos do Bazel.
Às vezes, é necessário definir um repositório que não represente um módulo do Bazel. Por exemplo, um que contenha um arquivo JSON simples a ser lido como dados.
Nesse caso, você pode usar a diretiva
use_repo_rule
para definir diretamente um repositório
ao invocar uma regra de repositório. Ele só vai ficar visível para o módulo em que foi definido.
Internamente, isso é implementado usando o mesmo mecanismo das extensões de módulo, que permite definir repositórios com mais flexibilidade.
Nomes de repositório e dependências estritas
O nome aparente de um repositório que apoia um
módulo para seus dependentes diretos é o nome do módulo, a menos que o
atributo repo_name
da diretiva bazel_dep
diga o contrário. Isso significa que um módulo só pode encontrar suas dependências diretas. Isso ajuda a evitar falhas acidentais devido a mudanças em dependências transitivas.
O nome canônico de um repositório que oferece suporte a um
módulo é module_name~version
(por exemplo, bazel_skylib~1.0.3
) ou module_name~
(por exemplo, bazel_features~
), dependendo de haver
várias versões do módulo em todo o gráfico de dependências (consulte
multiple_version_override
).
O formato do nome canônico não é uma API de que você deve depender e
está sujeito a mudanças a qualquer momento. Em vez de codificar o nome canônico,
use uma maneira compatível de extraí-lo diretamente do Bazel:
* Em arquivos BUILD e .bzl
, use
Label.repo_name
em uma instância Label
criada com uma string de rótulo fornecida pelo nome aparente do repositório, por exemplo,
Label("@bazel_skylib").repo_name
.
* Ao procurar runfiles, use
$(rlocationpath ...)
ou uma das bibliotecas de runfiles em
@bazel_tools//tools/{bash,cpp,java}/runfiles
ou, para um conjunto de regras rules_foo
,
em @rules_foo//foo/runfiles
.
* Ao interagir com o Bazel de uma ferramenta externa, como um ambiente de desenvolvimento integrado ou um servidor de linguagem, use o comando bazel mod dump_repo_mapping
para receber o mapeamento de nomes aparentes para nomes canônicos de um determinado conjunto de repositórios.
As extensões de módulo também podem introduzir outros repositórios no escopo visível de um módulo.