Sistemas de compilación basados en artefactos

Informar un problema . Ver fuente . Por la noche · 7.2 · 7.1 · 7.0 · 6.5 · 6.4

Esta página abarca los sistemas de compilación basados en artefactos y la filosofía detrás de sus de la creación de cuentas de servicio. Bazel es un sistema de compilación basado en artefactos. Mientras que la compilación basada en tareas los sistemas son un buen paso por encima de las secuencias de comandos de compilación, ya que brindan demasiada capacidad a a ingenieros individuales permitiéndoles definir sus propias tareas.

Los sistemas de compilación basados en artefactos tienen una pequeña cantidad de tareas definidas por el sistema. que los ingenieros pueden configurar de forma limitada. Los ingenieros aún le dicen al sistema qué compilar, pero el sistema de compilación determina cómo hacerlo. Al igual que con sistemas de compilación basados en tareas, sistemas de compilación basados en artefactos, como Bazel, tienen archivos de compilación, pero su contenido es muy diferente. Antes que que ser un conjunto imperativo de comandos en un lenguaje de programación completo de Turing. que describen cómo producir un resultado, los archivos de compilación en Bazel son una herramienta manifiesto que describe un conjunto de artefactos para compilar, sus dependencias y un un conjunto limitado de opciones que afectan la forma en que se crean. Cuando los ingenieros ejecutan bazel en la línea de comandos, especifican un conjunto de destinos para compilar (el qué) y Bazel es responsable de configurar, ejecutar y programar la compilación. pasos (el cómo). Debido a que el sistema de compilación ahora tiene el control total para ejecutar y cuándo, puede dar garantías mucho más contundentes que le permitan sea más eficiente, a la vez que se garantiza la precisión.

Una perspectiva funcional

Es fácil crear una analogía entre los sistemas de compilación basados en artefactos y los sistemas programación. Los lenguajes de programación imperativos tradicionales (como Java, C y Python) especifican listas de sentencias que se ejecutarán una tras otra, en el de la misma manera que los sistemas de compilación basados en tareas permiten a los programadores definir una serie de pasos ejecutar. Los lenguajes de programación funcionales (como Haskell y ML) en contrastan, se estructuran más como una serie de ecuaciones matemáticas. En lenguajes funcionales, el programador describe un cálculo a realizar, pero deja al equipo la información sobre cuándo y cómo se ejecuta ese cálculo compilador.

Esto se relaciona con la idea de declarar un manifiesto en un sistema de compilación basado en artefactos. y dejar que el sistema descubra la manera de ejecutar la compilación. Muchos problemas no pueden expresarse fácilmente mediante la programación funcional, pero los que sí se benefician en gran medida a partir de él: el lenguaje a menudo es capaz de paralelizar trivialmente ese tipo de programas y garantizar sólidas garantías sobre su precisión que serían imposibles en un lenguaje imperativo. Los problemas más fáciles de expresar mediante la programación funcional son las que simplemente implican transformar una pieza de datos en otro mediante una serie de reglas o funciones. Eso es exactamente Qué es un sistema de compilación: todo el sistema es efectivamente una función matemática. que toma archivos de origen (y herramientas como el compilador) como entradas y produce binarios como salidas. Por lo tanto, no es de extrañar que funcione bien sistema en torno a los principios de la programación funcional.

Información sobre los sistemas de compilación basados en artefactos

El sistema de compilación de Google, Blaze, fue el primer sistema de compilación basado en artefactos. Bazel es la versión de código abierto de Blaze.

Así se ve un archivo de compilación (que normalmente se llama BUILD) en Bazel:

java_binary(
    name = "MyBinary",
    srcs = ["MyBinary.java"],
    deps = [
        ":mylib",
    ],
)
java_library(
    name = "mylib",
    srcs = ["MyLibrary.java", "MyHelper.java"],
    visibility = ["//java/com/example/myproduct:__subpackages__"],
    deps = [
        "//java/com/example/common",
        "//java/com/example/myproduct/otherlib",
    ],
)

