Um destino A depende de um destino B se B for necessário para A no momento da criação ou
execução. A relação depende de induz um
gráfico acíclico dirigido
(DAG, na sigla em inglês) em relação aos destinos e é chamado de gráfico de dependências.
As dependências diretas de um destino são os outros destinos que podem ser acessados por um caminho de comprimento 1 no gráfico de dependências. As dependências transitivas de um destino são aqueles 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ências: 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 é 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 ocorra rotineiramente durante um build.
Um destino X tem uma dependência declarada no 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 de
o gráfico de dependências declaradas D. Ou seja, cada par de
nós conectados diretamente x --> y em A também precisa estar conectado diretamente em
D. Pode-se 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, e nada mais.
A falha em observar esse princípio causa um comportamento indefinido: o build pode falhar, mas, pior ainda, ele pode depender de algumas operações anteriores ou de dependências declaradas transitivas que o destino tenha. O Bazel verifica se há dependências ausentes e informa erros, mas não é possível que essa verificação seja concluída em todos os casos.
Não é necessário (e nem recomendado) 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, recriando 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ências declarado. As dependências indiretas
não aparecem no arquivo BUILD. Como a regra não depende diretamente do provedor, não há como rastrear mudanças, conforme mostrado na linha do tempo de exemplo a seguir:
1. As dependências declaradas correspondem às dependências reais
No início, 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, e, portanto, a transitivamente
depende 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 superaproximam as dependências reais. Tudo está 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 declará-la no arquivo de build
a/BUILD.
a / a.in |
|
|---|---|
import b;
import c;
b.foo();
c.garply();
|
|
|
|
|
As dependências declaradas não superaproximam mais as dependências reais.
Isso pode ser criado corretamente, 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, em c.
3. Divergência entre gráficos de dependências declaradas e reais
O risco é revelado quando alguém refatora b para que ele não dependa mais de
c, quebrando a inadvertidamente 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ências declarado agora é uma subaproximação das dependências reais, mesmo quando fechado transitivamente. É provável que o build falhe.
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. Eles são explicados abaixo. Para
mais detalhes, consulte
Atributos comuns a todas as regras.
Muitas regras também têm outros atributos para tipos de dependências específicos da regra, por exemplo, compiler ou resources. Eles são detalhados na
Enciclopédia de builds.
Dependências srcs
Arquivos consumidos diretamente pela regra ou regras que geram arquivos de origem.
Dependências deps
Regra que aponta para módulos compilados separadamente, fornecendo arquivos de cabeçalho, símbolos, bibliotecas, dados etc.
Dependências 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 ao conteúdo de um arquivo. Ao criar o teste de unidade, você não precisa do arquivo, mas ele é necessário ao executar o teste. O mesmo se aplica a 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. 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. Por 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 se referir a esses arquivos unindo 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 identificadores para referenciar diretórios
Ao analisar nossos arquivos BUILD, você pode notar que alguns identificadores data se referem a diretórios. Esses identificadores terminam com /. ou /, como estes 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 recriaçõ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 realiza uma recriação somente quando o próprio diretório
muda (devido à adição ou exclusão de arquivos), mas não é possível 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, você precisa
enumerar o conjunto de arquivos contidos neles, explicitamente ou usando a
glob() função. Use ** para forçar a recursão do
glob().
Recomendado —
data = glob(["testdata/**"])
Infelizmente, há alguns cenários em que os identificadores de diretório precisam ser usados.
Por exemplo, se o diretório testdata contiver arquivos cujos nomes não
estiverem em conformidade com a sintaxe do identificador,
a enumeração explícita de arquivos ou o uso da
glob() função produzirá um erro de identificadores inválidos. Nesse caso, é necessário usar identificadores de diretório, mas esteja ciente do
risco associado de recriações incorretas descritas acima.
Se você precisar usar identificadores de diretório, lembre-se 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. É possível usar filegroup() para
agrupar arquivos no arquivo BUILD:
filegroup(
name = 'my_data',
srcs = glob(['my_unittest_data/*'])
)
Em seguida, você pode referenciar o identificador my_data como a dependência de dados no teste.
| Arquivos BUILD | Visibilidad |