Sistemas de compilación basados en tareas

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

En esta página se abarcan los sistemas de compilación basados en tareas, su funcionamiento y algunas de las complicaciones que pueden ocurrir con los sistemas basados en tareas. Después de las secuencias de comandos de shell, los sistemas de compilación basados en tareas son la próxima evolución lógica de la compilación.

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

En un sistema de compilación basado en tareas, la unidad fundamental de trabajo es la tarea. Cada tarea es una secuencia de comandos que puede ejecutar cualquier tipo de lógica, y las tareas especifican otras las tareas como dependencias que deben ejecutarse antes que ellas. La mayoría de los principales sistemas de compilación en uso en la actualidad, como Ant, Maven, Gradle, Grunt y Rake, se basan en tareas. En lugar de secuencias de comandos de shell, la mayoría de los sistemas de compilación modernos requieren que los ingenieros creen archivos de compilación que describen cómo realizar la compilación.

Toma este ejemplo del Manual de Ant:

<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. junto con una lista de tareas (las etiquetas <target> del XML). (Ant usa la palabra objetivo para representar una tarea y usa la palabra tarea para referirse a comandos). Cada tarea ejecuta una lista de posibles comandos definidos por Ant, que incluyen crear y borrar directorios, ejecutar javac y crear un archivo JAR. Este conjunto de comandos puede extenderse complementos para cubrir cualquier tipo de lógica. Cada tarea también puede definir las tareas que de las que depende a través del atributo de dependencias. Estas dependencias forman un grafo acíclico como se ve en la Figura 1.

Gráfico de acrílico que muestra dependencias

Figura 1: Un grafo acíclico que muestra dependencias

Los usuarios realizan compilaciones proporcionando tareas a la herramienta de línea de comandos de Ant. Por ejemplo: Cuando un usuario escribe ant dist, Ant sigue estos pasos:

  1. Carga un archivo llamado build.xml en el directorio actual y lo analiza para crear la estructura del grafo que se muestra en la Figura 1.
  2. Busca la tarea dist que se proporcionó en la línea de comandos. descubre que depende de la tarea llamada compile.
  3. Busca la tarea llamada compile y descubre que depende de la tarea llamada init.
  4. Busca la tarea llamada init y descubre que no tiene dependencias.
  5. Ejecuta los comandos definidos en la tarea init.
  6. Ejecuta los comandos definidos en la tarea compile dado que todo eso se hayan ejecutado las dependencias de la tarea.
  7. Ejecuta los comandos definidos en la tarea dist dado que todo eso se hayan ejecutado las dependencias de la tarea.

Al final, el código que ejecuta Ant cuando ejecuta la tarea dist es equivalente a la siguiente secuencia de comandos de shell:

