Una descripción general de cómo aprender a construir sistemas desde cero

fondo

Recientemente, he estado haciendo trabajos relacionados con la compilación y la optimización. En el proceso de trabajo, descubrí que había estado ignorando algo importante. A diferencia de los proyectos simples que escribí, un proyecto maduro se construye en base al sistema de construcción. Si no entiendo la construcción También es difícil optimizar el sistema Después de estudiar por mí mismo, espero acumular estos contenidos a través de algunos artículos. Espero convertir estos contenidos en una serie. En primer lugar, tendré una comprensión macro de los conceptos básicos del sistema de construcción y luego introduciré los sistemas de construcción de uso común, como cmake, gradle y bazel.

¿Qué es un sistema de construcción?

Entonces, ¿qué es un sistema de construcción? Se puede dividir en compilación y sistema, y ​​la definición de sistema es la siguiente:

系统是由相互作用相互依赖的若干组成部分结合而成的,具有特定功能的有机整体,而且这个有机整体又是它从属的更大系统的组成部分。

Entonces, ¿cómo entendemos la construcción?

与构建相关联的是编译,编译是将一个源文件转换成一个二进制文件,而构建就是对于编译的安排,在一个大的工程中包含很多源文件,其中可能还包含这复杂的依赖关系,构建就是对于多个编译的合理安排。

Sistema de compilación: un todo orgánico con la organización de múltiples funciones de compilación.

¿Por qué necesita un sistema de compilación?

La mayoría de los ingenieros usan ejemplos muy simples cuando aprenden a programar. Es posible que el ejemplo solo tenga un archivo fuente, por lo que la mayoría de los ingenieros comienzan llamando directamente a herramientas como gcc o javac, o usando las herramientas de compilación convenientes proporcionadas en el IDE, como El siguiente ejemplo convierte el código fuente en el mismo directorio en un archivo binario.

javac *.java

javac es muy inteligente y puede buscar código para importar en subdirectorios del directorio actual. Pero no puede encontrar el código almacenado en otras partes del sistema de archivos (quizás una biblioteca compartida por varios proyectos). También solo sabe cómo estructurar el código Java. Los sistemas grandes a menudo involucran diferentes partes escritas en varios lenguajes de programación, con redes entre estas partes, lo que significa que un solo compilador no puede construir todo el sistema.

Cuando el proyecto se vuelve cada vez más complejo, un simple comando de compilación no puede cumplir con los requisitos y se requiere una combinación de múltiples comandos de compilación. En este momento, puede pensar en usar scripts de shell para organizar los comandos de compilación. Estos scripts construirán la aplicación. en el orden correcto, pero como el proyecto La expansión adicional, el shell tampoco puede hacer lo que quiere y encontrará muchos problemas:

  • Las compilaciones se vuelven tediosas. A medida que los sistemas se vuelven más complejos, dedica casi tanto tiempo a crear sus scripts como a su código real. La depuración de scripts de shell es una molestia, y cada vez más hacks se superponen.
  • muy lento. Para asegurarse de no depender accidentalmente de bibliotecas obsoletas, debe hacer que el script de compilación genere cada dependencia en orden cada vez que se ejecuta. Entonces, uno consideraría agregar algo de lógica para detectar qué partes deben reconstruirse, pero eso suena muy complejo y propenso a errores para una secuencia de comandos que se volverá cada vez más compleja de administrar.

Así que mi propio entendimiento es que desde el shell gcc inicial hasta el sistema de construcción perfecto actual, es una abstracción adicional del proceso de compilación,
agregando una capa de abstracción para hacer que la compilación y la construcción sean más fáciles de entender y mantener, y la capa inferior no lo mismo, solo para que sea más fácil de entender y mantener.

Clasificación de los sistemas de construcción.

sistema de construcción basado en tareas

En un sistema de compilación basado en tareas, la unidad básica de trabajo es una tarea. Cada tarea es un script que puede realizar cualquier tipo de lógica que especifique otras tareas como dependencias que deben ejecutarse antes. La mayoría de los principales sistemas de compilación que se utilizan en la actualidad (como Ant, Maven, Gradle, Grunt y Rake) se basan en tareas. La mayoría de los sistemas de compilación modernos requieren que los ingenieros creen archivos de compilación que describan cómo se ejecutan los archivos, en lugar de scripts de shell.