En Bazel, los archivos BUILD definen los destinos; estos son los dos tipos de destinos java_binary y java_library. Cada destino corresponde a un artefacto que el sistema puede crear: los objetivos binarios producen objetos binarios que pueden se ejecutan directamente, y los destinos de las bibliotecas producen bibliotecas binarios u otras bibliotecas. Cada objetivo tiene:

  • name: Indica cómo se hace referencia al destino en la línea de comandos y en otras destinos
  • srcs: Son los archivos de origen que se compilarán para crear el artefacto del destino.
  • deps: Otros destinos que se deben compilar antes que este y vincular a it

Las dependencias pueden estar dentro del mismo paquete (como las de MyBinary dependencia en :mylib) o en un paquete diferente en la misma jerarquía de fuentes (como la dependencia de mylib en //java/com/example/common).

Al igual que con los sistemas de compilación basados en tareas, puedes realizar compilaciones usando la línea de comandos de Bazel herramienta. Para compilar el destino MyBinary, ejecuta bazel build :MyBinary. Después del ingresando ese comando por primera vez en un repositorio limpio, Bazel:

  1. Analiza cada archivo BUILD en el espacio de trabajo para crear un gráfico de dependencias. entre artefactos.
  2. Usa el gráfico para determinar las dependencias transitivas de MyBinary. que cada objetivo del que depende MyBinary y cada uno de los objetivos de los que de la que dependen los objetivos, de forma recursiva.
  3. Compila cada una de esas dependencias, en orden. Bazel comienza por compilar cada objetivo que no tiene otras dependencias y que realiza un seguimiento de qué dependencias que aún se deben crear para cada objetivo. En cuanto todas las solicitudes de las dependencias, Bazel comenzará a compilar ese destino. Este proceso continúa hasta que cada una de las dependencias transitivas de MyBinary construyen.
  4. Compila MyBinary para producir un objeto binario ejecutable final que se vincula en todos las dependencias que se compilaron en el paso 3.

En esencia, puede parecer que lo que está sucediendo aquí diferente de lo que sucedía cuando se usaba un sistema de compilación basado en tareas. De hecho, resultado final es el mismo objeto binario, y el proceso para producirlo involucró analizar varios pasos para encontrar dependencias entre ellos y, luego, ejecutar los pasos en orden. Pero existen diferencias críticas. El primero aparece paso 3: como Bazel sabe que cada destino solo produce una biblioteca de Java, sabe que lo único que tiene que hacer es ejecutar el compilador de Java en lugar de una ejecución definida por el usuario, de modo que sepa que es seguro ejecutar estos pasos en paralelo. Esto puede producir una mejora del rendimiento de orden de magnitud en comparación con la compilación se orienta de a uno por vez en una máquina de varios núcleos, y solo es posible porque el enfoque basado en artefactos deja al sistema de compilación a cargo de su propia ejecución para garantizar más garantías sobre el paralelismo.

Sin embargo, los beneficios se extienden más allá del paralelismo. Otra cosa que este Este enfoque se vuelve evidente cuando el desarrollador escribe bazel build :MyBinary por segunda vez sin hacer ningún cambio: Bazel sale en menos. de un segundo con un mensaje que dice que el destino está actualizado. Este es gracias al paradigma de programación funcional del que hablamos, antes: Bazel sabe que cada destino es el resultado únicamente de la ejecución de un compilador y sabe que el resultado del compilador de Java solo depende de sus entradas, de modo que las salidas se puedan volver a usar, siempre y cuando estas no hayan cambiado. Y este análisis funciona en todos los niveles; Si MyBinary.java cambia, Bazel sabrá para recompilar MyBinary, pero reutilizar mylib. Si un archivo fuente para //java/com/example/common, Bazel sabrá que debe volver a compilar esa biblioteca mylib y MyBinary, pero reutiliza //java/com/example/myproduct/otherlib. Debido a que Bazel conoce las propiedades de las herramientas que ejecuta en cada paso, puede reconstruir solo el conjunto mínimo de artefactos cada vez, mientras lo que garantiza que no producirá compilaciones inactivas.

Reformular el proceso de compilación en términos de artefactos en lugar de tareas es sutil. pero poderosa. Al reducir la flexibilidad expuesta al programador, la creación puede saber más sobre lo que se hace en cada paso de la compilación. Puede usar estos conocimientos para que la compilación sea mucho más eficiente mediante la paralelización de la compilación procesos y reutilizar sus resultados. Pero en realidad este es solo el primer paso, y estos componentes básicos de paralelismo y reutilización son la base de un entorno altamente escalable y eficiente.

Otros trucos ingeniosos de Bazel

Los sistemas de compilación basados en artefactos resuelven fundamentalmente los problemas con el paralelismo. y reutilización que son inherentes a los sistemas de compilación basados en tareas. Pero todavía hay un algunos problemas que surgieron antes y que no hemos abordado. Bazel es inteligente de resolver cada una de ellas y deberíamos analizarlas antes de continuar.

Herramientas como dependencias

Un problema con el que nos encontramos antes fue que las compilaciones dependían de las herramientas instaladas en nuestra máquina, y podría ser difícil reproducir las compilaciones en todos los sistemas debido a en diferentes versiones o ubicaciones de las herramientas. El problema se vuelve aún más difícil cuando en tu proyecto se usan lenguajes que requieren herramientas distintas plataforma para la que se compilan o compilan (por ejemplo, Windows versus Linux), y cada una de esas plataformas requiere un conjunto de herramientas un poco diferente mismo trabajo.

Para resolver la primera parte de este problema, Bazel trata a las herramientas como dependencias para para cada objetivo. Cada java_library del espacio de trabajo depende implícitamente de un archivo que usa un compilador conocido de forma predeterminada. Cada vez que Bazel crea un java_library, comprueba que el compilador especificado esté disponible. en una ubicación conocida. Como con cualquier otra dependencia, si el compilador de Java cambia, cada artefacto que depende de él se reconstruye.

Bazel resuelve la segunda parte del problema, la independencia de la plataforma, estableciendo parámetros de configuración de compilación. En lugar de los destinos en función directamente de sus herramientas, dependen de los tipos de configuración:

  • Configuración del host: herramientas de compilación que se ejecutan durante la compilación
  • Target configuration: Compila el objeto binario que solicitaste en última instancia.

Cómo extender el sistema de compilación

Bazel incluye objetivos para varios lenguajes de programación populares. pero los ingenieros siempre querrán hacer más, como parte del beneficio de los modelos es su flexibilidad para admitir cualquier tipo de proceso de compilación. sería mejor no renunciar a eso en un sistema de compilación basado en artefactos. Afortunadamente, Bazel permite que sus tipos de destinos admitidos se extiendan agregar reglas personalizadas.

Para definir una regla en Bazel, el autor de la regla declara las entradas que la regla requiere (en forma de atributos pasados en el archivo BUILD) y la conjunto de salidas que produce la regla. El autor también define las acciones que una regla de firewall. Cada acción declara sus entradas y salidas, ejecuta un ejecutable en particular o escribe una cadena determinada en un archivo, y puede conectadas a otras acciones a través de sus entradas y salidas. Esto significa que las acciones son la unidad de componibilidad de nivel más bajo en el sistema de compilación, una acción puede hacer lo que quiera, siempre que use solo las entradas y salidas declaradas. Bazel se encarga de programar las acciones y almacenar en caché sus resultados según corresponda.

El sistema no es infalible, ya que no hay forma de detener a un desarrollador de acciones. de hacer algo como introducir un proceso no determinista como parte de su acción. Sin embargo, esto no ocurre muy a menudo en la práctica, y se está impulsando las posibilidades de abuso, hasta el nivel de acción, disminuyen en gran medida oportunidades de errores. Las reglas compatibles con muchos lenguajes y herramientas comunes son ampliamente disponibles en línea, y la mayoría de los proyectos nunca tendrán que definir su las reglas de firewall. Incluso para los casos en los que sí lo tienen, las definiciones de las reglas solo deben definirse en una en un lugar central del repositorio, lo que significa que la mayoría de los ingenieros podrán usar de implementar esas reglas sin preocuparse nunca por su implementación.

Aislar el entorno

Las acciones parecen tener los mismos problemas que las tareas de otras sistemas, ¿no es posible escribir acciones que ambas escriban en la misma y terminan en conflicto? En realidad, Bazel hace estas conflictos imposibles con la zona de pruebas Compatible sistemas, cada acción está aislada del resto a través de un sistema de archivos en la zona de pruebas. Efectivamente, cada acción puede obtener solo una vista restringida de la sistema de archivos que incluya las entradas que declaró y cualquier salida que tenga producidos. Esto se aplica a sistemas como LXC en Linux, la misma tecnología detrás de Docker. Esto significa que es imposible que las acciones entren en conflicto con una porque no pueden leer archivos que no declaran, así como los archivos que escribe, pero que no declara, se desecharán cuando la acción para finalizar la tarea. Bazel también usa zonas de pruebas para impedir que las acciones se comuniquen a través de la red.

Haz que las dependencias externas sean deterministas

Todavía queda un problema: los sistemas de compilación a menudo necesitan descargar dependencias (ya sean herramientas o bibliotecas) de fuentes externas en lugar de compilarlos directamente. Esto se puede observar en el ejemplo a través de la Dependencia @com_google_common_guava_guava//jar, que descarga un archivo JAR de Maven.

El uso de archivos fuera del espacio de trabajo actual es riesgoso. Esos archivos podrían cambian en cualquier momento, lo que podría requerir que el sistema de compilación verifique constantemente si están actualizados. Si un archivo remoto cambia sin los cambios correspondientes en el código fuente del lugar de trabajo, también puede generar compilaciones irreproducibles, una compilación podría funcionar un día y fallar al siguiente sin motivo aparente debido a un evento cambio de dependencia. Por último, una dependencia externa puede introducir una gran seguridad cuando es propiedad de un tercero: si un atacante puede infiltrarse en ese servidor de terceros, pueden reemplazar el archivo de dependencia con algo como su propio diseño, lo que le da el control total de tu compilación entorno y su resultado.

El problema fundamental es que queremos que el sistema de compilación archivos sin tener que incluirlos en el control de código fuente. Cómo actualizar una dependencia debe ser una elección consciente, pero debe hacerse una vez en lugar de ser gestionados por ingenieros individuales o automáticamente por el en un sistema de archivos. Esto se debe a que, incluso con un modelo “Live at Head”, seguimos buscando compilaciones sea determinista, lo que implica que, si compruebas una confirmación de la última deberías ver tus dependencias tal como estaban en ese momento y no como están ahora mismo.

Bazel y algunos otros sistemas de compilación solucionan este problema solicitando un de workspacewide que enumera un hash criptográfico para cada una dependencia en el espacio de trabajo. El hash es una forma concisa de representar de manera inequívoca sin cargar todo el archivo en el control de código fuente. Cada vez que se crea un nuevo se hace referencia a una dependencia externa desde un lugar de trabajo, el hash de esa dependencia se agregar al manifiesto, ya sea manual o automáticamente. Cuando Bazel ejecuta un esta compilación compara el hash real de su dependencia almacenada en caché con el hash el hash definido en el manifiesto y vuelve a descargar el archivo solo si el hash difiere.

Si el artefacto que descargamos tiene un hash diferente al declarado en la la compilación fallará a menos que se actualice el hash del manifiesto. Esta se puede hacer automáticamente, pero ese cambio debe aprobarse y registrarse el control de fuente antes de que la compilación acepte la dependencia nueva. Esto significa que siempre hay un registro de cuándo se actualizó una dependencia y una solicitud La dependencia no puede cambiar sin el cambio correspondiente en la fuente del lugar de trabajo. También significa que, cuando se comprueba una versión anterior del código fuente, el se garantiza que se usen las mismas dependencias que se usaron en ese momento cuando se registró esa versión (de lo contrario, fallará si esas dependencias se ya no están disponibles).

Por supuesto, puede seguir siendo un problema si un servidor remoto deja de estar disponible comienza a entregar datos dañados; esto puede hacer que todas tus compilaciones comiencen a fallar si no tienes disponible otra copia de esa dependencia. Para evitar esto, recomendamos que, para cualquier proyecto no trivial, dupliques todos sus dependencias en servidores o servicios confiables y controlados. De lo contrario, siempre estará a merced de un tercero por la seguridad del sistema de acceso, incluso si los hashes registrados garantizan su seguridad.