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 npm, um módulo 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, que declara o nome,
a versão, a lista de dependências diretas e outras informações. 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")
Consulte a lista completa de diretivas disponíveis em arquivos MODULE.bazel
.
Para executar a resolução de módulo, 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 a ser usada. O Bazel representa cada módulo com um repositório e consulta o registro novamente para saber como definir cada um dos repositórios.
Formato da versão
O Bazel tem um ecossistema diversificado, e os projetos usam vários esquemas de controle de versão. O
mais conhecido é o SemVer, mas também há
projetos importantes que usam esquemas diferentes, como o
Abseil, que
tem 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 é relaxado para permitir qualquer número de segmentos. - No SemVer, cada um dos segmentos na parte "release" precisa ser apenas de dígitos. No Bazel, isso é relaxado 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 dos aumentos de versão principal, secundária e de patch não é aplicada. No entanto, consulte o nível de compatibilidade para mais detalhes sobre como denotamos 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 da SemVer a
e b
comparam a < b
se e somente se o mesmo for verdadeiro quando
forem comparadas como versões de módulo do Bazel.
Seleção da versão
Considere o problema de dependência de diamante, um item básico no espaço de gerenciamento de dependências 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
Minimal Version Selection
(MVS) introduzido no sistema de módulos Go. O MVS assume 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 recente
especificada por qualquer dependente (D 1.1
no nosso exemplo). Ele é chamado de "mínimo"
porque D 1.1
é a versão mais antiga que pode atender aos nossos requisitos.
Mesmo que D 1.2
ou uma versão mais recente exista, não a selecionamos. O uso do MVS cria um
processo de seleção de versão de alta fidelidade e reprodutível.
Versões retiradas
O registro pode declarar determinadas versões como retiradas se elas precisarem ser evitadas,
como em caso de vulnerabilidades de segurança. O Bazel gera um erro ao selecionar uma
versão removida de um módulo. Para corrigir esse erro, faça upgrade para uma versão mais recente
que não tenha sido removida ou use a flag
--allow_yanked_versions
para permitir explicitamente a versão removida.
Nível de compatibilidade
No Go, a suposição da MVS sobre compatibilidade com versões anteriores funciona porque ela 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.x
e A 2.x
são considerados módulos distintos e podem
coexistir no gráfico de dependência resolvido. Isso é possível ao
codificar a versão principal no caminho do pacote em Go, para que não haja
conflitos no momento da compilação ou vinculação.
O Bazel, no entanto, 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 existem versões do mesmo módulo com diferentes níveis de compatibilidade
no gráfico de dependência resolvido.
Modifica
Especifique substituições no arquivo MODULE.bazel
para mudar o comportamento da resolução do módulo
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 no gráfico de dependências. Embora apenas as substituições do módulo raiz tenham efeito, elas podem ser para dependências transitivas das quais 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, independentemente 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 transferido por download.
Esses atributos são opcionais e podem ser combinados entre si.
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ências resolvido.
É possível especificar uma lista explícita de versões permitidas para o módulo, que precisa estar presente no gráfico de dependências antes da resolução. É necessário que exista 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 do módulo para a versão mais recente permitida mais próxima com o mesmo nível de compatibilidade. Se não houver uma versão permitida mais recente 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 no upgrade de1.1
para1.3
, de1.5
para1.7
e de outras versões. - Uma substituição de várias versões que permite
1.5
e2.0
resulta em um erro, porque1.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, porque1.9
não está presente no gráfico de dependência antes da resolução.
Além disso, os usuários também podem substituir o registro usando o atributo registry
,
de forma semelhante às substituições de versão única.
Substituições que não são do registro
As substituições que não são 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 sem 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 para ser lido como dados.
Nesse caso, você pode usar a diretiva
use_repo_rule
para definir diretamente um repositório
invocando uma regra de repositório. Esse repositório só fica visível para o módulo em que é
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 rígidas
O nome aparente de um repositório que dá suporte a um
módulo para os dependentes diretos é definido como 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 as dependências
diretas. Isso ajuda a evitar interrupções acidentais devido a mudanças nas
dependências transitivas.
O nome canônico de um repositório que faz backup de um
módulo é module_name~version
(por exemplo, bazel_skylib~1.0.3
) ou module_name~
(por exemplo, bazel_features~
), dependendo se há
várias versões do módulo em todo o gráfico de dependências (consulte
multiple_version_override
).
O formato de nome canônico não é uma API da qual você precisa depender e
está sujeito a alterações a qualquer momento. Em vez de fixar o nome canônico,
use uma maneira compatível para recebê-lo diretamente do Bazel:
* Nos arquivos BUILD e .bzl
, use
Label.repo_name
em uma instância Label
criada a partir de 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 uma regra 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.