<project name="MyProject" default="dist" basedir=".">
   <description>
     simple example build file
   </description>
   <!-- set global properties for this build -->
   <property name="src" location="src"/>
   <property name="build" location="build"/>
   <property name="dist" location="dist"/>

   <target name="init">
     <!-- Create the time stamp -->
     <tstamp/>
     <!-- Create the build directory structure used by compile -->
     <mkdir dir="${build}"/>
   </target>
   <target name="compile" depends="init"
       description="compile the source">
     <!-- Compile the Java code from ${src} into ${build} -->
     <javac srcdir="${src}" destdir="${build}"/>
   </target>
   <target name="dist" depends="compile"
       description="generate the distribution">
     <!-- Create the distribution directory -->
     <mkdir dir="${dist}/lib"/>
     <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
     <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
   </target>
   <target name="clean"
       description="clean up">
     <!-- Delete the ${build} and ${dist} directory trees -->
     <delete dir="${build}"/>
     <delete dir="${dist}"/>
   </target>
</project>

El archivo de compilación está escrito en XML y define algunos metadatos simples sobre la compilación, así como una lista de tareas (etiquetas en XML). (Ant usa la palabra objetivo para referirse a tareas y la palabra tarea para referirse a comandos). Cada tarea ejecuta una lista de posibles comandos definidos por Ant, incluida la creación y eliminación de directorios, la ejecución de javac y la creación de archivos JAR. Los complementos proporcionados por el usuario pueden ampliar este conjunto de comandos para cubrir cualquier tipo de lógica. Cada tarea también puede definir las tareas de las que depende a través de la propiedad de dependencia. Estas dependencias forman un gráfico acíclico, como se muestra en la siguiente figura.

  • Un archivo llamado build.xml se carga en el directorio actual y se analiza para crear la estructura del diagrama como se muestra.
  • Busque una tarea llamada dist proporcionada en la línea de comando y descubra que depende de una tarea llamada compilar.
  • Busque una tarea llamada compilar y descubra que depende de una tarea llamada init.
  • Buscando una tarea llamada init, no tiene dependencias.
  • Ejecuta los comandos definidos en la tarea de inicio.
  • Ejecuta los comandos definidos en la tarea de compilación (siempre que se hayan ejecutado todas las dependencias de la tarea correspondiente).
  • Ejecuta los comandos definidos en la tarea dist (dado que se han ejecutado todas las dependencias de la tarea correspondiente)
    Finalmente, el código que ejecuta Ant al ejecutar la tarea dist es equivalente al siguiente script de shell:
./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

Después de eliminar la sintaxis, un archivo de compilación y un script de compilación no son realmente tan diferentes. Sin embargo, al hacerlo, hemos ganado mucho. Podemos crear nuevos archivos de compilación en otros directorios y vincularlos entre sí. Podemos agregar fácilmente nuevas tareas basadas en tareas existentes en formas arbitrariamente complejas. Simplemente pasamos el nombre de una sola tarea a la herramienta de línea de comandos ant y descubrirá todo lo que debe ejecutarse.

Desventajas de los sistemas de compilación basados ​​en tareas

Difícil de ejecutar en paralelo

Las estaciones de trabajo de desarrollo modernas son muy poderosas, con varios núcleos que pueden ejecutar varios pasos de compilación en paralelo. Sin embargo, los sistemas basados ​​en tareas a menudo parecen tener dificultades para ejecutar tareas en paralelo, suponiendo que la tarea A depende de la tarea B y la tarea C. Dado que la tarea B y la tarea C son independientes entre sí, ¿es seguro ejecutar estas tareas al mismo tiempo para que el sistema pueda completar la tarea A más rápido? Tal vez no tengan acceso a ninguno de los mismos recursos. Pero también podría ser diferente: ambos podrían usar el mismo archivo para rastrear su estado y ejecutar ambos al mismo tiempo podría causar un conflicto. Por lo general, el sistema no tiene forma de saber esto, por lo que se arriesga a estos conflictos (lo que lleva a problemas de compilación raros pero difíciles de depurar) o debe restringir todo el proceso de compilación para que se ejecute en un solo subproceso en un solo proceso. Esto sería un gran desperdicio de potentes máquinas de desarrollo y elimina por completo la posibilidad de distribuir compilaciones en varias máquinas.

problema de construcción incremental

