Un destino A
depende de un destino B
si A
necesita B
en el momento de la compilación o la ejecución. La relación depends upon induce un grafo acíclico dirigido (DAG) sobre los destinos, y se denomina grafo de dependencias.
Las dependencias directas de un destino son aquellos otros destinos a los que se puede llegar con una ruta de longitud 1 en el gráfico de dependencias. Las dependencias transitivas de un destino son aquellos destinos de los que depende a través de una ruta de cualquier longitud en el gráfico.
De hecho, en el contexto de las compilaciones, hay dos gráficos de dependencias: el gráfico de dependencias reales y el gráfico de dependencias declaradas. La mayoría de las veces, los dos gráficos son tan similares que no es necesario hacer esta distinción, pero es útil para el análisis que se presenta a continuación.
Dependencias reales y declaradas
Un destino X
es realmente dependiente del destino Y
si Y
debe estar presente, compilado y actualizado para que X
se compile correctamente. Compilado podría significar generado, procesado, compilado, vinculado, archivado, comprimido, ejecutado o cualquier otro tipo de tarea que se realice de forma rutinaria durante una compilación.
Un destino X
tiene una dependencia declarada en el destino Y
si hay un borde de dependencia de X
a Y
en el paquete de X
.
Para que las compilaciones sean correctas, el gráfico de dependencias reales A debe ser un subgráfico del gráfico de dependencias declaradas D. Es decir, cada par de nodos x --> y
conectados directamente en A también debe estar conectado directamente en D. Se puede decir que D es una sobreaproximación de A.
Los escritores de archivos BUILD
deben declarar explícitamente todas las dependencias directas reales de cada regla al sistema de compilación, y nada más.
Si no se observa este principio, se produce un comportamiento indefinido: la compilación puede fallar, pero, lo que es peor, puede depender de algunas operaciones previas o de dependencias declaradas transitivas que el destino tenga. Bazel verifica si faltan dependencias y registra errores, pero no es posible que esta verificación sea completa en todos los casos.
No es necesario (ni se recomienda) intentar enumerar todo lo que se importa de forma indirecta, incluso si A
lo necesita en el tiempo de ejecución.
Durante la compilación del destino X
, la herramienta de compilación inspecciona el cierre transitivo completo de las dependencias de X
para garantizar que cualquier cambio en esos destinos se refleje en el resultado final, y vuelve a compilar los elementos intermedios según sea necesario.
La naturaleza transitiva de las dependencias genera un error común. A veces, el código de un archivo puede usar código proporcionado por una dependencia indirecta, es decir, un borde transitivo, pero no directo, en el grafo de dependencias declarado. Las dependencias indirectas no aparecen en el archivo BUILD
. Dado que la regla no depende directamente del proveedor, no hay forma de hacer un seguimiento de los cambios, como se muestra en el siguiente ejemplo de cronograma:
1. Las dependencias declaradas coinciden con las dependencias reales
Al principio, todo funciona. El código del paquete a
usa código del paquete b
.
El código del paquete b
usa el código del paquete c
y, por lo tanto, a
depende de c
de forma transitiva.
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(); } |
|
|
Las dependencias declaradas son una sobreaproximación de las dependencias reales. Todo está bien.
2. Cómo agregar una dependencia no declarada
Se introduce un riesgo latente cuando alguien agrega código a a
que crea una dependencia real directa en c
, pero olvida declararla en el archivo de compilación a/BUILD
.
a / a.in |
|
---|---|
import b; import c; b.foo(); c.garply(); |
|
|
|
Las dependencias declaradas ya no son una sobreaproximación de las dependencias reales.
Esto puede compilarse correctamente, ya que los cierres transitivos de los dos gráficos son iguales, pero oculta un problema: a
tiene una dependencia real, pero no declarada, en c
.
3. Divergencia entre los gráficos de dependencia declarados y reales
El riesgo se revela cuando alguien refactoriza b
para que ya no dependa de c
, lo que, sin querer, rompe a
sin que sea culpa suya.
b/BUILD |
|
---|---|
rule( name = "b", srcs = "b.in", deps = "//d:d", ) |
|
b / b.in |
|
import d; function foo() { d.baz(); } |
|
|
|
El gráfico de dependencias declarado ahora es una subestimación de las dependencias reales, incluso cuando se cierra de forma transitiva, por lo que es probable que la compilación falle.
El problema se podría haber evitado si se hubiera asegurado de que la dependencia real de a
a c
introducida en el paso 2 se hubiera declarado correctamente en el archivo BUILD
.
Tipos de dependencias
La mayoría de las reglas de compilación tienen tres atributos para especificar diferentes tipos de dependencias genéricas: srcs
, deps
y data
. Estos se explican a continuación. Para obtener más detalles, consulta Atributos comunes a todas las reglas.
Muchas reglas también tienen atributos adicionales para tipos de dependencias específicos de la regla, por ejemplo, compiler
o resources
. Estos se detallan en la Enciclopedia de compilación.
Dependencias de srcs
Son los archivos que consume directamente la regla o las reglas que generan archivos fuente.
Dependencias de deps
Es una regla que apunta a módulos compilados por separado que proporcionan archivos de encabezado, símbolos, bibliotecas, datos, etcétera.
Dependencias de data
Es posible que un destino de compilación necesite algunos archivos de datos para ejecutarse correctamente. Estos archivos de datos no son código fuente: no afectan la forma en que se compila el destino. Por ejemplo, una prueba de unidades puede comparar el resultado de una función con el contenido de un archivo. Cuando compilas la prueba de unidad, no necesitas el archivo, pero sí lo necesitas cuando ejecutas la prueba. Lo mismo se aplica a las herramientas que se inician durante la ejecución.
El sistema de compilación ejecuta pruebas en un directorio aislado en el que solo están disponibles los archivos que se indican como data
. Por lo tanto, si un archivo binario, una biblioteca o una prueba necesitan algunos archivos para ejecutarse, especifícalos (o una regla de compilación que los contenga) en data
. Por ejemplo:
# 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",
...
],
)
Estos archivos están disponibles con la ruta de acceso relativa path/to/data/file
. En las pruebas, puedes hacer referencia a estos archivos uniendo las rutas de acceso del directorio de origen de la prueba y la ruta de acceso relativa al espacio de trabajo, por ejemplo, ${TEST_SRCDIR}/workspace/path/to/data/file
.
Cómo usar etiquetas para hacer referencia a directorios
Mientras revisas nuestros archivos BUILD
, es posible que notes que algunas etiquetas data
hacen referencia a directorios. Estas etiquetas terminan con /.
o /
, como en los siguientes ejemplos, que no debes usar:
No se recomienda: data = ["//data/regression:unittest/."]
No se recomienda: data = ["testdata/."]
No se recomienda: data = ["testdata/"]
Esto parece conveniente, en especial para las pruebas, ya que permite que una prueba use todos los archivos de datos del directorio.
Pero intenta no hacerlo. Para garantizar que las compilaciones incrementales sean correctas (y que las pruebas se vuelvan a ejecutar) después de un cambio, el sistema de compilación debe conocer el conjunto completo de archivos que son entradas para la compilación (o la prueba). Cuando especificas un directorio, el sistema de compilación solo realiza una recompilación cuando cambia el directorio en sí (debido a la adición o eliminación de archivos), pero no podrá detectar ediciones en archivos individuales, ya que esos cambios no afectan el directorio que los contiene.
En lugar de especificar directorios como entradas para el sistema de compilación, debes enumerar el conjunto de archivos que contienen, ya sea de forma explícita o con la función glob()
. (Usa **
para forzar que glob()
sea recursivo).
Recomendado:data = glob(["testdata/**"])
Lamentablemente, hay algunas situaciones en las que se deben usar etiquetas de directorio.
Por ejemplo, si el directorio testdata
contiene archivos cuyos nombres no cumplen con la sintaxis de etiquetas, la enumeración explícita de archivos o el uso de la función glob()
produce un error de etiquetas no válidas. En este caso, debes usar etiquetas de directorio, pero ten cuidado con el riesgo asociado de recompilaciones incorrectas que se describió anteriormente.
Si debes usar etiquetas de directorio, ten en cuenta que no puedes hacer referencia al paquete principal con una ruta de acceso relativa ../
. En su lugar, usa una ruta de acceso absoluta como //data/regression:unittest/.
.
Cualquier regla externa, como una prueba, que necesite usar varios archivos debe declarar explícitamente su dependencia de todos ellos. Puedes usar filegroup()
para agrupar archivos en el archivo BUILD
:
filegroup(
name = 'my_data',
srcs = glob(['my_unittest_data/*'])
)
Luego, puedes hacer referencia a la etiqueta my_data
como la dependencia de datos en tu prueba.
Archivos BUILD | Visibilidad |