Um A
de destino depende de um B
de destino se B
for necessário para A
no momento de build ou
de execução. A relação depende de induz um
gráfico acíclico dirigido
(DAG) sobre os destinos e é chamado de gráfico de dependência.
As dependências diretas de um destino são os outros destinos acessíveis por um caminho de comprimento 1 no gráfico de dependências. As dependências transitivas de um destino são aqueles 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 gráfico de dependências reais e o gráfico 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
depende do destino Y
se Y
precisa 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 ocorra rotineiramente durante um build.
Um destino X
tem uma dependência declarada no destino Y
se houver uma borda 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, cada par de
nós x --> y
conectados diretamente em A também precisa estar conectado diretamente em
D. Podemos dizer que D é uma superaproximaçã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.
A não observação desse princípio causa um comportamento indefinido: o build pode falhar, mas, pior ainda, o build pode depender de algumas operações anteriores ou de dependências declaradas transitivamente que o destino tenha. O Bazel verifica se há dependências ausentes e informa erros, mas essa verificação não pode ser concluída em todos os casos.
Não é necessário (e não é recomendável) tentar listar tudo o que é 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 todas 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
borda 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 há como acompanhar as mudanças, conforme mostrado na linha do tempo de exemplo a seguir:
1. As dependências declaradas correspondem às dependências reais
No começo, tudo funciona. O código no pacote a
usa o código no pacote b
.
O código no pacote b
usa o código no pacote c
. 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 são uma aproximação das dependências reais. Tudo bem.
2. Como adicionar uma dependência não declarada
Um risco latente é introduzido quando alguém adiciona código ao a
que cria uma
dependência real direta em c
, mas esquece de declará-lo no arquivo de build
a/BUILD
.
a / a.in |
|
---|---|
import b; import c; b.foo(); c.garply(); |
|
As dependências declaradas não são mais aproximadas demais das dependências reais.
Isso pode ser criado corretamente, porque os fechamentos transitivos dos dois gráficos são iguais,
mas mascaram um problema: a
tem uma dependência real, mas não declarada, em c
.
3. Divergência entre gráficos de dependência declarados e reais
O risco é revelado quando alguém refatoriza 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 aproximação das dependências reais, mesmo quando fechado transitivamente. O build provavelmente vai falhar.
O problema poderia ter sido evitado garantindo que a dependência real de
a
para c
introduzida na etapa 2 fosse 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 atributos adicionais para tipos específicos de
dependências, por exemplo, compiler
ou resources
. Eles são detalhados na
Enciclopédia do Build.
Dependências de srcs
Arquivos consumidos diretamente pela regra ou regras que geram arquivos de origem.
Dependências de deps
Regra que aponta para módulos compilados separadamente que fornecem arquivos de cabeçalho, símbolos, bibliotecas, dados etc.
Dependências de data
Um destino de build pode precisar de alguns arquivos de dados para ser executado corretamente. Esses arquivos de dados não são 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 arquivos listados como
data
estão disponíveis. Portanto, 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,
é possível fazer referência a esses arquivos juntando 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ê pode 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 estar ciente do
conjunto completo de arquivos que são entradas para o build (ou teste). Quando você especifica
um diretório, o sistema de build executa uma recriação apenas quando o 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 que os contém.
Em vez de especificar diretórios como entradas para o sistema de build, enumere o conjunto de arquivos contidos neles, explicitamente ou usando a
função glob()
. Use **
para forçar o
glob()
a ser recursivo.
Recomendado:
data = glob(["testdata/**"])
Infelizmente, há alguns cenários em que é necessário usar rótulos de diretório.
Por exemplo, se o diretório testdata
contiver arquivos com nomes que não
estão em conformidade com a sintaxe de rótulo,
a enumeração explícita de arquivos ou o uso da
função glob()
produzirá um erro de rótulos
inválidos. Nesse caso, é necessário usar rótulos de diretório, mas tome cuidado com o
risco associado de reconstruçõ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 pai 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 fazer referência ao identificador my_data
como a dependência de dados no teste.
Arquivos BUILD | Visibilidade |