Un buen sistema de compilación permite a los ingenieros realizar compilaciones incrementales confiables sin tener que reconstruir todo el código base desde cero si se trata de un pequeño cambio. La compilación incremental es especialmente importante si el sistema de compilación es lento y no puede ejecutar los pasos de compilación en paralelo por las razones anteriores. En un sistema de compilación basado en tareas, muchas tareas simplemente toman un conjunto de archivos de origen y ejecutan el compilador para crear un conjunto de archivos binarios, por lo que no es necesario volver a ejecutarlos siempre que los archivos de origen subyacentes no hayan cambiado. . Pero sin información adicional, el sistema no puede estar seguro de que los archivos de origen no hayan cambiado y, para garantizar la corrección, el sistema generalmente tiene que volver a ejecutar cada tarea durante cada compilación.

Difícil de mantener y depurar scripts

Los scripts de compilación impuestos por los sistemas de compilación basados ​​en tareas suelen ser difíciles de trabajar. Si bien los scripts de compilación suelen ser menos estrictos, como los sistemas de compilación, son un lugar fácil para ocultar errores. A continuación se enumeran algunos ejemplos de errores comunes que pueden ocurrir al usar el sistema de compilación basado en tareas:

  • La Tarea A depende de la Tarea B para producir un determinado archivo como salida. Los propietarios de la tarea B no se dieron cuenta de que otras tareas dependían de ella, por lo que la cambiaron para generar resultados en otro lugar. Una vez que alguien intenta ejecutar la tarea A y descubre que la tarea falla, no hay forma de que el sistema lo detecte.
  • La tarea A depende de la tarea B, que a su vez depende de la tarea C, que produce ciertos archivos como salida requerida por la tarea A. El propietario de la tarea B decide que ya no necesita depender de la tarea C, lo que hace que la tarea A falle.
  • El desarrollador de la nueva tarea cambió la ubicación de la herramienta o el valor de una variable de entorno específica. Las tareas se ejecutan en sus máquinas, pero fallan cuando otros desarrolladores lo intentan.
  • Las tareas contienen componentes no deterministas, como descargar archivos de Internet o agregar marcas de tiempo a las compilaciones. Ahora, los usuarios pueden obtener resultados diferentes cada vez que ejecutan una compilación, lo que significa que los ingenieros a veces no pueden replicar y corregir fallas o fallas que ocurren en los sistemas de compilación automatizados.
  • Las tareas con múltiples dependencias pueden crear condiciones de carrera. Si la tarea A depende de la tarea B y la tarea C, y ambas tareas B y C modifican el mismo archivo, la tarea A obtendrá diferentes resultados dependiendo de cuál de las tareas B y C complete.

Sistema de construcción basado en artefactos

Se puede establecer una analogía entre los sistemas de construcción basados ​​en artefactos y la programación funcional. Los lenguajes de programación imperativos tradicionales como Java, C y Python especifican una lista de declaraciones para ejecutar una por una, de la misma manera que los sistemas de compilación basados ​​en tareas permiten a los programadores definir una secuencia de pasos para ejecutar. Por el contrario, los lenguajes de programación funcional como Haskell y ML están estructurados más como una serie de ecuaciones matemáticas. En un lenguaje funcional, el programador describe el cálculo a realizar, pero deja los detalles de cuándo y cómo se realiza el cálculo al compilador.

