Dependências

Um A de destino depende de um B de destino se B for necessário para o A no tempo de compilação ou execução. A relação depende induz um gráfico acíclico dirigido (DAG, na sigla em inglês) sobre os objetivos 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 tamanho 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 tamanho pelo gráfico.

Na verdade, no contexto das 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 não é necessário fazer essa distinção, mas é útil para a discussão abaixo.

Dependências reais e declaradas

Um X de destino depende 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 um dos outros tipos de tarefas que ocorrem rotineiramente durante um build.

Um X de destino tem uma dependência declarada na Y de destino se houver uma borda de dependência de X a Y no pacote de X.

Para builds corretos, o gráfico das dependências A reais 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 todas as regras para o sistema de build, e nada mais.

O não cumprimento desse princípio causa um comportamento indefinido: o build pode falhar, mas, pior, o build pode depender de algumas operações anteriores ou das dependências declaradas transitivas que o destino tem. Ele verifica se há dependências ausentes e relata erros, mas não é possível que essa verificação seja concluída em todos os casos.

Você não precisa (nem deve) 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 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, uma borda 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 é possível rastrear mudanças, conforme mostrado neste exemplo de linha do tempo:

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 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();
}
      
Gráfico de dependências declarados com setas conectando a, b e c
Gráfico de dependências declaradas
Gráfico de dependências real que corresponde ao gráfico de dependências
                  declarado com setas conectando a, b e c
Gráfico de dependências real

As dependências declaradas se aproximam mais das 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 declará-lo no arquivo de build a/BUILD.

a / a.in  
        import b;
        import c;
        b.foo();
        c.garply();
      
 
Gráfico de dependências declarados com setas conectando a, b e c
Gráfico de dependências declaradas
Gráfico de dependências real com setas conectando a, b e c. Uma seta também conecta A a C. Não corresponde ao
                  gráfico de dependências declarado
Gráfico de dependências real

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, interrompendo 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();
      }
      
Gráfico de dependências declarados com setas conectando a e b.
                  b não se conecta mais a c, o que interrompe a conexão de a com c.
Gráfico de dependências declaradas
Gráfico de dependência real que mostra uma conexão a b e c, mas b não se conecta mais a c
Gráfico de dependências real

O gráfico de dependências declarado agora é uma aproximação das dependências reais, mesmo quando fechado de modo transitivo. 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 tenha sido declarada corretamente no arquivo BUILD.

Tipos de dependências

A maioria das regras de criação 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 outros atributos para tipos de dependências específicos das regras, por exemplo, compiler ou resources. Isso está detalhado na Enciclopédia de build.

srcs dependências

Arquivos consumidos diretamente pela regra ou pelas regras que geram arquivos de origem.

deps dependências

Regra que aponta para módulos compilados separadamente que fornecem arquivos principais, símbolos, bibliotecas, dados etc.

data dependências

Um destino de compilação pode precisar de alguns arquivos de dados para ser executado corretamente. Esses arquivos de dados não são código-fonte e 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. Quando você cria o teste de unidade, 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 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 consultar 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 marcadores para referenciar diretórios

Ao analisar nossos arquivos BUILD, talvez você note 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 recomendadodata = ["//data/regression:unittest/."]

Não recomendadodata = ["testdata/."]

Não recomendadodata = ["testdata/"]

Isso parece conveniente, principalmente para testes, porque permite que um teste use todos os arquivos de dados do 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 a compilação (ou teste). Quando você especifica um diretório, o sistema de build executa uma recriação somente 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, porque essas mudanças não afetam o diretório delimitado. 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.

Recomendadodata = glob(["testdata/**"])

Infelizmente, existem alguns cenários em que os rótulos 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. É necessário usar rótulos de diretório nesse caso, mas tenha cuidado com o risco associado de recompilaçõ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, é possível referenciar o rótulo my_data como a dependência de dados no teste.

Arquivos BUILD Visibilidade