Dependências

Informar um problema Ver código-fonte

Um destino A depende de um destino B se B for necessário para A no tempo de compilação ou execução. A relação de dependências 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 destinos de que ele depende por um caminho de qualquer duração pelo gráfico.

Na verdade, no contexto dos 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

Uma X de destino depende do destino Y se a Y precisar estar presente, construída e atualizada para que a X seja criada corretamente. Build pode significar gerado, processado, compilado, vinculado, arquivado, compactado, executado ou qualquer outro tipo de tarefas que ocorrem rotineiramente durante uma compilação.

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

Para versões corretas, o gráfico das dependências reais A precisa ser um subgráfico do gráfico das dependências declaradas D. Ou seja, cada par de nós conectados diretamente x --> y em A também deve estar diretamente conectado em D. É possível dizer que D é uma aproximação de A.

Os gravadores de arquivos BUILD precisam declarar explicitamente todas as dependências diretas reais para cada regra ao sistema de compilação e nenhuma outra.

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

Você não precisa (e não deve) tentar listar tudo importado indiretamente, mesmo que ele seja necessário por A no momento da execução.

Durante um build de destino X, a ferramenta de build inspeciona o fechamento transitivo de 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 causa um erro comum. Às vezes, o código em um arquivo pode usar 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 rastrear as alterações, como mostrado neste exemplo de cronograma:

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 do pacote b. O código no pacote b usa o código do pacote c. Portanto, a depende de forma transitiva 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ência declarado com setas que conectam a, b e c
Gráfico de dependência declarado
Gráfico de dependência real que corresponde ao gráfico de dependência declarado com setas conectando a, b e c
Gráfico de dependências real

As dependências declaradas superam as dependências reais. Tudo certo.

2. Como adicionar uma dependência não declarada

Um perigo latente é introduzido quando alguém adiciona um 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();
      
 
Gráfico de dependência declarado com setas que conectam a, b e c
Gráfico de dependência declarado
Gráfico de dependência real com setas que conectam a, b e c. Uma seta também conecta A a C. Isso não corresponde ao
                  gráfico de dependências declarado
Gráfico de dependências real

As dependências declaradas não superam mais as dependências reais. Isso pode ser normal, 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 perigo é revelado quando alguém refatora o b para que não dependa mais do c, interrompendo inadvertidamente o 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();
      }
      
Gráfico de dependência declarado com setas que conectam a e b.
                  b não se conecta mais a c, o que interrompe a conexão de c a c
Gráfico de dependência declarado
Gráfico de dependência real que mostra uma conexão com b e c,
                  mas b não se conecta mais com 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 temporariamente. É provável que o build falhe.

O problema pode ter sido revertido ao garantir 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 compilação 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 específicos de dependências, por exemplo, compiler ou resources. Elas são detalhadas na Criação da enciclopédia.

srcs dependências

Arquivos consumidos diretamente pelas regras que geram arquivos de origem.

deps dependências

Regra que aponta para módulos compilados separadamente fornecendo arquivos de cabeçalho, símbolos, bibliotecas, dados etc.

data 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 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 se aplica a ferramentas iniciadas durante a execução.

O sistema de compilação 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 compilação 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. Em testes, você pode 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 você não deve usar:

Não recomendadodata = ["//data/regression:unittest/."]

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

Não recomendadodata = ["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 a recriação incremental de testes incrementais e uma nova execuçã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 compilação realiza uma recriação apenas quando o próprio diretório é alterado (devido à adição ou exclusão de arquivos), mas não consegue detectar edições em arquivos individuais, uma vez que essas mudanças não afetam o diretório incluído. Em vez de especificar diretórios como entradas para o sistema de compilação, enumera o conjunto de arquivos contidos neles, seja de forma explícita ou usando a função glob(). Use ** para forçar a glob() a ser recursiva.

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

Infelizmente, há alguns casos em que os identificadores de diretório precisam ser usados. Por exemplo, se o diretório testdata tiver 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álido. Nesse caso, é preciso usar rótulos de diretório, 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 precisa usar vários arquivos precisa declarar explicitamente a dependência em todos eles. É possível usar filegroup() para agrupar arquivos no 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