Este documento es una descripción de la base de código y de la estructura de Bazel. Integra está dirigido a personas dispuestas a contribuir con Bazel, no a usuarios finales.
Introducción
La base de código de Bazel es grande (código de producción de ~350 KLOC y prueba de ~260 KLOC). código) y nadie está familiarizado con el panorama completo: todos conocen su en un valle muy bien, pero pocos saben qué hay en las colinas de cada dirección IP.
Para que las personas en la mitad de su recorrido no se encuentren en un en un bosque oscuro con el camino sencillo perdido, este documento intenta dar una descripción general de la base de código para que sea más fácil comenzar a usar trabajando en él.
La versión pública del código fuente de Bazel se encuentra en GitHub en github.com/bazelbuild/bazel. No es la “fuente de verdad”; deriva de un árbol de fuentes internas de Google que contiene funciones adicionales que no son útiles fuera de Google. El a largo plazo es hacer de GitHub la fuente de información.
Las contribuciones se aceptan a través del mecanismo normal de solicitud de extracción de GitHub, e importadas manualmente por un empleado de Google al árbol de fuentes internas, y se volverá a exportar a GitHub.
Arquitectura cliente/servidor
La mayor parte de Bazel reside en un proceso del servidor que se mantiene en la RAM entre las compilaciones. Esto permite que Bazel mantenga el estado entre compilaciones.
Por eso la línea de comandos de Bazel tiene dos tipos de opciones: inicio y . En una línea de comandos como esta:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Algunas opciones (--host_jvm_args=
) aparecen antes del nombre del comando que se ejecutará.
y otras son posteriores a (-c opt
); El tipo anterior se denomina “opción de inicio”. y
afecta el proceso del servidor en su conjunto, mientras que este último tipo, el
opción", solo afecta a un único comando.
Cada instancia de servidor tiene un único espacio de trabajo asociado (colección de archivos árboles conocidos como “repositorios”) y cada espacio de trabajo suele tener un único espacio de Compute Engine. Esto se puede eludir si se especifica una base de salida personalizada Consulta la sección "Diseño del directorio" para obtener más información.
Bazel se distribuye como un único ejecutable ELF que también es un archivo ZIP válido.
Cuando escribes bazel
, el ejecutable de ELF anterior implementado en C++ (el archivo
"cliente") obtiene el control. Configura un proceso de servidor adecuado
los siguientes pasos:
- Comprueba si ya se extrajo. De lo contrario, lo hace. Esta es de donde proviene la implementación del servidor.
- Verifica si hay una instancia de servidor activa que funcione: está en ejecución,
Tiene las opciones de inicio correctas y usa el directorio del espacio de trabajo adecuado. Integra
Encuentra el servidor en ejecución observando el directorio
$OUTPUT_BASE/server
. donde hay un archivo de bloqueo con el puerto en el que escucha el servidor. - Si es necesario, finaliza el proceso del servidor anterior.
- Si es necesario, inicia un nuevo proceso de servidor.
Una vez que esté listo un proceso del servidor adecuado, se deberá ejecutar el comando
se le comunica a través de una interfaz gRPC, la salida de Bazel se canaliza
a la terminal. Solo se puede ejecutar un comando a la vez. Este es
implementado mediante un mecanismo de bloqueo elaborado con partes en C++ y partes en
Java Hay infraestructura para ejecutar
varios comandos en paralelo
ya que la incapacidad de ejecutar bazel version
en paralelo con otro comando
es un poco vergonzoso. El principal bloqueador es el ciclo de vida de las BlazeModule
y algunos estados en BlazeRuntime
.
Al final de un comando, el servidor de Bazel transmite el código de salida al cliente
debería devolverse. Una arruga interesante es la implementación de bazel run
: el
trabajo de este comando es ejecutar algo que Bazel acaba de compilar, pero no puede hacerlo
del proceso del servidor
porque no tiene una terminal. En cambio, le indica
al cliente qué objeto binario debe ujexec()
y con qué argumentos.
Cuando uno presiona Ctrl + C, el cliente lo traduce a una llamada Cancel en gRPC , que intenta finalizar el comando lo antes posible. Después del tercera Ctrl-C, el cliente envía una SIGKILL al servidor en su lugar.
El código fuente del cliente se encuentra en src/main/cpp
y el protocolo que se usa para
comunicarse con el servidor está en src/main/protobuf/command_server.proto
.
El punto de entrada principal del servidor es BlazeRuntime.main()
y las llamadas a gRPC
del cliente son manejados por GrpcServerImpl.run()
.
Diseño de directorio
Bazel crea un conjunto de directorios un tanto complicado durante una compilación. Un está disponible en el diseño del directorio de salida.
El “repositorio principal” es el árbol de fuentes en el que se ejecuta Bazel. Por lo general, corresponde a algo que verificaste en el control de origen. La raíz de este directorio es conocida como “raíz del espacio de trabajo”.
Bazel coloca todos sus datos en la “raíz del usuario de salida”. Por lo general,
$HOME/.cache/bazel/_bazel_${USER}
, pero se puede anular con el
Opción de inicio de --output_user_root
.
La "base de instalaciones" es de donde se extrae Bazel. Esto se hace automáticamente
y cada versión de Bazel obtiene un subdirectorio basado en su suma de comprobación en el
una base de instalaciones amplia. Se encuentra en $OUTPUT_USER_ROOT/install
de forma predeterminada y se puede cambiar
con la opción de línea de comandos --install_base
.
La “base de salida” es el lugar donde la instancia de Bazel se conectó a un servicio
el espacio de trabajo. Cada base de salida tiene, como máximo, una instancia de servidor de Bazel.
en ejecución en cualquier momento. Por lo general, es a la(s) $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
. Se puede cambiar con la opción de inicio --output_base
.
que es, entre otras cosas, útil para evadir la limitación que solo
una instancia de Bazel puede ejecutarse en cualquier lugar de trabajo en cualquier momento.
El directorio de salida contiene, entre otras cosas, lo siguiente:
- Los repositorios externos recuperados en
$OUTPUT_BASE/external
. - La raíz de ejecución, un directorio que contiene symlinks a todos los archivos
de la compilación actual. Se encuentra en
$OUTPUT_BASE/execroot
. Durante la compilación, el directorio de trabajo es$EXECROOT/<name of main repository>
. Tenemos planificado cambiar esto a$EXECROOT
, aunque es una a largo plazo porque es un cambio muy incompatible. - Archivos compilados durante la compilación.
El proceso de ejecutar un comando
Una vez que el servidor de Bazel obtiene el control y se le informa sobre un comando que necesita para ejecutar, se produce la siguiente secuencia de eventos:
Se informa a
BlazeCommandDispatcher
sobre la nueva solicitud. Decide si el comando necesita un espacio de trabajo en el que ejecutarse (casi todos los comandos excepto para las que no están relacionadas con el código fuente, como la versión help) y si se está ejecutando otro comando.Se encontró el comando correcto. Cada comando debe implementar la interfaz
BlazeCommand
y debe tener la anotación@Command
(esta es una especie de antipatrón, sería bueno si todos los metadatos que necesita un comando descritos por los métodos enBlazeCommand
)Se analizan las opciones de la línea de comandos. Cada comando tiene una línea de comandos diferente de Kubernetes, que se describen en la anotación
@Command
.Se crea un bus de eventos. El bus de eventos es una transmisión de eventos que ocurren durante la compilación. Algunos de estos se exportan fuera de Bazel en la sección de eventos de compilación para indicarle al mundo cómo se compila en la nube.
El comando obtiene el control. Los comandos más interesantes son aquellos que ejecutan un compilación: compilación, prueba, ejecución, cobertura, etc. Esta funcionalidad es implementado por
BuildTool
.Se analiza el conjunto de patrones de destino en la línea de comandos y los comodines, como Se resolvieron
//pkg:all
y//pkg/...
. Esto se implementa enAnalysisPhaseRunner.evaluateTargetPatterns()
y se recreó en Skyframe comoTargetPatternPhaseValue
La fase de carga y análisis se ejecuta para producir el grafo de acciones (un grafo acíclico de comandos que deben ejecutarse para la compilación).
Se ejecuta la fase de ejecución. Esto significa ejecutar cada acción necesaria para y compilar los objetivos de nivel superior que se solicitan.
Opciones de línea de comandos
Las opciones de línea de comandos para una invocación de Bazel se describen en un
OptionsParsingResult
, que, a su vez, contiene un mapa de "option
clases" a los valores de las opciones. Una "clase de opción" es una subclase de
OptionsBase
y agrupa opciones de línea de comandos relacionadas con cada una
entre sí. Por ejemplo:
- Opciones relacionadas con un lenguaje de programación (
CppOptions
oJavaOptions
). Estos deberían ser una subclase deFragmentOptions
y, finalmente, se unen. en un objetoBuildOptions
. - Opciones relacionadas con la forma en que Bazel ejecuta acciones (
ExecutionOptions
)
Estas opciones están diseñadas para ser utilizadas en la fase de análisis y (ya sea
a través de RuleContext.getFragment()
en Java o ctx.fragments
en Starlark).
Algunas de ellas (por ejemplo, si se debe realizar un análisis de C++ o no) se leen
en la fase de ejecución, pero eso siempre requiere un mantenimiento explícito, ya que
BuildConfiguration
no está disponible en ese momento. Para obtener más información, consulta la
en la sección “Configuraciones”.
ADVERTENCIA: Nos gusta simular que las instancias de OptionsBase
son inmutables y
usarlas de esa manera (como una parte de SkyKeys
). Este no es el caso y
Modificarlas es una forma muy buena
de romper Bazel de maneras sutiles
para depurar. Lamentablemente, hacerlos realmente inmutables es un gran esfuerzo.
(Modificar un objeto FragmentOptions
inmediatamente después de la construcción antes que cualquier otra persona
puedes mantener una referencia a ella y antes de que se realice equals()
o hashCode()
está bien que lo llames).
Bazel aprende sobre las clases de opciones de las siguientes maneras:
- Algunos están conectados por cable a Bazel (
CommonCommandOptions
). - Desde la anotación
@Command
en cada comando de Bazel - Desde
ConfiguredRuleClassProvider
(estas son opciones de línea de comandos relacionadas a lenguajes de programación individuales) - Las reglas de Starlark también pueden definir sus propias opciones (consulte aquí).
Cada opción (excepto las opciones definidas por Starlark) es una variable miembro de un
Subclase FragmentOptions
que tiene la anotación @Option
, que especifica
el nombre y el tipo de la opción de línea de comandos, junto con un texto de ayuda.
El tipo de Java del valor de una opción de línea de comandos suele ser algo simple
(una cadena, un número entero, un valor booleano, una etiqueta, etcétera). Sin embargo, también admitimos
opciones de tipos más complicados; en este caso, el trabajo de convertir
de línea de comandos al tipo de datos recae en una implementación de
com.google.devtools.common.options.Converter
El árbol fuente, como lo ve Bazel
Bazel se dedica a compilar software, que ocurre a través de la lectura y interpretar el código fuente. Todo el código fuente en el que opera Bazel se llama “espacio de trabajo” y se estructura en repositorios, paquetes y las reglas de firewall.
Repositorios
Un "repositorio" es un árbol de fuentes en el que trabaja un desarrollador; generalmente representa un solo proyecto. El antepasado de Bazel, Blaze, operaba en un monorepo, es decir, un solo árbol de fuentes que contenga todo el código fuente que se usó para ejecutar la compilación. Por el contrario, Bazel admite proyectos cuyo código fuente abarca varios de Cloud Storage. El repositorio desde el que se invoca a Bazel se denomina el repositorio “principal” “repositorio” y los demás se denominan “repositorios externos”.
Un repositorio se marca con un archivo de límite de repositorio (MODULE.bazel
, REPO.bazel
o
en contextos heredados, WORKSPACE
o WORKSPACE.bazel
) en su directorio raíz. El
El repositorio principal es el árbol de fuentes desde donde invocas a Bazel. Repositorios externos
se definen de varias maneras: consulta dependencias externas
descripción general para obtener más información.
El código de los repositorios externos se vincula con un symlink o se descarga en
$OUTPUT_BASE/external
Cuando se ejecuta la compilación, se debe unir todo el árbol de fuentes. este
lo realiza SymlinkForest
, que vincula cada paquete con un symlink a cada paquete en el repositorio principal
a $EXECROOT
y a cada repositorio externo a $EXECROOT/external
o
$EXECROOT/..
Paquetes
Cada repositorio está compuesto por paquetes, una colección de archivos relacionados y
una especificación de las dependencias. Estas se especifican con un archivo llamado
BUILD
o BUILD.bazel
. Si existen ambos, Bazel prefiere BUILD.bazel
. el motivo
El motivo por el que todavía se aceptan archivos BUILD
es que Blaze, el principal de Bazel, usó este
el nombre del archivo. Sin embargo, resultó ser un segmento de ruta de uso frecuente, especialmente
en Windows, donde los nombres de archivos no distinguen mayúsculas de minúsculas.
Los paquetes son independientes entre sí: cambia el archivo BUILD
de un paquete.
no puede hacer que otros paquetes cambien. La adición o eliminación de archivos BUILD
_puede _cambiar otros paquetes, ya que los globs recursivos se detienen en los límites de los paquetes.
y, por lo tanto, la presencia de un archivo BUILD
detiene la recursividad.
La evaluación de un archivo BUILD
se llama “carga de paquetes”. Se implementa
en la clase PackageFactory
, llama al intérprete de Starlark y
requiere conocimiento del conjunto de clases de reglas disponibles. El resultado del paquete
la carga es un objeto Package
. Es principalmente un mapa de una cadena (el nombre de una
objetivo) con el mismo objetivo.
Una gran parte de la complejidad
durante la carga de paquetes es global:
requiere que cada archivo de origen se muestre explícitamente y, en su lugar, se pueda ejecutar globs
(como glob(["**/*.java"])
). A diferencia de la shell, admite globs recursivos que
descender a subdirectorios (pero no a subpaquetes). Esto requiere acceso a
y, como puede ser lento, implementamos todo tipo de trucos para
para que se ejecute en paralelo
y de la forma más eficiente posible.
El gesto de globo terráqueo se implementa en las siguientes clases:
LegacyGlobber
, un globo terráqueo rápido y feliz que no reconoce el SkyframeSkyframeHybridGlobber
, una versión que usa Skyframe y vuelve a el globber heredado para evitar los "reinicios de Skyframe" (como se describe a continuación)
La clase Package
en sí contiene algunos miembros que se usan exclusivamente para lo siguiente:
analizar el estado “externo” (relacionadas con dependencias externas) y que no
tiene sentido para paquetes reales. Este es
un defecto de diseño porque los objetos que describen paquetes regulares no deberían contener
campos que describen algo más. Estos incluyen los siguientes:
- Las asignaciones del repositorio
- Las cadenas de herramientas registradas
- Las plataformas de ejecución registradas
Idealmente, habría más separación entre analizar el estado paquete
analizar paquetes normales, de modo que Package
no necesite satisfacer las necesidades de
las necesidades de ambos. Lamentablemente, esto es difícil de hacer
se entrelazan bastante.
Etiquetas, destinos y reglas
Los paquetes se componen de destinos, que tienen los siguientes tipos:
- Files: Los elementos que son la entrada o el resultado de la compilación En Con la jerga de Bazel, los llamamos artefactos (que se analizan en otro lugar). Es posible que no todas los archivos creados durante la compilación son objetivos; es común que la salida sea Bazel no tenga una etiqueta asociada.
- Reglas: En ellas, se describen los pasos para derivar los resultados a partir de las entradas. Ellas
generalmente se asocian con un lenguaje de programación (como
cc_library
,java_library
opy_library
), pero hay algunos que se pueden disfrutar sin importar el idioma. (por ejemplo,genrule
ofilegroup
) - Grupos de paquetes: que se analizan en la sección Visibilidad.
El nombre de un objetivo se denomina etiqueta. La sintaxis de las etiquetas es
@repo//pac/kage:name
, en el que repo
es el nombre del repositorio en el que está la etiqueta
en, pac/kage
es el directorio en el que se encuentra su archivo BUILD
y name
es la ruta de acceso de
el archivo (si la etiqueta hace referencia a un archivo de origen) en relación con el directorio de la
. Cuando se hace referencia a un destino en la línea de comandos, algunas partes de la etiqueta
que se pueden omitir:
- Si se omite el repositorio, se considera que la etiqueta está en el en un repositorio de confianza.
- Si se omite la parte del paquete (como
name
o:name
), se usa la etiqueta. en el paquete del directorio de trabajo actual (rutas de acceso relativas que contenga referencias de nivel superior (..)
Un tipo de regla (como "biblioteca C++") se denomina "clase de regla". Las clases de reglas pueden
implementarse en Starlark (la función rule()
) o en Java (también denominado
"reglas nativas", escribe RuleClass
). A largo plazo, cada lenguaje específico
de reglas de firewall en Starlark, pero algunas familias de reglas heredadas (como Java
o C++) seguirán estando en Java por el momento.
Las clases de reglas de Starlark deben importarse al comienzo de los archivos BUILD
usando la sentencia load()
, mientras que las clases de reglas de Java son conocido por
Bazel, en virtud de su registro en ConfiguredRuleClassProvider
.
Las clases de reglas contienen la siguiente información:
- Sus atributos (como
srcs
ydeps
): sus tipos, valores predeterminados restricciones, etcétera. - Las transiciones de la configuración y los aspectos vinculados a cada atributo, si los hay
- La implementación de la regla
- Los proveedores de información transitiva proporcionan la regla "por lo general", crea
Nota sobre la terminología: En la base de código, a menudo usamos "Rule" para que se refiera el objetivo
creado por una clase de regla. Pero en Starlark y en la documentación para el usuario,
"Regla" debe usarse exclusivamente para hacer referencia a la clase de regla en sí; el objetivo
es solo un "objetivo". Además, ten en cuenta que, a pesar de que RuleClass
tiene "clase" en su
no existe una relación de herencia de Java entre una clase de regla y los destinos
de ese tipo.
Skyframe
El framework de evaluación subyacente a Bazel se llama Skyframe. Su modelo consiste en todo lo que se debe construir durante una compilación se organiza en una grafo acíclico con aristas que apuntan desde cualquier dato hacia sus dependencias, es decir, otros datos que hay que conocer para construirlos.
Los nodos del gráfico se llaman SkyValue
y sus nombres se llaman
SkyKey
Ambos son profundamente inmutables. solo los objetos inmutables deben ser
a la que pueden acceder. Esta invariante casi siempre se mantiene. En caso de que no
(como para las clases de opciones individuales BuildOptions
, que es miembro de
BuildConfigurationValue
y su SkyKey
) nos esforzamos por no cambiar
o cambiarlos de formas que no son observables desde el exterior.
De esto se desprende que todo lo que se procesa dentro de Skyframe (como
destinos configurados) también deben ser inmutables.
La forma más conveniente de observar el gráfico de Skyframe es ejecutar bazel dump
--skyframe=deps
, que vuelca el gráfico, una SkyValue
por línea. Es lo mejor
para construcciones diminutas, ya que pueden ser bastante grandes.
Skyframe se encuentra en el paquete com.google.devtools.build.skyframe
. El
El paquete com.google.devtools.build.lib.skyframe
con nombre similar contiene el
de Bazel sobre Skyframe. Para obtener más información sobre Skyframe,
disponibles aquí.
Para evaluar un elemento SkyKey
en un elemento SkyValue
, Skyframe invocará el
SkyFunction
correspondiente al tipo de clave. Durante el ciclo de vida de la función
evaluación, puede solicitar otras dependencias de Skyframe llamando al
varias sobrecargas de SkyFunction.Environment.getValue()
. Este tiene el
un efecto secundario de registrar esas dependencias
en el gráfico interno de Skyframe,
que Skyframe sabrá que debe reevaluar la función cuando cualquiera de sus dependencias
cambio. En otras palabras, el almacenamiento en caché y el procesamiento incremental de Skyframe funcionan en
el nivel de detalle de SkyFunction
y SkyValue
Cada vez que un objeto SkyFunction
solicita una dependencia que no está disponible, getValue()
.
devolverá un valor nulo. La función luego debería devolver el control a Skyframe
y devolverá un valor nulo. Más adelante, Skyframe evaluará el
una dependencia no disponible, reinicia la función desde el principio
hora en que la llamada a getValue()
se realizará correctamente con un resultado no nulo.
Una consecuencia de esto es que cualquier cálculo realizado dentro del SkyFunction
antes del reinicio. Pero esto no incluye el trabajo realizado para
evaluar la dependencia SkyValues
, que se almacenan en caché Por eso, solemos trabajar
en torno a este problema de la siguiente manera:
- Declaración de dependencias en lotes (mediante
getValuesAndExceptions()
) en limita la cantidad de reinicios. - Divide un
SkyValue
en partes separadas que se calculan con diferentesSkyFunction
para que se puedan procesar y almacenar en caché de forma independiente. Esta debe hacerse estratégicamente, ya que tiene el potencial de aumentar la memoria de uso de la nube. - Almacenar estados entre reinicios, ya sea usando
SkyFunction.Environment.getState()
o conservar una caché estática ad hoc “detrás de la parte posterior de Skyframe”. Con SkyFunctions complejas, la administración del estado entre reinicios puede ser complicado, así que Se introdujeron losStateMachine
para un estructurado y ordenado para la simultaneidad lógica reanudar los procesamientos jerárquicos dentro de unSkyFunction
. Ejemplo:DependencyResolver#computeDependencies
usa unStateMachine
congetState()
para calcular el conjunto potencialmente enorme de dependencias directas de un destino configurado, que pueden generar o reinicios costosos.
En esencia, Bazel necesita estos tipos
de soluciones porque hay cientos de
miles de nodos de Skyframe en tránsito es común, y la compatibilidad de Java con
los subprocesos ligeros no superan
de implementación de StateMachine
a partir de 2023.
Starlark
Starlark es el lenguaje específico del dominio que se usa para configurar y extender Bazel Se concibió como un subconjunto restringido de Python que tiene muchos menos tipos más restricciones en el flujo de control y, lo más importante, una inmutabilidad sólida que garantiza la habilitación de lecturas simultáneas. No es de Turing completa, desalienta a algunos usuarios (pero no a todos) a intentar lograr objetivos tareas de programación dentro del lenguaje.
Starlark se implementa en el paquete net.starlark.java
.
También cuenta con una implementación de Go independiente
aquí. Java
que se usa en Bazel es actualmente un intérprete.
Starlark se usa en varios contextos, incluidos los siguientes:
- Archivos
BUILD
. Aquí es donde se definen los objetivos de compilación nuevos. Starlark El código que se ejecuta en este contexto solo tiene acceso al contenido deBUILD
archivo y.bzl
archivos cargados por este. - El archivo
MODULE.bazel
. Es decir, cuando las dependencias externas definido. El código de Starlark que se ejecuta en este contexto tiene un acceso muy limitado con algunas directivas predefinidas. - Archivos
.bzl
. Aquí es donde las nuevas reglas de compilación, reglas de repositorio, extensiones definidas por el usuario. Aquí, código de Starlark puede definir nuevas funciones y cargar de otros archivos.bzl
.
Los dialectos disponibles para los archivos BUILD
y .bzl
son ligeramente diferentes.
porque expresan diferentes cosas. Hay una lista de diferencias disponible
aquí.
Obtén más información sobre Starlark aquí.
La fase de carga y análisis
En la fase de carga y análisis, Bazel determina las acciones necesarias para crear una regla particular. Su unidad básica es un “objetivo configurado”, que es de manera razonable, un par (objetivo, configuración).
Se llama “fase de carga/análisis” porque se puede dividir en dos partes distintas, que solían ser serializadas, pero ahora pueden superponerse con el tiempo:
- Cargar paquetes, es decir, convertir archivos
BUILD
en objetosPackage
que los representan - Analizar los destinos configurados, es decir, ejecutar la implementación del reglas para producir el gráfico de acción
Cada destino configurado en el cierre transitivo de los destinos configurados solicitados en la línea de comandos se deben analizar de abajo hacia arriba; es decir, nodos hoja primero y, luego, hasta los de la línea de comandos. Las entradas al análisis de en un único destino configurado son las siguientes:
- La configuración. ("cómo" crear esa regla; por ejemplo, la regla sino también elementos como las opciones de línea de comandos que el usuario desea pasan al compilador C++)
- Las dependencias directas. Sus proveedores de información transitiva están disponibles a la regla que se está analizando. Se llaman así porque proporcionan una “propiedad de datos integrados” de la información en el cierre transitivo de la configuración como todos los archivos .jar en la ruta de clase o todos los archivos .o que deben vincularse a un objeto binario C++)
- El objetivo en sí. Este es el resultado de cargar el paquete de destino en la nube. Para las reglas, esto incluye sus atributos, que suelen ser de seguridad en la nube.
- La implementación del destino configurado Para las reglas, esto puede estar en Starlark o en Java. Se implementaron todos los destinos configurados que no corresponden a reglas en Java.
El resultado de analizar un destino configurado es el siguiente:
- Los proveedores de información transitiva que configuraron los objetivos que dependen de ella pueden acceso
- Los artefactos que puede crear y las acciones que los producen
La API que se ofrece a las reglas de Java es RuleContext
, que es el equivalente de la
Argumento ctx
de las reglas de Starlark. Su API es más potente, pero al mismo tiempo
con el tiempo, es más fácil hacer Bad ThingsTM, por ejemplo, escribir código cuyo tiempo o
la complejidad del espacio es cuadrática (o peor), para hacer que el servidor de Bazel falle con un
excepción de Java o infringir invariantes (por ejemplo, modificar un objeto
Options
o haciendo que un destino configurado sea mutable)
El algoritmo que determina las dependencias directas de un destino configurado
vive en DependencyResolver.dependentNodeMap()
.
Configuraciones
Las configuraciones son el "cómo" de crear un objetivo: para qué plataforma, con qué opciones de línea de comandos, etcétera.
Se puede compilar el mismo destino para varias configuraciones en la misma compilación. Esta es útil, por ejemplo, cuando se usa el mismo código para una herramienta que se ejecuta durante la compilación y el código de destino, y realizaremos compilaciones cruzadas o cuando Compilar una app para Android pesada (una que contenga código nativo para varias CPU) arquitecturas)
De forma conceptual, la configuración es una instancia BuildOptions
. Sin embargo, en
práctica, BuildOptions
se une a BuildConfiguration
que proporciona
varias piezas adicionales de funcionalidad. Se propaga desde la parte superior
gráfico de dependencia en la parte inferior. Si cambia, la compilación debe ser
volver a analizarse.
Esto genera anomalías, como tener que volver a analizar toda la compilación si, por Por ejemplo, cambia la cantidad de ejecuciones de prueba solicitadas, a pesar de que afecta los objetivos de prueba (tenemos planes de "cortar" la configuración para que no es el caso, pero aún no está listo).
Cuando una implementación de reglas necesita parte de la configuración, debe declarar
en su definición con RuleClass.Builder.requiresConfigurationFragments()
de Google Cloud. Esto se hace para evitar errores (como reglas de Python que usan el fragmento de Java) y
para facilitar el recorte de configuración de modo que, por ejemplo, si las opciones de Python cambian, C++
y los objetivos no
deban volver a analizarse.
La configuración de una regla no es necesariamente la misma que la de su elemento "superior" . El proceso de cambio de configuración en un perímetro de dependencia se denomina “transición de configuración”. Puede suceder en dos lugares:
- En un perímetro de dependencia Estas transiciones se especifican en
Attribute.Builder.cfg()
y son funciones de unRule
(en el que el transición) y unBuildOptions
(la configuración original) a uno o másBuildOptions
(la configuración de salida). - En cualquier perímetro entrante a un destino configurado. Estas se especifican en
RuleClass.Builder.cfg()
Las clases relevantes son TransitionFactory
y ConfigurationTransition
.
Se usan transiciones de configuración, por ejemplo:
- Declarar que una dependencia en particular se usa durante la compilación y por lo que deberían integrarse en la arquitectura de ejecución
- Para declarar que una dependencia en particular debe compilarse para varios arquitecturas (como código nativo en APK multiarquitectura de Android)
Si una transición de configuración da como resultado varias configuraciones, se denomina transición dividida.
Las transiciones de configuración también pueden implementarse en Starlark (documentación aquí).
Proveedores de información transitiva
Los proveedores de información transitiva son una forma (y la _única_vía) para los objetivos configurados. para informar sobre otros destinos configurados que dependen de él. El motivo "transitivo" en su nombre es que, por lo general, se trata de el cierre transitivo de un destino configurado.
Por lo general, existe una correspondencia 1:1 entre los proveedores de información transitiva de Java.
y Starlark (la excepción es DefaultInfo
, que es una combinación de
FileProvider
, FilesToRunProvider
y RunfilesProvider
porque esa API se
más estilo Starlark que una transliteración directa del Java).
Su clave es una de las siguientes cosas:
- Un objeto de clase de Java. Esta opción solo está disponible para los proveedores que no están
accesibles desde Starlark. Estos proveedores son una subclase de
TransitiveInfoProvider
- Una string. Esto es heredado y se desaconseja, ya que puede sufrir
conflictos de nombres. Estos proveedores de información transitiva son subclases directas de
build.lib.packages.Info
- Símbolo de un proveedor. Se puede crear desde Starlark con el
provider()
. y es la forma recomendada para crear nuevos proveedores. El símbolo es representado por una instanciaProvider.Key
en Java.
Los proveedores nuevos implementados en Java deben implementarse usando BuiltinProvider
.
NativeProvider
dejó de estar disponible (aún no tuvimos tiempo de quitarlo).
No se puede acceder a las subclases de TransitiveInfoProvider
desde Starlark.
Destinos configurados
Los destinos configurados se implementan como RuleConfiguredTargetFactory
. Hay un
para cada clase de regla implementada en Java. Destinos configurados de Starlark
se crean a través de StarlarkRuleConfiguredTargetUtil.buildRule()
.
Las fábricas de destino configuradas deben usar RuleConfiguredTargetBuilder
para
construir su valor de retorno. Consta de lo siguiente:
- Su
filesToBuild
, el concepto confuso de "el conjunto de archivos que esta regla representa". Son los archivos que se compilan cuando el destino configurado está en la línea de comandos o en los srcs de una genrule. - Sus archivos de ejecución, regulares y datos.
- Sus grupos de salida. Estos son varios "otros conjuntos de archivos" la regla puede
compilar. Se puede acceder a ellas mediante el atributo output_group de la
en Build y con el proveedor
OutputGroupInfo
en Java.
Archivos de ejecución
Algunos objetos binarios necesitan archivos de datos para ejecutarse. Un ejemplo destacado son las pruebas que requieren archivos de entrada. En Bazel, esto se representa con el concepto de “runfiles”. R “árbol de runfiles” es un árbol de directorios de los archivos de datos para un objeto binario en particular. Se crea en el sistema de archivos como un árbol de symlink con enlaces simbólicos individuales. que apuntan a los archivos en los árboles de fuentes de resultados.
Un conjunto de archivos de ejecución se representa como una instancia de Runfiles
. Es conceptualmente una
asignar de la ruta de un archivo en el árbol de archivos de ejecución a la instancia Artifact
que
lo representa. Es un poco más complicado que usar una sola Map
para dos.
motivos:
- La mayoría de las veces, la ruta de acceso de los archivos de ejecución de un archivo es la misma que su execpath. Los usamos para ahorrar algo de RAM.
- Existen varios tipos heredados de entradas en árboles de archivos de ejecución, que también necesitan que se representará.
Los archivos de ejecución se recopilan con RunfilesProvider
, una instancia de esta clase.
representa los archivos de ejecución de un destino configurado (como una biblioteca) y su
de cierre y se recopilan como un conjunto anidado (de hecho,
implementadas con conjuntos anidados en la cubierta): cada destino une los runfiles
de sus dependencias, agrega algunas propias y, luego, envía la configuración resultante hacia arriba
en el gráfico de dependencias. Una instancia RunfilesProvider
contiene dos Runfiles
una para cuando se depende de la regla a través de los “datos” el atributo y
una para cada otro tipo de dependencia entrante. Esto se debe a que un objetivo
A veces presenta diferentes archivos de ejecución cuando se depende de ellos a través de un atributo de datos.
que de otra forma. Este es un comportamiento heredado no deseado que no hemos resuelto.
eliminando todavía.
Los archivos de ejecución de los objetos binarios se representan como una instancia de RunfilesSupport
. Esta
es diferente de Runfiles
porque RunfilesSupport
tiene la capacidad de
que se está compilando (a diferencia de Runfiles
, que es solo una asignación). Esta
requiere los siguientes componentes adicionales:
- Manifiesto de los archivos de ejecución de entrada Esta es una descripción serializada del de archivos de ejecución. Se usa como proxy para el contenido del árbol de archivos de ejecución. y Bazel supone que el árbol de archivos de ejecución cambia solo si el contenido del cambio de manifiesto.
- El manifiesto de runfiles de salida. Las bibliotecas de entorno de ejecución que manejan árboles de archivos de ejecución, especialmente en Windows, que a veces no admite enlaces simbólicos.
- El intermediario de runfiles. Para que exista un árbol de runfiles, se necesita para compilar el árbol y el artefacto al que apuntan. En orden para disminuir la cantidad de aristas de dependencia, se puede usar que se usa para representarlos a todos.
- Argumentos de la línea de comandos para ejecutar el objeto binario cuyos archivos de ejecución se
El objeto
RunfilesSupport
representa.
Aspectos
Los aspectos son una forma de "propagar el procesamiento por el gráfico de dependencia". Son
descritos para los usuarios de Bazel
aquí. Un buen
Un ejemplo motivador son los búferes de protocolo: una regla proto_library
no debe conocer
sobre un lenguaje en particular, sino crear
la implementación de un protocolo
mensaje búfer (la "unidad básica" de búferes de protocolo) en cualquier programación
lenguaje debe acoplarse a la regla proto_library
de modo que, si dos objetivos en
el mismo lenguaje dependen del mismo búfer de protocolo, se compila una sola vez.
Al igual que los objetivos configurados, se representan en Skyframe como un SkyValue
.
y la forma en que se construyen es muy similar a cómo se configuran
tienen una clase de fábrica llamada ConfiguredAspectFactory
que tiene
acceso a un RuleContext
, pero, a diferencia de las fábricas de destino configuradas, también sabe
sobre el destino configurado al que se conecta y sus proveedores.
El conjunto de aspectos propagados por el gráfico de dependencia se especifica para cada
con la función Attribute.Builder.aspects()
. Existen varias
clases con nombres confusos que participan en el proceso:
AspectClass
es la implementación del aspecto. Puede estar en Java (en cuyo caso es una subclase) o en Starlark (en cuyo caso es una instancia deStarlarkAspectClass
). Es análogo aRuleConfiguredTargetFactory
AspectDefinition
es la definición del aspecto. que incluye las los proveedores que requiere, los proveedores que proporciona y contiene una referencia a su implementación, como la instancia deAspectClass
adecuada. Es análogo aRuleClass
.AspectParameters
es una forma de parametrizar un aspecto que se propaga hacia abajo. el gráfico de dependencia. Actualmente es una cadena a la asignación de cadenas. Un buen ejemplo de la razón por la que es útil son los búferes de protocolo: si un lenguaje tiene varias APIs, el información sobre la API para la que deben compilarse los búferes de protocolo se propagarán hacia abajo en el gráfico de dependencias.Aspect
representa todos los datos que se necesitan para procesar un aspecto que se propaga hacia abajo en el gráfico de dependencias. Consiste en la clase de aspecto, su definición y sus parámetros.RuleAspect
es la función que determina qué aspectos de una regla en particular. debe propagarse. Es unRule
-> FunciónAspect
.
Una complicación un tanto inesperada es que los aspectos se pueden adjuntar a otros aspectos.
Por ejemplo, un aspecto que recopile la ruta de clase para un IDE de Java probablemente
quieres saber sobre todos los archivos .jar en la ruta de clase, pero algunos
búferes de protocolo. En ese caso, se recomienda conectar el aspecto IDE al
(regla proto_library
+ aspecto de proto de Java).
La complejidad de los aspectos sobre los aspectos se captura en la clase.
AspectCollection
Plataformas y cadenas de herramientas
Bazel admite compilaciones multiplataforma, es decir, compilaciones en las que existan múltiples arquitecturas en las que se ejecutan acciones de compilación y varias arquitecturas para qué código se compila. Estas arquitecturas se denominan plataformas en Bazel jerga (documentación completa) aquí)
Una plataforma se describe mediante una asignación de pares clave-valor de la configuración de restricciones (como
el concepto de "arquitectura de CPU") hasta los valores de restricción (como una CPU en particular
como x86_64). Tenemos un "diccionario" de las restricciones de uso más frecuente
y valores en el repositorio @platforms
.
El concepto de cadena de herramientas proviene del hecho de que, según las plataformas se ejecuta la compilación y las plataformas a las que se orienta, es posible que se deba usar compiladores diferentes; Por ejemplo, una cadena de herramientas C++ particular puede ejecutarse en un de un SO específico y poder orientar a otros SO. Bazel debe determinar el código C++. compilador que se usa según la ejecución establecida y la plataforma de destino (documentación para cadenas de herramientas aquí).
Para ello, las cadenas de herramientas se anotan con el conjunto de ejecuciones y para las restricciones de plataformas de destino que admiten. Para ello, la definición de Una cadena de herramientas se divide en dos partes:
- Una regla
toolchain()
que describe el conjunto de ejecución y destino restricciones que admite una cadena de herramientas y le indica qué tipo (por ejemplo, C++ o Java) de la cadena de herramientas en que se encuentra (esta última está representada por la reglatoolchain_type()
) - Una regla específica del lenguaje que describe la cadena de herramientas real (como
cc_toolchain()
)
Esto se hace así porque necesitamos conocer las restricciones de cada
de la cadena de herramientas para resolver la cadena de herramientas y determinar
Las reglas *_toolchain()
contienen mucha más información que esa, por lo que requieren más
el tiempo de carga.
Las plataformas de ejecución se especifican de una de las siguientes maneras:
- En el archivo MODULE.bazel con la función
register_execution_platforms()
- En la línea de comandos con la línea de comandos --extra_execution_platforms opción
El conjunto de plataformas de ejecución disponibles se calcula en
RegisteredExecutionPlatformsFunction
La plataforma de destino para un destino configurado está determinada por
PlatformOptions.computeTargetPlatform()
Es una lista de plataformas
finalmente quiere admitir varias plataformas de destino, pero no se ha implementado
aún.
El conjunto de cadenas de herramientas que se usarán para un destino configurado está determinado por
ToolchainResolutionFunction
Es una función de:
- El conjunto de cadenas de herramientas registradas (en el archivo MODULE.bazel y el configuración)
- La ejecución deseada y las plataformas de destino (en la configuración)
- El conjunto de tipos de cadenas de herramientas que requiere el destino configurado (en
UnloadedToolchainContextKey)
- El conjunto de restricciones de la plataforma de ejecución del destino configurado (el
exec_compatible_with
) y la configuración (--experimental_add_exec_constraints_to_targets
), enUnloadedToolchainContextKey
Su resultado es un UnloadedToolchainContext
, que es básicamente un mapa de
tipo de cadena de herramientas (representado como una instancia de ToolchainTypeInfo
) a la etiqueta de
la cadena de herramientas seleccionada. Se llama "descargada". porque no contiene
cadenas de herramientas en sí, solo sus etiquetas.
Luego, las cadenas de herramientas se cargan con ResolvedToolchainContext.load()
.
y se usan en la implementación del destino configurado que los solicitó.
También tenemos un sistema heredado que se basa en que haya un solo “host”
configuración y los parámetros de configuración de destino representados por varios
de configuración, como --cpu
. Estamos realizando la transición gradualmente a lo anterior
en un sistema de archivos. Para manejar casos en los que los usuarios dependen de la configuración heredada
y, además, implementamos
asignaciones de plataforma
para traducir entre las marcas heredadas y las restricciones de la plataforma de estilo nuevo.
Su código está en PlatformMappingFunction
y usa un "poco" que no sea Starlark.
de Wikipedia".
Limitaciones
A veces, uno quiere designar un objetivo como compatible solo con unos pocos y plataformas de Google Cloud. Desafortunadamente, Bazel cuenta con varios mecanismos para lograr este objetivo:
- Restricciones específicas de reglas
environment_group()
/environment()
- Restricciones de la plataforma
Por lo general, las restricciones específicas para reglas se usan en Google para reglas de Java. que se
cuando salen y no están disponibles en Bazel, pero el código fuente puede
que contengan referencias. El atributo que administra esto se denomina
constraints=
Environment_group() y Environment()
Estas reglas son un mecanismo heredado y no se usan ampliamente.
Todas las reglas de compilación pueden declarar qué "entornos" para los que se pueden crear,
"entorno" es una instancia de la regla environment()
.
Existen varias formas de especificar entornos compatibles para una regla:
- A través del atributo
restricted_to=
Esta es la forma más directa de especificación; declara el conjunto exacto de entornos que admite la regla para este grupo. - A través del atributo
compatible_with=
De esta forma, se declaran los entornos como una regla admite además del estándar en entornos compatibles de forma predeterminada. - A través de los atributos a nivel de paquete
default_restricted_to=
ydefault_compatible_with=
- A través de especificaciones predeterminadas en reglas
environment_group()
Cada entorno pertenece a un grupo de intercambios de tráfico relacionados por temas (como “CPU arquitecturas", "versiones de JDK" o “sistemas operativos de dispositivos móviles”). El del grupo de entornos incluye cuáles de estos entornos debe ser compatible con la configuración “default” si no se especifica lo contrario en elrestricted_to=
/environment()
atributos. Una regla sin tal hereda todos los valores predeterminados. - Mediante un valor predeterminado de clase de regla Esto anula los valores predeterminados globales
instancias de la clase de regla determinada. Esto se puede usar, por ejemplo, para hacer
todas las reglas
*_test
se pueden probar sin que cada instancia tenga que realizar explícitamente declarar esta capacidad.
environment()
se implementa como una regla normal, mientras que environment_group()
es una subclase de Target
, pero no de Rule
(EnvironmentGroup
) y de
que está disponible de forma predeterminada en Starlark.
(StarlarkLibrary.environmentGroup()
), lo que finalmente crea una versión epónima
objetivo. Esto es para evitar una dependencia cíclica que podría surgir porque cada
entorno debe declarar el grupo de entornos al que pertenece y que cada uno
el grupo de entornos debe declarar sus entornos predeterminados.
Se puede restringir una compilación a un entorno determinado con el
Opción de línea de comandos --target_environment
.
La implementación de la verificación de restricciones se encuentra en
RuleContextConstraintSemantics
y TopLevelConstraintSemantics
.
Restricciones de la plataforma
El "oficial" actual manera de describir qué plataformas es compatible con un destino es usando las mismas restricciones que se usan para describir las cadenas de herramientas y las plataformas. Se encuentra en proceso de revisión en una solicitud de extracción #10945:
Visibilidad
Si trabajas en una base de código grande con muchos desarrolladores (como en Google), puedes queremos evitar que los demás dependan de forma arbitraria de su código. De lo contrario, según la ley de Hyrum, Las personas usarán los comportamientos que considera implementar más detalles.
Bazel admite esto con el mecanismo llamado visibilidad: puedes declarar que un un destino específico solo se puede depender del uso visibilidad. Esta es un poco especial porque, aunque contiene una lista de etiquetas, estas las etiquetas pueden codificar un patrón sobre nombres de paquetes en lugar de un puntero a cualquier en un objetivo en particular. (Sí, es un defecto de diseño).
Esto se implementa en los siguientes lugares:
- La interfaz
RuleVisibility
representa una declaración de visibilidad. Puede ser una constante (completamente pública o completamente privada) o una lista de etiquetas. - Las etiquetas pueden referirse a grupos de paquetes (lista predefinida de paquetes), a
paquetes directos (
//pkg:__pkg__
) o subárboles de paquetes (//pkg:__subpackages__
). Esto es diferente de la sintaxis de la línea de comandos, que usa//pkg:*
o//pkg/...
. - Los grupos de paquetes se implementan como su propio destino (
PackageGroup
). objetivo configurado (PackageGroupConfiguredTarget
). Probablemente, podríamos reemplázalas por reglas simples si quisiéramos. Su lógica se implementa con la ayuda dePackageSpecification
, que corresponde a un patrón único como//pkg/...
;PackageGroupContents
, que corresponde a un solo atributopackages
depackage_group
yPackageSpecificationProvider
, que se agrega a través de unapackage_group
y suincludes
transitivo. - La conversión de listas de etiquetas de visibilidad a dependencias se realiza en
DependencyResolver.visitTargetVisibility
y algunos otros lugares. - La verificación real se realiza
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
Conjuntos anidados
Un destino configurado suele agregar un conjunto de archivos de sus dependencias agrega lo suyo y une el conjunto agregado en un proveedor de información transitiva que los destinos configurados que dependen de él pueden hacer lo mismo. Ejemplos:
- Los archivos de encabezado C++ usados para una compilación
- Los archivos de objeto que representan el cierre transitivo de un
cc_library
- El conjunto de archivos .jar que debe estar en la ruta de clase para que una regla de Java compilar o ejecutar
- El conjunto de archivos de Python en el cierre transitivo de una regla de Python
Si hiciéramos esto de manera simple usando, por ejemplo, List
o Set
, obtendremos
uso de memoria cuadrática: si hay una cadena de N reglas y cada regla agrega un
tendríamos más de 1 + 2...+ N miembros de la colección.
Para evitar este problema, se nos ocurrió el concepto de una
NestedSet
Es una estructura de datos que se compone de otros NestedSet
instancias y algunos miembros propios, lo que forma un grafo acíclico dirigido
de conjuntos. Son inmutables y sus miembros se pueden iterar. Definimos
orden de iteración múltiple (NestedSet.Order
): pedido por adelantado, pedido posterior, topológico
(un nodo siempre viene después de sus principales) y “no me importa, pero debería ser
mismo cada vez".
La misma estructura de datos se llama depset
en Starlark.
Artefactos y acciones
La compilación real consiste en un conjunto de comandos que deben ejecutarse para producir
el resultado que quiere el usuario. Los comandos se representan como instancias del
clase Action
, y los archivos se representan como instancias de la clase
Artifact
Se organizan en un grafo acíclico dirigido y bipartito denominado
“gráfico de acciones”.
Hay dos tipos de artefactos: los de origen (aquellos que están disponibles antes de que Bazel comience a ejecutarse) y artefactos derivados (que deben construyen). Los artefactos derivados pueden ser de varios tipos:
- **Artefactos normales. **Estas se verifican para garantizar que estén actualizadas su suma de comprobación, con mtime como atajo; no verificamos la suma del archivo si su ctime no ha cambiado.
- Artefactos de symlink sin resolver. Se comprueba que estén actualizados llamando a readlink(). A diferencia de los artefactos comunes, estos pueden ser colgantes symlinks. Por lo general, se usa en los casos en que se agrupan algunos archivos en un de algún tipo de archivo.
- Artefactos de árboles. No son archivos individuales, sino árboles de directorios. Ellas
para comprobar su actualización mediante la verificación del conjunto de archivos
contenidos. Se representan como
TreeArtifact
. - Artefactos de metadatos constantes. Los cambios en estos artefactos no activan un reconstruir. Se usa exclusivamente para información sobre sellos de compilación: no queremos reconstruir solo porque cambió la hora actual.
No hay una razón fundamental por la que los artefactos de origen no puedan ser artefactos de árbol o
artefactos de symlink sin resolver, es solo que aún no los implementamos (nosotros
Sin embargo, deberías hacer referencia a un directorio de origen en un archivo BUILD
es uno de los
pocos problemas de inexactitud conocidos y de larga data con Bazel tenemos un
implementación.
propiedad de JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Un tipo notable de Artifact
son los intermediarios. Están indicados por Artifact
que son las salidas de MiddlemanAction
. Están acostumbrados a
haz algunos casos especiales:
- La agregación de intermediarios se utiliza para agrupar artefactos. Esto es para que si muchas acciones usan el mismo conjunto grande de entradas, no tenemos N*M. bordes de dependencia, solo N+M (se reemplazan por conjuntos anidados)
- La programación de intermediarios de dependencia garantiza que una acción se ejecute antes que otra.
Se usan principalmente para el análisis con lint, pero también para la compilación de C++ (consulta la sección
CcCompilationContext.createMiddleman()
para obtener una explicación) - Se usan intermediarios de archivos de ejecución para garantizar la presencia de un árbol de archivos de ejecución que no necesite depender por separado del manifiesto de salida y cada artefacto único al que hace referencia el árbol de archivos de ejecución.
Las acciones se entienden mejor como un comando que se debe ejecutar, el entorno que necesita y el conjunto de resultados que produce. Los siguientes son los aspectos principales componentes de la descripción de una acción:
- La línea de comandos que se debe ejecutar
- Los artefactos de entrada que necesita
- Las variables de entorno que se deben configurar
- Anotaciones que describen el entorno (como la plataforma) en el que debe ejecutarse
También hay algunos otros casos especiales, como escribir un archivo cuyo contenido se
que Bazel conoce. Son una subclase de AbstractAction
. La mayoría de las acciones
un objeto SpawnAction
o StarlarkAction
(igualmente, podría decirse que no deberían ser
clases separadas), aunque Java y C++ tienen sus propios tipos de acciones
(JavaCompileAction
, CppCompileAction
y CppLinkAction
).
Con el tiempo, queremos mover todo a SpawnAction
; JavaCompileAction
es
pero C++ es un caso especial debido al análisis del archivo .d y
incluyen el análisis.
El gráfico de acción está principalmente "incorporado" en el gráfico de Skyframe: conceptualmente, el
la ejecución de una acción se representa como una invocación del
ActionExecutionFunction
La asignación desde un perímetro de dependencia de un gráfico de acción a un
El perímetro de dependencia de Skyframe se describe en
ActionExecutionFunction.getInputDeps()
y Artifact.key()
, y tiene algunas
optimizaciones para mantener baja la cantidad de bordes de Skyframe:
- Los artefactos derivados no tienen sus propios
SkyValue
. En cambio,Artifact.getGeneratingActionKey()
se usa para encontrar la clave del acción que la genera - Los conjuntos anidados tienen su propia clave Skyframe.
Acciones compartidas
Algunas acciones se generan a partir de varios objetivos configurados. Las reglas de Starlark son son más limitadas, ya que solo pueden colocar sus acciones derivadas en un determinado por su configuración y su paquete (pero aun así, reglas del mismo paquete pueden entrar en conflicto), pero las reglas implementadas en Java pueden artefactos derivados en cualquier lugar.
Esto se considera un error, pero eliminarlo es muy difícil. ya que produce ahorros significativos en el tiempo de ejecución cuando, por ejemplo, una el archivo fuente se debe procesar de alguna manera, y se hace referencia a ese archivo múltiples reglas (handwave-handwave). Esto tiene el costo de una parte de la RAM: cada una de una acción compartida debe almacenarse en la memoria por separado.
Si dos acciones generan el mismo archivo de salida, deben ser exactamente iguales:
tengan las mismas entradas y salidas, y ejecuten la misma línea de comandos. Esta
de equivalencia se implementa en Actions.canBeShared()
y es
entre las fases de análisis y ejecución analizando cada Action.
Esto se implementa en SkyframeActionExecutor.findAndStoreArtifactConflicts()
.
y es uno de los pocos lugares en Bazel que requiere una conexión “global” vista de la
compilar.
La fase de ejecución
En este momento, Bazel comienza a ejecutar acciones de compilación, como los comandos que para producir resultados.
Lo primero que hace Bazel después de la fase de análisis es determinar qué
Se deben compilar los artefactos. La lógica para esto está codificada
TopLevelArtifactHelper
; En términos generales, es el filesToBuild
de la
objetivos configurados en la línea de comandos y el contenido de una salida especial
grupo con el propósito explícito de expresar "si este objetivo se encuentra en el comando
línea, compila estos artefactos”.
El siguiente paso es crear la raíz de ejecución. Como Bazel tiene la opción de leer
paquetes de origen de diferentes ubicaciones en el sistema de archivos (--package_path
),
debe proporcionar acciones ejecutadas localmente con un árbol de fuentes completo. Este es
manejada por la clase SymlinkForest
y funciona tomando nota de cada destino
en la fase de análisis y crear un árbol de directorios con enlaces simbólicos
cada paquete con un objetivo usado de su ubicación real. Una alternativa sería
ser pasar las rutas de acceso correctas a los comandos (teniendo en cuenta --package_path
).
Esto es indeseable por los siguientes motivos:
- Cambia las líneas de comandos de acción cuando un paquete se mueve desde una ruta de paquete entrada a otra (algo que solía suceder)
- El resultado es diferentes líneas de comandos si una acción se ejecuta de forma remota que si se ejecuta a nivel local
- Requiere una transformación de la línea de comandos específica para la herramienta en uso (considera la diferencia entre rutas de clase de Java y rutas de inclusión de C++)
- Cambiar la línea de comandos de una acción invalida su entrada de caché de acciones
--package_path
se da de baja de manera gradual y constante
Luego, Bazel comienza a recorrer el grafo de acción (el grafo dirigido bipartito y dirigido).
compuesta por acciones y sus artefactos de entrada y salida) y acciones en ejecución.
La ejecución de cada acción se representa con una instancia de SkyValue
.
la clase ActionExecutionValue
.
Como ejecutar una acción es costoso, tenemos algunas capas de almacenamiento en caché golpearse detrás de Skyframe:
ActionExecutionFunction.stateMap
contiene datos para reiniciar Skyframe deActionExecutionFunction
económicos- La caché de acciones locales contiene datos sobre el estado del sistema de archivos
- Los sistemas de ejecución remota suelen contener su propia caché
La caché de acciones locales
Esta caché es otra capa que se encuentra detrás de Skyframe; incluso si una acción es volver a ejecutarse en Skyframe, aún puede ser un hit en la caché de acciones locales. Integra representa el estado del sistema de archivos local y se serializa en el disco, significa que, cuando se inicia un nuevo servidor de Bazel, se puede obtener la caché de acciones locales hits aunque el gráfico de Skyframe esté vacío.
Se verifica esta caché para detectar hits con el método
ActionCacheChecker.getTokenIfNeedToExecute()
Al contrario de su nombre, es un mapa de la ruta de acceso de un artefacto derivado al acción que la emitió. La acción se describe de la siguiente manera:
- El conjunto de sus archivos de entrada y salida, y su suma de verificación
- Su "clave de acción", que suele ser la línea de comandos que se ejecutó, pero
en general, representa todo lo que no es capturado por la suma de comprobación del
archivos de entrada (como en el caso de
FileWriteAction
, es la suma de comprobación de los datos) que está escrito)
También existe una "caché de acciones de arriba abajo" muy experimental que aún está debajo que usa hashes transitivos para evitar ir a la caché tantos veces.
Descubrimiento y reducción de entradas
Algunas acciones son más complicadas que solo tener un conjunto de entradas. Cambios en el conjunto de entradas de una acción se presenta de dos formas:
- Una acción puede descubrir nuevas entradas antes de su ejecución o decidir que algunas
de sus entradas no son realmente necesarias. El ejemplo canónico es C++,
en la que es mejor hacer una suposición fundamentada sobre qué encabezados genera un C++
del archivo a partir de su cierre transitivo, de modo que no prestemos atención a enviar cada
archivo a ejecutores remotos; por lo que tenemos la opción de no registrar todos
como una “entrada”, pero escanea el archivo fuente para
incluidos los encabezados y solo los marcaremos como entradas
se menciona en las sentencias
#include
(sobreestimamos para no tener que implementar un preprocesador de C completo). Esta opción está conectada para "falso" en Bazel y solo se usan en Google. - Una acción puede darse cuenta de que algunos archivos no se usaron durante su ejecución. En C++, esto se denomina “archivos .d”: el compilador indica qué archivos de encabezado se usar después del hecho, y para evitar la vergüenza de tener de incrementalidad que Make, Bazel usa este hecho. Esto ofrece una mejor que el escáner de inclusiones, ya que se basa en el compilador.
Estas se implementan con métodos en Action:
- Se llama a
Action.discoverInputs()
. Debería mostrar un conjunto anidado de Los artefactos que se determina que son obligatorios. Deben ser artefactos de origen para que no haya aristas de dependencia en el gráfico de acción que no tengan un equivalente en el gráfico de destino configurado. - La acción se ejecuta llamando a
Action.execute()
. - Al final de
Action.execute()
, la acción puede llamarAction.updateInputs()
para decirle a Bazel que no todas sus entradas según tus necesidades. Esto puede generar compilaciones incrementales incorrectas si se usa una entrada se informan como sin usar.
Cuando una caché de acciones muestra un hit en una instancia de Action nueva (como la
después de reiniciar el servidor), Bazel llama a updateInputs()
para que el conjunto de
“entradas” refleja el resultado del descubrimiento y la reducción de entradas realizadas anteriormente.
Las acciones de Starlark pueden usar la función para declarar algunas entradas como no utilizadas.
usando el argumento unused_inputs_list=
de
ctx.actions.run()
Distintas formas de ejecutar acciones: estrategias o ActionContexts
Algunas acciones se pueden ejecutar de diferentes maneras. Por ejemplo, una línea de comandos puede ser
que se ejecutan de forma local, local, pero en diversos tipos de zonas de pruebas o de forma remota. El
que lo representa se llama ActionContext
(o Strategy
, ya que
solo funcionó a la mitad con un cambio de nombre).
El ciclo de vida de un contexto de acción es el siguiente:
- Cuando se inicia la fase de ejecución, ¿qué se pregunta a las instancias de
BlazeModule
? y los contextos de acción que tienen. Esto sucede en el constructorExecutionTool
Los tipos de contexto de acción se identifican con unClass
de Java. que hace referencia a una subinterfaz deActionContext
y que que debe implementar el contexto de acción. - El contexto de acción adecuado se selecciona entre los disponibles y se
reenviado a
ActionExecutionContext
yBlazeExecutor
. - Acciones que solicitan contextos con
ActionExecutionContext.getContext()
yBlazeExecutor.getStrategy()
(en realidad, solo debería haber una forma de ...)
Las estrategias son libres de recurrir a otras estrategias para hacer su trabajo; que se usa para ejemplo, en la estrategia dinámica que inicia las acciones de forma local y remota y, luego, usa lo que termine primero.
Una estrategia notable es la que implementa procesos trabajadores persistentes
(WorkerSpawnStrategy
). La idea es que algunas herramientas tienen un largo tiempo de inicio
y, por lo tanto, debe reutilizarse entre acciones en lugar de iniciar una nueva
en cada acción (representa un posible problema de corrección, ya que Bazel
depende de la promesa del proceso trabajador
de que no lleva
estado entre solicitudes individuales)
Si la herramienta cambia, el proceso de trabajador debe reiniciarse. Si un trabajador
que pueden reutilizarse se determina calculando una suma de comprobación para la herramienta utilizada con
WorkerFilesHash
Se basa en saber qué entradas de la acción representan
parte de la herramienta y que representan entradas; lo determina el creador
de la acción: Spawn.getToolFiles()
y los archivos de ejecución de Spawn
son
contarse como partes de la herramienta.
Más información sobre estrategias (o contextos de acción):
- Hay información disponible sobre varias estrategias para ejecutar acciones aquí.
- Información sobre la estrategia dinámica, en la que ejecutamos una acción tanto de forma local y remota para ver qué termina primero está disponible aquí.
- La información sobre las particularidades de ejecutar acciones de forma local está disponible aquí.
El administrador de recursos local
Bazel puede ejecutar muchas acciones en paralelo. La cantidad de acciones locales que debe ejecutarse en paralelo difiere de una acción a otra: cuantos más recursos se que esta acción requiera, se deberían ejecutar menos instancias al mismo tiempo para evitar sobrecargar la máquina local.
Esto se implementa en la clase ResourceManager
: cada acción debe
con una estimación de los recursos locales que requiere en forma de
Instancia de ResourceSet
(CPU y RAM). Luego, cuando los contextos de acción hacen algo
que requiere recursos locales, llama a ResourceManager.acquireResources()
y se bloquean hasta que los recursos necesarios
estén disponibles.
Hay una descripción más detallada de la administración de recursos locales disponible aquí.
La estructura del directorio de salida
Cada acción requiere un lugar separado en el directorio de salida, donde se ubica sus resultados. Por lo general, la ubicación de los artefactos derivados es la siguiente:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
¿Cómo es el nombre del directorio asociado a un directorio en particular configuración determinada? Hay dos propiedades deseables en conflicto:
- Si pueden ocurrir dos configuraciones en la misma compilación, deberían tener directorios diferentes, de manera que ambos puedan tener su propia versión del mismo acción; de lo contrario, si las dos configuraciones no coinciden, como el comando línea de una acción que produce el mismo archivo de salida, Bazel no sabe qué acción a elegir (un "conflicto de acción")
- Si dos configuraciones representan "aproximadamente" lo mismo, deberían tener con el mismo nombre para que las acciones ejecutadas en una se puedan reutilizar en la otra si las líneas de comandos coincidan; por ejemplo, cambia las opciones de la línea de comandos para el compilador de Java no debe generar acciones de compilación de C++ nuevamente.
Hasta ahora, no hemos encontrado una forma sensacional de resolver este problema, lo que tiene similitudes con el problema de los recortes de configuración. Un debate más extenso de opciones disponibles. aquí. Las principales áreas problemáticas son las reglas de Starlark (cuyos autores no suelen tener profundamente con Bazel) y aspectos, que agregan otra dimensión al espacio de cosas que pueden producir el "mismo" archivo de salida.
El enfoque actual es que el segmento de ruta para la configuración
<CPU>-<compilation mode>
con varios sufijos agregados para que la configuración
las transiciones implementadas en Java no generan conflictos de acciones. Además, un
del conjunto de transiciones de configuración de Starlark para que los usuarios
no pueden causar conflictos de acciones. No es para nada perfecto. Esto se implementa en
OutputDirectories.buildMnemonic()
y se basa en cada fragmento de configuración
y agrega su propia parte al nombre del directorio de salida.
Pruebas
Bazel tiene una amplia compatibilidad para ejecutar pruebas. Es compatible con:
- Ejecución de pruebas de forma remota (si hay un backend de ejecución remota disponible)
- Ejecución de pruebas varias veces en paralelo (para reducir o recopilar el tiempo) datos).
- Fragmentación de pruebas (dividiendo casos de prueba en la misma prueba en varios procesos) para conocer la velocidad)
- Volver a ejecutar pruebas inestables
- Agrupa pruebas en paquetes de pruebas
Las pruebas son destinos configurados normales que tienen un TestProvider, que describe cómo debe ejecutarse la prueba:
- Los artefactos cuya compilación dio como resultado la prueba que se estaba ejecutando. Esta es una "caché"
estado" que contenga un mensaje
TestResultData
serializado - La cantidad de veces que se debe ejecutar la prueba
- La cantidad de fragmentos en los que se debe dividir la prueba
- Algunos parámetros sobre cómo se debe ejecutar la prueba (como el tiempo de espera de la prueba)
Determina qué pruebas ejecutar
Determinar qué pruebas se ejecutan es un proceso elaborado.
Primero, durante el análisis de patrones de destino, los conjuntos de pruebas se expanden de manera recursiva. El
la expansión se implementa en TestsForTargetPatternFunction
. Un poco
una dificultad sorprendente es que si un paquete de pruebas no declara ninguna prueba, se refiere a
todas las pruebas en su paquete. Esto se implementa en Package.beforeBuild()
por
Agregar un atributo implícito llamado $implicit_tests
para probar las reglas del paquete
Luego, las pruebas se filtran por tamaño, etiquetas, tiempo de espera e idioma según la
opciones de línea de comandos. Esto se implementa en TestFilter
y se llama desde
TargetPatternPhaseFunction.determineTests()
durante el análisis de destino
el resultado se coloca en TargetPatternPhaseValue.getTestsToRunLabels()
. El motivo
¿Por qué los atributos de la regla que se pueden filtrar no son configurables es que esta
se hace antes de la fase de análisis, por lo tanto, la configuración no es
disponibles.
Luego, esto se procesa aún más en BuildView.createResult()
: destinos cuyo
análisis fallidos se filtran y las pruebas se dividen en exclusivas y
pruebas no exclusivas. Luego, se coloca en AnalysisResult
, que es la forma
ExecutionTool
sabe qué pruebas debe ejecutar.
Para dar algo de transparencia a este proceso elaborado, la tests()
de consultas (implementado en TestsFunction
) está disponible para determinar qué pruebas
cuando se especifica un destino específico en la línea de comandos. Es
pero, lamentablemente, se trata de una reimplementación,
por lo que es probable que se desvíe de lo anterior
varias maneras sutiles.
Cómo ejecutar pruebas
Para ejecutar las pruebas, se solicitan artefactos de estado de caché. Esto, luego,
da como resultado la ejecución de un TestRunnerAction
, que finalmente llama a
TestActionContext
elegida por la opción de línea de comandos --test_strategy
que
ejecuta la prueba de la manera solicitada.
Las pruebas se ejecutan de acuerdo con un protocolo elaborado que usa variables de entorno para indicar a las pruebas qué se espera de ellos. Una descripción detallada de lo que Bazel de las pruebas y qué pruebas pueden esperar de Bazel está disponible aquí. En el más simple: un código de salida de 0 significa que tuvo éxito; todo lo demás, que falla.
Además del archivo de estado de caché, cada proceso de prueba emite varios
archivos. Se colocan en el “directorio de registro de pruebas”. que es el subdirectorio llamado
testlogs
del directorio de salida de la configuración de destino:
test.xml
, un archivo en formato XML de estilo JUnit que detalla los casos de prueba individuales en el fragmento de pruebatest.log
es el resultado de la consola de la prueba. stdout y stderr no son separados.test.outputs
, el "directorio de resultados no declarados"; esto se usa en las pruebas que quieren generar archivos además de los que imprimen en la terminal.
Hay dos cosas que pueden suceder durante la ejecución de prueba que no pueden durante creación de objetivos regulares: ejecución de prueba exclusiva y transmisión de resultados
Algunas pruebas deben ejecutarse en modo exclusivo, por ejemplo, no en paralelo con
otras pruebas. Esto se puede obtener agregando tags=["exclusive"]
al
regla de prueba o ejecutando la prueba con --test_strategy=exclusive
. Cada
prueba se ejecuta mediante una invocación de Skyframe independiente que solicita la ejecución del
después de la prueba "main" compilar. Esto se implementa en
SkyframeExecutor.runExclusiveTest()
A diferencia de las acciones normales, cuyo resultado de la terminal se vuelca cuando la acción
finaliza el proceso, el usuario puede solicitar que se transmita el resultado de las pruebas
a informarse sobre el progreso de una prueba de larga duración. Esto se especifica mediante el
Opción de línea de comandos --test_output=streamed
e implica pruebas exclusivas
la ejecución para que los resultados
de diferentes pruebas no se intercalan.
Esto se implementa en la clase StreamedTestOutput
, cuyo nombre es apropiado, y funciona
sondeo cambia al archivo test.log
de la prueba en cuestión y vuelca nuevos
Bytes a la terminal en la que Bazel reglas.
Los resultados de las pruebas ejecutadas están disponibles en el bus de eventos observando
varios eventos (como TestAttempt
, TestResult
o TestingCompleteEvent
)
Se vuelcan al protocolo de eventos de compilación y se emiten a la consola.
por AggregatingTestListener
Recopilación de cobertura
La cobertura se informa en las pruebas en formato LCOV en los archivos
bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
Para recopilar cobertura, cada ejecución de prueba se une a una secuencia de comandos llamada
collect_coverage.sh
Esta secuencia de comandos configura el entorno de la prueba para habilitar la recopilación de cobertura y determinar dónde se escriben los archivos de cobertura en los entornos de ejecución de cobertura. Luego, ejecuta la prueba. Una prueba puede ejecutar múltiples subprocesos y consistir de partes escritas en múltiples lenguajes de programación diferentes (con entornos de ejecución de recopilación de cobertura). La secuencia de comandos del wrapper se encarga los archivos resultantes en formato LCOV si es necesario y los combina en una sola .
La interposición de collect_coverage.sh
se realiza con estrategias de prueba y
Es necesario que collect_coverage.sh
esté en las entradas de la prueba. Este es
se logra mediante el atributo implícito :coverage_support
, que se resuelve
el valor de la marca de configuración --coverage_support
(consulta la sección
TestConfiguration.TestOptions.coverageSupport
)
Algunos idiomas usan instrumentación sin conexión, lo que significa que la cobertura se agrega durante el tiempo de compilación (como C++), mientras que otros lo hacen en línea. Es decir, la instrumentación de cobertura se agrega en la ejecución tiempo.
Otro concepto fundamental es la cobertura de referencia. Esta es la cobertura de una biblioteca
binario o se prueba si no se ejecutó ningún código. El problema que resuelve es que si
si quieres calcular la cobertura de prueba de un objeto binario, no basta con combinar
la cobertura de todas las pruebas, ya que puede haber un código en el objeto binario que no
no se puede vincular
con ninguna prueba. Por lo tanto, lo que hacemos es emitir un archivo de cobertura para cada
binario que contiene solo los archivos para los que recopilamos cobertura, sin
a una línea de producción de datos. El archivo de cobertura de referencia para un objetivo está en
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
También se generan
para binarios y bibliotecas, además de las pruebas si pasas el
marca --nobuild_tests_only
para Bazel.
Actualmente, la cobertura del modelo de referencia no funciona.
Hacemos un seguimiento de dos grupos de archivos para la recopilación de cobertura para cada regla: el conjunto de los archivos de instrumentación y el conjunto de archivos de metadatos de instrumentación.
El conjunto de archivos instrumentados es solo eso, un conjunto de archivos para instrumentar. Para de cobertura en línea, se puede usar en el tiempo de ejecución para decidir qué archivos instrumento. También se usa para implementar la cobertura de referencia.
El conjunto de archivos de metadatos de instrumentación es el conjunto de archivos adicionales que necesita una prueba para generar los archivos LCOV que Bazel requiere. En la práctica, esto consiste en archivos específicos del entorno de ejecución; Por ejemplo, gcc emite archivos .gcno durante la compilación. Estos se agregan al conjunto de entradas de acciones de prueba si el modo de cobertura está habilitado.
Si se recopila o no la cobertura se almacena
BuildConfiguration
Esto es útil porque es una manera fácil de cambiar la prueba
acción y el gráfico de acción en función de este bit, pero también significa que,
este bit está invertido, todos los objetivos se deben volver a analizar (algunos lenguajes, como
C++ requiere diferentes opciones de compilador para emitir código que pueda recopilar cobertura.
lo que mitiga este problema en cierta medida, ya que entonces se necesita un nuevo análisis de todas formas).
Se depende de los archivos de compatibilidad de cobertura a través de etiquetas en una de modo que la política de invocación pueda anularlos, lo que permite para diferenciarlas entre las diferentes versiones de Bazel. Idealmente, estas las diferencias y estandarizamos una de ellas.
También generamos un "informe de cobertura" que combina la cobertura recopilada para
cada prueba en una invocación de Bazel. Esto lo controla
CoverageReportActionFactory
y se llama desde BuildView.createResult()
. Integra
obtiene acceso a las herramientas que necesita consultando :coverage_report_generator
.
de la primera prueba que se ejecuta.
El motor de consultas
Bazel tiene un poco lenguaje solía preguntarle varias cosas sobre diversos gráficos. Los siguientes tipos de consultas se proporcionan los siguientes datos:
bazel query
se usa para investigar el gráfico de destino.bazel cquery
se usa para investigar el gráfico de destino configurado.bazel aquery
se usa para investigar el gráfico de acciones.
Cada uno de estos se implementa mediante la subclasificación AbstractBlazeQueryEnvironment
.
Se pueden realizar otras funciones de consulta adicionales subclasificando QueryFunction
.
de Google Cloud. Para permitir la transmisión de resultados de la consulta, en lugar de recopilarlos a algunos
estructura de datos, se pasa un query2.engine.Callback
a QueryFunction
, que
lo llama para obtener resultados que quiere devolver.
El resultado de una consulta puede emitirse de varias maneras: etiquetas, etiquetas y reglas.
clases, XML, protobuf, etcétera. Estos se implementan como subclases de
OutputFormatter
Un requisito sutil de algunos formatos de resultados de consultas (proto, definitivamente) es que Bazel necesita emitir _toda _la información que proporciona la carga de paquetes para que se puede diferenciar el resultado y determinar si un objetivo en particular ha cambiado. En consecuencia, los valores de atributos deben ser serializables. hay pocos tipos de atributos, sin atributos que tengan Starlark complejo. de salida. La solución alternativa habitual es usar una etiqueta y adjuntar el información a la regla con esa etiqueta. No es una solución alternativa muy satisfactoria Sería bueno quitar este requisito.
El sistema de módulos
Bazel se puede extender agregando módulos. Cada módulo debe subclasificar
BlazeModule
(el nombre es una reliquia de la historia de Bazel cuando solía ser
llamada Blaze) y obtiene información sobre varios eventos durante la ejecución de
un comando.
Por lo general, se usan para implementar varias piezas funcionalidad que solo algunas versiones de Bazel (como la que usamos en Google) necesitan:
- Interfaces para los sistemas de ejecución remota
- Comandos nuevos
El conjunto de puntos de extensión que ofrece BlazeModule
es un poco aleatorio. Lo que no debes hacer
usarlo como ejemplo de buenos principios de diseño.
El bus de eventos
La forma principal en que BlazeModules se comunica con el resto de Bazel es a través de un bus de eventos.
(EventBus
): Se crea una instancia nueva para cada compilación, en varias partes de Bazel.
pueden publicar eventos en él, y los módulos pueden registrar objetos de escucha para los eventos en los que
les interesa. Por ejemplo, los siguientes elementos se representan como eventos:
- Se determinó la lista de objetivos de compilación que se compilarán.
(
TargetParsingCompleteEvent
) - Se determinaron las configuraciones de nivel superior
(
BuildConfigurationEvent
) - Se compiló un destino, ya sea correctamente o no (
TargetCompleteEvent
) - Se ejecutó una prueba (
TestAttempt
,TestSummary
)
Algunos de estos eventos están representados fuera de Bazel en el
Protocolo de eventos de compilación
(son BuildEvent
). Esto permite elementos BlazeModule
y elementos
fuera del proceso de Bazel para observar la compilación. Se puede acceder a ellos como un
que contiene mensajes de protocolo, o Bazel puede conectarse a un servidor (llamado
(el servicio Build Event) para transmitir eventos.
Esto se implementa en build.lib.buildeventservice
y
build.lib.buildeventstream
paquetes de Java.
Repositorios externos
Por su parte, Bazel se diseñó originalmente para usarse en un monorepo (una única fuente con todo lo que se necesita construir), Bazel vive en un mundo donde esto no es necesariamente cierto. “Repositorios externos” son una abstracción que se usa para unen estos dos mundos: representan código necesario para la compilación, pero no está en el árbol de fuentes principal.
El archivo WORKSPACE
El conjunto de repositorios externos se determina mediante el análisis del archivo WORKSPACE. Por ejemplo, una declaración como la siguiente:
local_repository(name="foo", path="/foo/bar")
Está disponible el repositorio llamado @foo
. Dónde llega esto
es que se pueden definir nuevas reglas
de repositorio en archivos de Starlark,
se puede usar para cargar un nuevo código de Starlark, que se puede usar para definir nuevos
las reglas del repositorio y así sucesivamente.
Para manejar este caso, el análisis del archivo WORKSPACE (en
WorkspaceFileFunction
) se divide en fragmentos delineados por load()
.
declaraciones. El índice del fragmento se indica con WorkspaceFileKey.getIndex()
y
calcular WorkspaceFileFunction
hasta que el índice X significa evaluarlo hasta el
Xa sentencia load()
.
Recupera repositorios
Antes de que el código del repositorio esté disponible para Bazel, se debe
recuperado. Como resultado, Bazel crea un directorio en
$OUTPUT_BASE/external/<repository name>
La recuperación del repositorio se realiza en los siguientes pasos:
PackageLookupFunction
se da cuenta de que necesita un repositorio y crea unRepositoryName
comoSkyKey
, que invocaRepositoryLoaderFunction
RepositoryLoaderFunction
reenvía la solicitud aRepositoryDelegatorFunction
por motivos poco claros (el código indica que evitar volver a descargar elementos en caso de reinicios de Skyframe, pero no es una un razonamiento muy sólido)RepositoryDelegatorFunction
descubre la regla del repositorio que se le solicita. recupera mediante la iteración sobre los fragmentos del archivo WORKSPACE hasta que se realice se encontró el repositorio- Se encontró el
RepositoryFunction
adecuado que implementa el repositorio fetching; es la implementación de Starlark del repositorio mapa hard-coded para los repositorios que se implementan en Java.
Hay varias capas de almacenamiento en caché, ya que recuperar un repositorio puede ser costoso:
- Hay una caché para los archivos descargados que tiene como clave su suma de comprobación.
(
RepositoryCache
). Esto requiere que la suma de comprobación esté disponible en el WORKSPACE, pero eso es bueno para la hermeticidad de todos modos. Lo comparte cada instancia del servidor de Bazel en la misma estación de trabajo, sin importar cuál o base de salida en la que se ejecuten. - Un "archivo de marcador" se escribe para cada repositorio en
$OUTPUT_BASE/external
que contiene una suma de comprobación de la regla que se usó para recuperarla. Si el nombre se reinicia el servidor, pero la suma de verificación no cambia ni se vuelve a recuperar. Esta se implementa enRepositoryDelegatorFunction.DigestWriter
. - La opción de línea de comandos
--distdir
designa otra caché que se usa para buscar artefactos para descargar Esto es útil en la configuración empresarial y no debería recuperar elementos aleatorios de Internet. Este es implementada porDownloadManager
.
Una vez que se descarga el repositorio, los artefactos que contiene se tratan como fuente
artefactos. Esto plantea un problema porque Bazel suele buscar la actualización.
de artefactos de origen llamando a stat(), y estos artefactos también son
se invalidan cuando cambia la definición
del repositorio en el que se encuentran. Por lo tanto,
Los FileStateValue
de un artefacto en un repositorio externo deben depender de
a su repositorio externo. ExternalFilesHelper
se encarga de esta tarea.
Asignaciones de repositorios
Puede suceder que varios repositorios deseen depender del mismo repositorio,
pero en versiones diferentes (este es un ejemplo de la "dependencia de diamante
problema"). Por ejemplo, si dos objetos binarios en repositorios separados en la compilación
quieren depender de Guava, presumiblemente que ambas se refieren a Guava con etiquetas
a partir del @guava//
y esperar que eso signifique diferentes versiones de él.
Por lo tanto, Bazel permite reasignar etiquetas de repositorios externos para que las
la cadena @guava//
se puede referir a un repositorio de Guava (como @guava1//
) en el
repositorio de un objeto binario y otro repositorio de Guava (como @guava2//
), el
de la otra.
De manera alternativa, también se puede usar para unir diamantes. Si un repositorio
depende de @guava1//
y otro depende de @guava2//
, la asignación del repositorio
permite reasignar ambos repositorios para usar un repositorio @guava//
canónico.
La asignación se especifica en el archivo WORKSPACE como el atributo repo_mapping
de las definiciones de repositorios individuales. Luego, aparece en Skyframe como miembro de
WorkspaceFileValue
, donde se conecta a:
Package.Builder.repositoryMapping
, que se usa para transformar los valores de etiquetas atributos de reglas en el paqueteRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
, que se usa en la fase de análisis (para resuelve cuestiones como$(location)
, que no se analizan en la etapa de carga fase)BzlLoadFunction
para resolver etiquetas en sentencias load()
Bits de JNI
El servidor de Bazel está en su mayoría escrito en Java. La excepción son las partes que Java no puede hacerlo por sí solo o no podía hacerlo cuando lo implementamos. Esta se limita a la interacción con el sistema de archivos, el control de procesos y muchas otras cosas de bajo nivel.
El código C++ está en src/main/native y las clases Java con código nativo métodos son los siguientes:
NativePosixFiles
yNativePosixFileSystem
ProcessUtils
WindowsFileOperations
yWindowsFileProcesses
com.google.devtools.build.lib.platform
Resultado de la consola
Emitir resultados de la consola parece algo simple, pero la confluencia de la ejecución varios procesos (a veces de forma remota), almacenamiento en caché detallado, el deseo de tener una salida de terminal agradable y colorida, y tener un servidor de larga duración no es trivial.
Inmediatamente después de que el cliente ingresa la llamada RPC, hay dos RpcOutputStream
se crean instancias (para stdout y stderr) que reenvían los datos impresos en
con ellos al cliente. Luego, se unen en un OutErr
(un (stdout, stderr)
de sincronización). Todo lo que se deba imprimir en la consola pasa por estas
transmisiones continuas. Luego, estas transmisiones se transfieren
BlazeCommandDispatcher.execExclusively()
El resultado se imprime de forma predeterminada con secuencias de escape ANSI. Cuando no son
deseado (--color=no
), se quitan con una AnsiStrippingOutputStream
. En
Además, System.out
y System.err
se redireccionan a estos flujos de salida.
Esto es para que la información de depuración se pueda imprimir usando
System.err.println()
y aún terminarán en la salida de la terminal del cliente.
(que es diferente de la del servidor). Se tiene cuidado de que si un proceso
produce un resultado binario (como bazel query --output=proto
), no se combina stdout
antes de la entrevista.
Los mensajes cortos (errores, advertencias y similares) se expresan a través del
EventHandler
. En particular, son diferentes de lo que se publica en
el EventBus
(esto es confuso). Cada Event
tiene un EventKind
(error,
advertencia, información y otros) y pueden tener un Location
(el lugar en
el código fuente que causó el evento).
Algunas implementaciones de EventHandler
almacenan los eventos que recibieron. Se usa
para volver a reproducir información en la IU
causada por varios tipos de procesamiento
como las advertencias emitidas por un destino configurado
en caché.
Algunos EventHandler
también permiten publicar eventos que, finalmente, encuentran su camino a
el bus de eventos (los Event
normales _no _aparecen allí). Son
implementaciones de ExtendedEventHandler
y su uso principal es volver a reproducir contenido almacenado en caché
EventBus
eventos. Todos estos eventos EventBus
implementan Postable
, pero no
todo lo que se publica en EventBus
necesariamente implementa esta interfaz.
solo aquellos que están almacenados en caché por un ExtendedEventHandler
(sería bueno y
hacen la mayoría de las cosas; aunque no se aplica de manera forzosa)
El resultado de la terminal se emite en su mayoría a través de UiEventHandler
, que es
Es responsable de todos los formatos de salida y los informes de progreso sofisticados de Bazel.
hace. Tiene dos entradas:
- El bus de eventos
- La transmisión del evento se canalizó a través de Reporter.
La única conexión directa a la maquinaria de ejecución de comandos (por ejemplo, el resto del
Bazel) a la transmisión de RPC al cliente es a través de Reporter.getOutErr()
lo que permite el acceso directo a estas transmisiones. Solo se usa cuando un comando necesita
para volcar grandes cantidades de datos binarios posibles (como bazel query
).
Cómo generar perfiles de Bazel
Bazel es rápido. Bazel también es lento, ya que las compilaciones tienden a crecer hasta solo
límite de lo que es soportable. Por este motivo, Bazel incluye un generador de perfiles que puede
para generar perfiles de compilaciones y Bazel. Se implementa en una clase
llamado Profiler
de forma correcta. Está activado de forma predeterminada, aunque solo registra
datos resumidos para que su sobrecarga sea tolerable; La línea de comandos
--record_full_profiler_data
permite que grabe todo lo que pueda.
Emite un perfil en el formato del generador de perfiles de Chrome. se ve mejor en Chrome. Su modelo de datos es el de pilas de tareas: uno puede iniciar y finalizar tareas, y se supone que deben anidarse cuidadosamente entre sí. Cada subproceso de Java obtiene su propia pila de tareas. TODO: ¿Cómo funciona esto con las acciones y estilo que transmite la continuación?
El generador de perfiles se inicia y se detiene en BlazeRuntime.initProfiler()
.
BlazeRuntime.afterCommand()
respectivamente y los intentos de mantenerse activos por el mismo tiempo
como sea posible para
que podamos perfilar todo. Para agregar algo al perfil,
llama a Profiler.instance().profile()
. Muestra un Closeable
cuyo cierre
representa el final de la tarea. Se usa mejor con
pruebas con recursos
declaraciones.
También realizamos perfiles de memoria rudimentarios en MemoryProfiler
. También está siempre activada
y en su mayoría registra los tamaños máximos de montón y el comportamiento de GC.
Prueba Bazel
Bazel tiene dos tipos principales de pruebas: unas que observan a Bazel como una “caja negra”. y los que solo ejecutan la fase de análisis. Las anteriores "pruebas de integración" y las últimas "pruebas de unidades", aunque son más parecidas a las pruebas de integración que están, bueno, menos integrados. También tenemos algunas pruebas de unidades reales, necesario.
Entre las pruebas de integración, tenemos dos tipos:
- Unas implementadas con un framework de prueba bash muy elaborado bajo
src/test/shell
- Las que están implementadas en Java. Estos se implementan como subclases de
BuildIntegrationTestCase
BuildIntegrationTestCase
es el framework de prueba de integración preferido, ya que
esté bien equipado para la mayoría de los escenarios de prueba. Como es un framework de Java,
proporciona capacidad de depuración e integración perfecta con muchas aplicaciones comunes de desarrollo
con herramientas de visualización. Hay muchos ejemplos de clases BuildIntegrationTestCase
en la
Repositorio de Bazel.
Las pruebas de análisis se implementan como subclases de BuildViewTestCase
. Hay un
un sistema de archivos temporal que puedes usar para escribir archivos BUILD
y varios archivos auxiliares
pueden solicitar destinos configurados, cambiar la configuración
varios aspectos sobre el resultado del análisis.