./createTimestamp.sh
mkdir build/
javac src/* -d build/
mkdir -p dist/lib/
jar cf dist/lib/MyProject-$(date --iso-8601).jar build/*

Cuando se quita la sintaxis, el archivo de compilación y la secuencia de comandos de compilación no son muy diferentes. Pero ya ganamos mucho por hacer esto. Podemos crear archivos de compilación nuevos en otros directorios y vincularlos. Podemos agregar tareas nuevas que dependan de las tareas existentes de formas arbitrarias y complejas. Mié solo debes pasar el nombre de una tarea a la herramienta de línea de comandos de ant, y este determina todo lo que se debe ejecutar.

Ant es una pieza de software antigua, que se lanzó originalmente en el año 2000. Otras herramientas como Maven y Gradle mejoraron en Ant en los últimos años y, esencialmente, lo reemplazó con funciones como la administración automática de las dependencias y una sintaxis más limpia sin XML. Pero la naturaleza de estos modelos siguen siendo los mismos: permiten a los ingenieros escribir secuencias de comandos de compilación en un basadas en principios y modulares como tareas, y brindan herramientas para ejecutarlas y la administración de dependencias entre ellas.

El lado oscuro de los sistemas de compilación basados en tareas

Debido a que estas herramientas permiten esencialmente que los ingenieros definan cualquier script como una tarea, son muy potentes y te permiten hacer prácticamente todo lo que te imagines con ellos. Pero esa potencia trae desventajas, y los sistemas de compilación basados en tareas se vuelve difícil trabajar con ellas a medida que sus secuencias de comandos de compilación se vuelven más complejas. El problema con estos sistemas es que, en realidad, terminan ofreciendo demasiada potencia a no hay suficiente energía para el sistema. Debido a que el sistema no tiene idea lo que hacen las secuencias de comandos, el rendimiento se ve afectado, ya que debe ser muy conservador. en cómo programa y ejecuta los pasos de la compilación. Y no hay forma de que el sistema para confirmar que cada secuencia de comandos está haciendo lo que debería, por lo que las secuencias de comandos tienden a crecer complejidad y termina siendo otra cosa que necesita depuración.

Dificultad de paralelizar pasos de compilación

Las estaciones de trabajo de desarrollo modernas son potentes y tienen varios núcleos capaz de ejecutar varios pasos de compilación en paralelo. Pero los sistemas basados en tareas a menudo no pueden paralelizar la ejecución de tareas, incluso cuando parece que deberían que no puedas hacerlo. Supongamos que la tarea A depende de las tareas B y C. Debido a que las tareas B y C no dependen unos de otros, ¿es seguro ejecutarlas al mismo tiempo para para que el sistema llegue más rápido a la tarea A? Tal vez, si no tocan de los mismos recursos. Pero quizás no. Quizás ambos usen el mismo archivo para rastrear sus estados y ejecutarlos al mismo tiempo causa un conflicto. No hay en forma general para que el sistema lo sepa, por lo que tiene que arriesgarse (lo que genera problemas de compilación poco comunes pero muy difíciles de depurar), o debe restringir toda la compilación para que se ejecute en un solo subproceso, Esto puede ser un gran desperdicio de una potente máquina de desarrollador, descarta la posibilidad de distribuir la compilación entre varias máquinas.

Dificultad para realizar compilaciones incrementales

Un buen sistema de compilación permite a los ingenieros realizar compilaciones incrementales confiables, como para que un pequeño cambio no requiera que se reconstruya toda la base de código desde cero. Esto es muy importante si el sistema de compilación es lento y no puede paralelizar los pasos de compilación por los motivos antes mencionados. Pero, lamentablemente, los sistemas de compilación basados en tareas también tienen dificultades aquí. Como las tareas pueden hacer cualquier cosa, no hay forma de comprobar si ya se hicieron. Muchas tareas solo tienes que tomar un conjunto de archivos de origen y ejecutar un compilador para crear un conjunto de binaries; por lo que no es necesario volver a ejecutarlos si los archivos no cambiaron. Pero, sin información adicional, el sistema no puede decirlo seguramente. Puede que la tarea descargue un archivo que podría haber cambiado o quizás escribe una marca de tiempo que podría ser diferente en cada ejecución. Para garantizar el sistema, por lo general, debe volver a ejecutar cada tarea durante cada compilación. Algunos los sistemas de compilación intentan habilitar compilaciones incrementales permitiendo que los ingenieros especifiquen la condiciones bajo las que una tarea debe volver a ejecutarse. A veces esto es posible, pero suele ser un problema mucho más complicado de lo que parece. Por ejemplo, en los idiomas como C++, que permiten que otros archivos incluyan archivos, Es imposible determinar el conjunto completo de archivos que debe observarse para detectar cambios. sin analizar las fuentes de entrada. Los ingenieros suelen terminar tomando atajos y estos atajos pueden dar lugar a problemas poco comunes y frustrantes en los que el resultado de una tarea se o reutilizar, incluso cuando no debería ser así. Cuando esto sucede con frecuencia, los ingenieros el hábito de realizar ejecuciones limpias antes de cada compilación para obtener un estado actualizado contrarrestar por completo el propósito de contar con una compilación incremental en los primeros Averiguar cuándo se debe volver a ejecutar una tarea es sorprendentemente sutil y es un trabajo mejor manejado por las máquinas que por las personas.

Dificultades de mantener y depurar secuencias de comandos

Por último, las secuencias de comandos de compilación impuestas por los sistemas de compilación basados en tareas suelen ser difíciles de trabajar. Aunque a menudo reciben menos escrutinio, crea secuencias de comandos son código como el sistema que se compila y son lugares fáciles de ocultar para los errores. Estos son algunos ejemplos de errores que son muy comunes cuando se trabaja con un sistema de compilación basado en tareas:

  • La tarea A depende de la tarea B para producir un archivo en particular como resultado. El propietario de la tarea B no se da cuenta de que otras tareas dependen de ella, así que la cambian a producir resultados en una ubicación diferente. No se podrá detectar esto hasta que alguien intenta ejecutar la tarea A y descubre que falla.
  • La tarea A depende de la tarea B, que depende de la tarea C, que está produciendo una un archivo particular como resultado que se necesita para la tarea A. Propietario de la tarea B decide que ya no necesita depender de la tarea C, lo que causa que la tarea A fallar, aunque a la tarea B no le importa en absoluto la tarea C.
  • El desarrollador de una nueva tarea accidentalmente hace una suposición sobre la máquina que ejecuta la tarea, como la ubicación de una herramienta o el valor de variables de entorno particulares. La tarea funciona en su máquina, pero falla cada vez que otro desarrollador lo prueba.
  • Una tarea contiene un componente no determinista, como la descarga de un archivo de Internet o agregar una marca de tiempo a una compilación. Ahora, la gente tiene resultados potencialmente diferentes cada vez que ejecutan la compilación, lo que significa que los ingenieros no siempre podrán reproducir y reparar las fallas de los demás o las fallas que ocurren en un sistema de compilación automatizado.
  • Las tareas con varias dependencias pueden crear condiciones de carrera. Si la tarea A depende de las tareas B y C, y las tareas B y C modifican la misma archivo, la tarea A obtiene un resultado diferente según cuál de las tareas B y C termina primero.

No existe una manera general de solucionar estos problemas de rendimiento, precisión de mantenimiento en el framework basado en tareas que se describe aquí. Hasta el momento ya que los ingenieros pueden escribir código arbitrario que se ejecute durante la compilación, el sistema no puede tener información suficiente para ejecutar compilaciones rápidamente correctamente. Para resolver el problema, necesitamos quitarle algo de poder de las manos a los ingenieros, de nuevo en manos del sistema y rol del sistema no como ejecución de tareas, sino como producción de artefactos.

Este enfoque condujo a la creación de sistemas de compilación basados en artefactos, como Blaze. y Bazel.