Um destino A
depende de um destino B
se B
for necessário para A
no momento da criação ou
da execução. A relação depends upon induz um gráfico acíclico dirigido (DAG) em destinos, chamado de gráfico de dependência.
As dependências diretas de um destino são outros destinos alcançáveis por um caminho de comprimento 1 no gráfico de dependência. As dependências transitivas de um destino são os destinos de que ele depende por um caminho de qualquer comprimento no gráfico.
Na verdade, no contexto de builds, há dois gráficos de dependência: o de dependências reais e o de dependências declaradas. Na maioria das vezes, os dois gráficos são tão semelhantes que essa distinção não precisa ser feita, mas é útil para a discussão abaixo.
Dependências reais e declaradas
Um destino X
é realmente dependente do destino Y
se Y
precisar estar presente,
criado e atualizado para que X
seja criado corretamente. Criado pode significar gerado, processado, compilado, vinculado, arquivado, compactado, executado ou qualquer outro tipo de tarefa que ocorre rotineiramente durante um build.
Um destino X
tem uma dependência declarada do destino Y
se houver uma aresta de dependência de X
para Y
no pacote de X
.
Para builds corretos, o gráfico de dependências reais A precisa ser um subgráfico do gráfico de dependências declaradas D. Ou seja, todo par de nós x --> y
diretamente conectados em A também precisa estar diretamente conectado em D. Pode-se dizer que D é uma superestimação de A.
Os gravadores de arquivos BUILD
precisam declarar explicitamente todas as dependências diretas reais de cada regra para o sistema de build, e nada mais.
A não observância desse princípio causa um comportamento indefinido: a build pode falhar, mas, pior ainda, ela pode depender de algumas operações anteriores ou de dependências transitivas declaradas que o destino tem. O Bazel verifica se há dependências ausentes e informa erros, mas essa verificação não é completa em todos os casos.
Não é necessário (e nem recomendado) tentar listar tudo o que foi importado indiretamente,
mesmo que seja necessário para A
no momento da execução.
Durante um build do destino X
, a ferramenta de build inspeciona todo o fechamento transitivo de dependências de X
para garantir que as mudanças nesses destinos sejam refletidas no resultado final, reconstruindo os intermediários conforme necessário.
A natureza transitiva das dependências leva a um erro comum. Às vezes, o código em um arquivo pode usar o código fornecido por uma dependência indireta, uma aresta transitiva, mas não direta, no gráfico de dependência declarado. As dependências indiretas não aparecem no arquivo BUILD
. Como a regra não depende diretamente do provedor, não é possível rastrear mudanças, como mostrado na linha do tempo de exemplo a seguir:
1. As dependências declaradas correspondem às reais
No início, tudo funciona. O código no pacote a
usa código no pacote b
.
O código no pacote b
usa o código no pacote c
e, portanto, a
depende transitivamente de c
.
a/BUILD |
b/BUILD |
---|---|
rule( name = "a", srcs = "a.in", deps = "//b:b", ) |
rule( name = "b", srcs = "b.in", deps = "//c:c", ) |
a / a.in |
b / b.in |
import b; b.foo(); |
import c; function foo() { c.bar(); } |
|
|
As dependências declaradas superestimam as dependências reais. Tudo bem.
2. Como adicionar uma dependência não declarada
Um risco latente é introduzido quando alguém adiciona código a a
que cria uma dependência real direta em c
, mas esquece de declarar isso no arquivo de build a/BUILD
.
a / a.in |
|
---|---|
import b; import c; b.foo(); c.garply(); |
|
|
|
As dependências declaradas não superestimam mais as dependências reais.
Isso pode ser criado sem problemas, porque os fechamentos transitivos dos dois gráficos são iguais, mas mascara um problema: a
tem uma dependência real, mas não declarada, de c
.
3. Divergência entre os gráficos de dependência declarados e reais
O risco é revelado quando alguém refatora b
para que ele não dependa mais de c
, quebrando inadvertidamente a
sem culpa própria.
b/BUILD |
|
---|---|
rule( name = "b", srcs = "b.in", deps = "//d:d", ) |
|
b / b.in |
|
import d; function foo() { d.baz(); } |
|
|
|
O gráfico de dependência declarado agora é uma subaproximação das dependências reais, mesmo quando transitivamente fechado. É provável que o build falhe.
O problema poderia ter sido evitado se a dependência real de
a
para c
introduzida na etapa 2 tivesse sido declarada corretamente no arquivo BUILD
.
Tipos de dependências
A maioria das regras de build tem três atributos para especificar diferentes tipos de dependências genéricas: srcs
, deps
e data
. Elas são explicadas abaixo. Para mais detalhes, consulte Atributos comuns a todas as regras.
Muitas regras também têm outros atributos para tipos específicos de dependências, por exemplo, compiler
ou resources
. Esses detalhes estão na Build Encyclopedia.
Dependências do srcs
Arquivos consumidos diretamente pela regra ou regras que geram arquivos de origem.
Dependências do deps
Regra que aponta para módulos compilados separadamente que fornecem arquivos de cabeçalho, símbolos, bibliotecas, dados etc.
Dependências do data
Um destino de build pode precisar de alguns arquivos de dados para ser executado corretamente. Esses arquivos de dados não são código-fonte: eles não afetam a forma como o destino é criado. Por exemplo, um teste de unidade pode comparar a saída de uma função com o conteúdo de um arquivo. Ao criar o teste de unidade, você não precisa do arquivo, mas precisa dele ao executar o teste. O mesmo vale para ferramentas iniciadas durante a execução.
O sistema de build executa testes em um diretório isolado em que apenas os arquivos listados como
data
estão disponíveis. Assim, se um binário/biblioteca/teste precisar de alguns arquivos para ser executado,
especifique-os (ou uma regra de build que os contenha) em data
. Exemplo:
# I need a config file from a directory named env:
java_binary(
name = "setenv",
...
data = [":env/default_env.txt"],
)
# I need test data from another directory
sh_test(
name = "regtest",
srcs = ["regtest.sh"],
data = [
"//data:file1.txt",
"//data:file2.txt",
...
],
)
Esses arquivos estão disponíveis usando o caminho relativo path/to/data/file
. Nos testes, você pode se referir a esses arquivos combinando os caminhos do diretório de origem do teste e o caminho relativo ao espaço de trabalho. Por exemplo, ${TEST_SRCDIR}/workspace/path/to/data/file
.
Como usar rótulos para referenciar diretórios
Ao analisar nossos arquivos BUILD
, você vai notar que alguns rótulos data
se referem a diretórios. Esses rótulos terminam com /.
ou /
, como nestes exemplos, que não devem ser usados:
Não recomendado: data = ["//data/regression:unittest/."]
Não recomendado: data = ["testdata/."]
Não recomendado: data = ["testdata/"]
Isso parece conveniente, principalmente para testes, porque permite que um teste use todos os arquivos de dados no diretório.
Mas tente não fazer isso. Para garantir reconstruções incrementais corretas (e
reexecução de testes) após uma mudança, o sistema de build precisa conhecer o
conjunto completo de arquivos que são entradas para o build (ou teste). Quando você especifica um diretório, o sistema de build só faz uma reconstrução quando o próprio diretório muda (devido à adição ou exclusão de arquivos), mas não consegue detectar edições em arquivos individuais, já que essas mudanças não afetam o diretório de inclusão.
Em vez de especificar diretórios como entradas para o sistema de build, enumere o conjunto de arquivos contidos neles, de forma explícita ou usando a função glob()
. Use **
para forçar a recursão de glob()
.
Recomendado: data = glob(["testdata/**"])
Infelizmente, há alguns cenários em que os rótulos de diretório precisam ser usados.
Por exemplo, se o diretório testdata
tiver arquivos com nomes que não estão de acordo com a sintaxe de rótulo, a enumeração explícita de arquivos ou o uso da função glob()
vai gerar um erro de rótulos inválidos. Nesse caso, você precisa usar rótulos de diretório, mas tome cuidado com o risco associado de recriações incorretas descrito acima.
Se você precisar usar rótulos de diretório, não se esqueça de que não é possível se referir ao pacote principal com um caminho ../
relativo. Em vez disso, use um caminho absoluto como //data/regression:unittest/.
.
Qualquer regra externa, como um teste, que precise usar vários arquivos precisa declarar explicitamente a dependência de todos eles. Use filegroup()
para
agrupar arquivos no arquivo BUILD
:
filegroup(
name = 'my_data',
srcs = glob(['my_unittest_data/*'])
)
Em seguida, você pode referenciar o rótulo my_data
como a dependência de dados no seu teste.
Arquivos BUILD | Visibilidade |