Esto corresponde a la idea de declarar un manifiesto en un sistema de compilación basado en artefactos y dejar que el sistema determine cómo ejecutar la compilación. Muchos problemas no se pueden expresar fácilmente usando programación funcional, pero pueden beneficiarse enormemente de ella, y el lenguaje a menudo puede paralelizar dichos programas en paralelo. Los problemas más sencillos de resolver con programación funcional son aquellos que simplemente transforman un dato en otro utilizando una serie de reglas o funciones. Eso es exactamente lo que son los sistemas de compilación: todo el sistema es realmente una función matemática que toma archivos fuente (y herramientas como compiladores) como entrada y produce archivos binarios como salida.
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.
El archivo de compilación (generalmente llamado BUILD) en Bazel se ve así:

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, el archivo BUILD define el objetivo, donde los dos objetivos son java_binary y java_library. Cada objetivo corresponde a un artefacto que un sistema puede crear: los objetivos binarios producen archivos binarios directamente ejecutables y los objetivos de biblioteca producen bibliotecas que pueden ser utilizadas por binarios u otras bibliotecas. Cada objetivo tiene:

  • nombre: cómo hacer referencia a este objetivo desde la línea de comandos y otros objetivos
  • srcs: archivos fuente compilados para crear artefactos contra el objetivo
  • deps: otros destinos que deben construirse antes de este destino y vincularse a este destino
    Las dependencias pueden estar en el mismo paquete (p. ej., la dependencia de MyBinary en: mylib) u otros paquetes en la misma jerarquía de origen (p. ej., la dependencia de mylib en //java/com /ejemplo/común).

Al igual que con el sistema de compilación basado en tareas, utiliza las herramientas de línea de comandos de Bazel para realizar compilaciones. Para construir el objetivo MyBinary, ejecute bazel build :MyBinary. Después de ingresar este comando por primera vez en un código base limpio, Bazel hace lo siguiente.

  • Analiza cada archivo BUILD en el espacio de trabajo para crear un gráfico de dependencias entre artefactos.
  • Se utiliza un gráfico para determinar las dependencias transitivas de MyBinary; es decir, cada objetivo del que depende MyBinary y cada objetivo del que dependen esos objetivos se procesan de forma recursiva.
  • Genere cada una de estas dependencias en orden. Bazel primero crea cada destino sin otras dependencias y realiza un seguimiento de las dependencias que aún deben construirse para cada destino. Una vez que se construyen todas las dependencias de un destino, Bazel comienza a construir ese destino. Este proceso continúa hasta que se hayan creado todas las dependencias transitivas de MyBinary.
  • Cree MyBinary para producir un binario ejecutable final que vincule todas las dependencias creadas en el paso 3.

Fundamentalmente, los eventos que suceden aquí parecen ser muy diferentes de lo que sucede cuando se usa el sistema de compilación basado en tareas. De hecho, el resultado final es el mismo binario, y el proceso de generarlo implica analizar una serie de pasos para encontrar dependencias entre ellos y luego ejecutar los pasos secuencialmente. Pero hay serias diferencias entre los dos. Dado que Bazel sabe en el paso 3 que solo se genera una biblioteca de Java por destino, sabe que solo necesita ejecutar el compilador de Java y no scripts arbitrarios definidos por el usuario y, por lo tanto, sabe que puede ejecutar estos pasos en paralelo. Esto mejora el rendimiento en un orden de magnitud en comparación con la construcción de un objetivo a la vez en una máquina de múltiples núcleos, solo porque el enfoque basado en artefactos deja al sistema de construcción a cargo de su propia estrategia de ejecución, lo que garantiza mejor el paralelismo.

Sin embargo, la ventaja va más allá del paralelismo. Este enfoque hace que sea obvio para nosotros cuando un desarrollador ingresa a bazel build :MyBinary por segunda vez sin realizar ningún cambio: Bazel saldrá en menos de un segundo con un mensaje de que el objetivo está actualizado. Esto es posible porque discutimos el paradigma de programación funcional anteriormente: Bazel sabe que cada objetivo es solo el resultado de ejecutar el compilador de Java, y también sabe que la salida del compilador de Java solo depende de su entrada, por lo que puede reutilizarse siempre que ya que la entrada no ha cambiado la salida. Este análisis funciona en todos los niveles; si MyBinary.java cambia, Bazel sabe que debe reconstruir MyBinary pero reutilizar mylib. Si los archivos de origen para //java/com/example/common cambian, Bazel sabrá reconstruir la biblioteca, mylib y MyBinary, pero reutilizará //java/com/example/myproduct/otherlib. Dado que Bazel conoce las propiedades de las herramientas que ejecuta en cada paso, solo puede reconstruir un conjunto mínimo de artefactos cada vez, mientras se asegura de no producir una compilación desactualizada.

Reestructurar el proceso de construcción a partir de artefactos en lugar de tareas es sutil y poderoso. Al reducir la flexibilidad proporcionada al programador, el sistema de compilación puede obtener un conocimiento detallado de lo que hace cada paso del proceso de compilación. Puede usar esta información para mejorar la eficiencia de la compilación paralelizando el proceso de compilación y reutilizando su salida. Pero este es solo el primer paso, estas compilaciones de procesamiento y reutilización paralelos forman la base de un sistema de compilación distribuido y altamente escalable.

por fin

Este artículo presenta principalmente el concepto de sistema de compilación, como el sistema de compilación basado en tareas y el sistema de compilación basado en artefactos. Para obtener más artículos, puede prestar atención a la cuenta oficial QStack.

Supongo que te gusta

Origin blog.csdn.net/QStack/article/details/128810208
Recomendado
Clasificación