Um A
de destino depende de um B
de destino se B
for necessário para a A
no tempo de criação ou
execução. A relação depende de induz um gráfico acíclico dirigido (DAG, na sigla em inglês) sobre os destinos e é chamado de gráfico de dependência.
As dependências diretas de um destino são outros destinos acessíveis por um caminho de tamanho 1 no gráfico de dependência. As dependências transitivas de um destino são aqueles dos quais ele depende por um caminho de qualquer tamanho 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 X
de destino depende do Y
de destino 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 um dos outros tipos de tarefas que ocorrem rotineiramente durante um build.
Um X
de destino tem uma dependência declarada no destino Y
se houver uma borda
de dependência de X
a 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 conectados diretamente x --> y
em A também precisa estar diretamente conectado em
D. Pode-se dizer que D é uma aproximação excessiva de A.
Os gravadores de arquivos BUILD
precisam declarar explicitamente todas as dependências diretas
reais de cada regra para o sistema de compilação, e nada mais.
A não observação desse princípio causa um comportamento indefinido: o build pode falhar, mas pior, ele pode depender de algumas operações anteriores ou de dependências declaradas transitivas que o destino tem. Ele verifica se há dependências ausentes e relata erros, mas não é possível concluir essa verificação em todos os casos.
Você não precisa (nem deve) tentar listar tudo indiretamente importado,
mesmo que necessário para A
no tempo de execução.
Durante um build do X
de destino, a ferramenta de build inspeciona todo o fechamento transitivo
das dependências de X
para garantir que qualquer mudança nesses destinos seja
refletida no resultado final, recriando 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, ou seja, 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 rastrear as mudanças, conforme mostrado no
cronograma de exemplo abaixo:
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
. Portanto, o 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 aproximadas em relação às dependências reais. Tudo bem.
2. Como adicionar uma dependência não declarada
Um risco latente é introduzido quando alguém adiciona um código a a
que cria uma
dependência real direta em c
, mas se 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 se aproximam mais das 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, de c
.
3. Divergência entre gráficos de dependência declarados e reais
O perigo é revelado quando alguém refatora b
para que ele não dependa mais de
c
, inadvertidamente quebrando 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ências declaradas agora é uma subaproximação das dependências reais, mesmo quando transitivamente fechado. O build provavelmente falhará.
O problema poderia ter sido evitado garantindo que a dependência real de
a
a c
introduzida na etapa 2 tenha 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
. Eles são explicados a seguir. Para
mais detalhes, consulte
Atributos comuns a todas as regras.
Muitas regras também têm atributos adicionais para tipos de
dependências específicas da regra, por exemplo, compiler
ou resources
. Elas estão detalhadas na
enciclopédia de build.
srcs
de dependências
Arquivos consumidos diretamente pela regra ou por regras que geram arquivos de origem.
deps
de dependências
Regra que aponta para módulos compilados separadamente que fornecem arquivos principais, símbolos, bibliotecas, dados etc.
data
de dependências
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 criação do destino. 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 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. 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,
é possível se referir a esses arquivos mesclando 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 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, especialmente para testes, porque permite que um teste use todos os arquivos de dados no diretório.
Mas tente não fazer isso. Para garantir recompilações incrementais corretas (e
reexecução de testes) após uma mudança, o sistema de compilação 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 próprio diretório
muda (devido à adição ou exclusão de arquivos). No entanto, ele não detecta
edições em arquivos individuais, porque essas mudanças não afetam o diretório.
Em vez de especificar diretórios como entradas para o sistema de build, é preciso enumerar o conjunto de arquivos contidos neles explicitamente ou usando a função glob()
. Use **
para forçar a
glob()
a ser recursiva.
Recomendado —
data = glob(["testdata/**"])
Infelizmente, há alguns cenários em que os marcadores de diretório precisam ser usados.
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. Você precisa usar rótulos de diretório nesse caso, mas tenha cuidado com o
risco associado de recriações incorretas descritas acima.
Se você precisar usar rótulos 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. Você pode usar 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 teste.
Arquivos BUILD | Visibilidade |