Um módulo do Bazel é um projeto do Bazel que pode ter várias versões, cada uma delas 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, 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")
Para realizar a resolução do 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é que ele
descubra todo o gráfico de dependência.
Por padrão, o Bazel então 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õ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 das versões 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 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 e somente se o mesmo for válido quando
comparadas como versões de 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 versões. 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 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 é a versão mais antiga que pode atender aos nossos requisitos. Mesmo que D 1.2 ou mais recente exista, não a selecionamos. O uso do MVS cria um processo de seleção de versão de alta fidelidade e reproduzível.
Versões retiradas
O registro pode declarar determinadas versões como retiradas se elas precisarem ser evitadas (como vulnerabilidades de segurança). O Bazel gera um erro ao selecionar uma versão retirada de um módulo. Para corrigir esse erro, faça upgrade para uma versão mais recente,
não retirada ou use a
--allow_yanked_versions
flag para permitir explicitamente a versão retirada.
Nível de compatibilidade
No Go, a suposição do MVS sobre a 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, por sua vez, é possível codificando a versão principal no caminho do pacote no Go, para que não haja conflitos de tempo de compilação ou vinculação.
No entanto, o Bazel não pode fornecer essas garantias. Portanto, 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
module() diretiva. Com essas informações, o Bazel pode gerar um erro quando detecta que versões do mesmo módulo com níveis de compatibilidade diferentes existem no gráfico de dependência resolvido.
Modificações
Especifique modificações no arquivo MODULE.bazel para alterar o comportamento da resolução do módulo do Bazel. Somente as modificações do módulo raiz entram em vigor. Se um módulo for usado como dependência, as modificações serão ignoradas.
Cada modificação é especificada para um determinado nome de módulo, afetando todas as versões dele no gráfico de dependência. Embora apenas as modificaçõ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.
Modificaçã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 sejam solicitadas no gráfico de dependência. - 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.
Todos esses atributos são opcionais e podem ser combinados.
Modificação de várias versões
Uma multiple_version_override
pode ser especificada 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ência 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 do módulo 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ência antes da resolução e a versão principal for o nível de compatibilidade:
- Uma modificação de várias versões que permite
1.3,1.7e2.0resulta no upgrade de1.1para1.3,1.5para1.7e outras versões permanecem as mesmas. - Uma modificação de várias versões que permite
1.5e2.0resulta em um erro, já que1.7não tem uma versão mais alta no mesmo nível de compatibilidade para fazer upgrade. - Uma modificação de várias versões que permite
1.9e2.0resulta em um erro, já que1.9não está presente no gráfico de dependência antes da resolução.
Além disso, os usuários também podem modificar o registro usando o atributo registry, de maneira semelhante às modificações de versão única.
Modificações não registradas
As modificações não registradas 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 modificações não registradas:
Nomes de repositório e dependências estritas
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). Para módulos com uma
modificação não registrada, substitua a parte version com a string override. O formato de nome canônico não é uma API de que você deve depender e está sujeito a mudanças a qualquer momento.
O nome aparente de um repositório que faz backup de um
módulo para seus dependentes diretos é o nome do módulo, a menos que o
repo_name atributo da bazel_dep
diretiva 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 em dependências transitivas.
As extensões de módulo também podem introduzir outros repositórios no escopo visível de um módulo.