Asignatura: Entornos de programación

Herramientas de construcción (Build)

Utilidad ‘make’ y similares


Contenido

  1. Objetivo y funciones
  2. Compilación y montaje de programas
  3. Recompilación selectiva
  4. Recompilación automática mediante comparación de fechas
  5. Dependencias múltiples
    1. Ejemplo: diagrama modular
    2. Ejemplo: dependencias entre ficheros al compilar
    3. Ejemplo: regeneración simple
    4. Ejemplo: regeneración compleja
  6. Utilidad ‘make’
    1. Formato del ‘makefile’
    2. Ejemplo de ‘makefile’
    3. Cómo invocar ‘make’
    4. Variables
    5. Reglas implícitas
    6. Ejemplo de makefile con variables y reglas implícitas
    7. Objetivos ficticios (.PHONY)
    8. Operaciones frecuentes
    9. Otras aplicaciones
    10. Ejemplo: ‘Backup’/‘Mirror’ incremental
    11. Dependencias automáticas
    12. Herramienta makedepend
    13. Compilador gcc/g++
    14. Desventajas de ‘make’
  7. Otras herramientas de construcción

Objetivo y funciones

El objetivo general de las herramientas de construcción (Build o Make) es automatizar el proceso de generación y/o actualización de un conjunto de ficheros (objetivo) que se construyen a partir de otros (fuente). Y no sólo automatizarlo, sino hacerlo de manera eficiente.

La automatización del proceso de construcción se podría hacer simplemente con un guión fijo de órdenes. Esto resulta adecuado para construir el objetivo por primera vez, pero no es lo más eficiente para actualizar un objetivo ya construido tras algunos cambios en los ficheros fuente, ya que se reconstruirían siempre todos los ficheros cada vez que se invocase esta operación, cuando en muchos casos sólo algunos de los ficheros a reconstruir necesitan realmente ser actualizados.

Una forma sencilla de conseguir un buen grado de eficiencia es comparar la fecha de actualización de cada fichero a reconstruir con la de los ficheros a partir de los cuales hay que regenerarlo. Si el fichero objetivo es posterior a todos aquellos ficheros fuente de los que depende, entonces no necesitaría ser regenerado.

La aplicación típica de estas herramientas de construcción es la compilación y montaje de programas ejecutables a partir de los ficheros fuente correspondientes. Esto es especialmente importante en el caso de lenguajes tales como C y C++, cuyos compiladores no tienen una forma razonablemente segura de identificar todos los ficheros fuente necesarios para construir un programa ejecutable completo partiendo sólo del fichero fuente principal.

Compilación y montaje de programas

El proceso clásico de construcción de un programa ejecutable a partir de los ficheros fuente consiste en compilar cada fichero fuente por separado, para obtener el correspondiente fichero objeto, y luego combinar los ficheros objeto (junto con funciones tomadas de la librería estándar) para obtener el programa ejecutable. En el ejemplo de la figura se muestra la construcción de un programa ejecutable a partir de dos ficheros fuente en lenguaje C:

make1

Recompilación selectiva

Tras construir el programa por primera vez y conservar los ficheros objeto intermedios, sólo será necesarios recompilar los ficheros fuente que se hayan modificado desde la última construcción. En general será siempre necesario repetir el paso final de combinar los objetos para obtener el ejecutable. Las figuras siguientes muestran el proceso de reconstruir el ejemplo anterior tras modificar sólo uno u otro de los ficheros fuente, pero no los dos a la vez.

Recompilación automática mediante comparación de fechas

El proceso de recompilación selectiva puede automatizarse mediante una herramienta que compare las fechas de actualización de los ficheros fuente y las de los ficheros objeto y ejecutable. Sólo hay que regenerar los ficheros cuya fecha de actualización sea anterior a la de aquellos a partir de los cuales se reconstruye. En la figura se muestran ejemplos de la hora de actualización de cada fichero, y los cambios que podrían producirse al actualizar los que no están al día:

make4

Las acciones hay que realizarlas en un orden adecuado. No tiene sentido regenerar un fichero en particular a partir de otros que no están actualizados, sino que la regeneración de cada fichero debe hacerse después de haber actualizado a su vez, en caso necesario, cada uno de los ficheros a partir de los cuales hay que hacer su reconstrucción.

Dependencias múltiples

En casos muy sencillos como el del ejemplo anterior no hace falta ninguna herramienta especial que ayude a construir el programa. Se podrían invocar las operaciones manualmente. Pero en la mayoría de los casos reales, con un mayor número de ficheros fuente y dependencias complejas entre ellos, es necesario disponer de alguna herramienta de ayuda. A continuación se muestra otro ejemplo de programa modular algo más complejo que el anterior. Realmente sigue siendo muy sencillo, pero ya se empiezan a apreciar las dificultades para reconstruirlo de manera eficiente sin herramientas de ayuda.

Ejemplo: diagrama modular

El diagrama modular de este ejemplo es:

make5

Ejemplo: dependencias entre ficheros al compilar

Cada módulo (excepto quizá el programa principal) debe corresponder a dos ficheros fuente: uno de cabecera (.h) con la descripción de su interfaz, y otro (.cpp) con el código de implementación. Las dependencias de los ficheros objeto y ejecutable con los ficheros fuente individuales sería:

make6

La flechas con línea continua representan dependencias directas, y las de línea discontinua representan dependencias indirectas, debidas a que ciertos ficheros fuente importan (con #include) ciertos ficheros de cabecera.

Tanto las dependencias directas como las indirectas deben ser tenidas en cuenta para determinar si un determinado fichero objeto está al día o no.

Ejemplo: regeneración simple

Cuando cambia algún fichero fuente hay que reconstruir todo lo que depende de él, directa o indirectamente. Si cambia un fichero de implementación la reconstrucción exige normalmente poco trabajo:

make7

Ejemplo: regeneración compleja

Por el contrario, si cambia un fichero de cabecera, especialmente de los módulos de nivel inferior, entonces el trabajo de reconstrucción puede ser bastante grande:

make8

A pesar de lo sencillo de este ejemplo, ya se empieza a ver la dificultad de realizar manualmente las reconstrucciones, porque es fácil olvidar alguna de las dependencias entre ficheros, especialmente las indirectas.

Utilidad ‘make

Este herramienta, originada en UNIX, sirve para automatizar el proceso de regeneración selectiva de ciertos ficheros a partir de otros de los que depende:

La herramienta make opera de forma recursiva, partiendo del objetivo final. Para cada posible fichero a regenerar (fichero objetivo), primero se reconstruyen, si es necesario, los ficheros de los que depende de manera inmediata, y a continuación se comprueban las fechas de actualización (que pueden haber cambiado si se ha regenerado alguno de ellos) para ver si el objetivo está al día.

Formato del ‘makefile

Los makefile deben contener la información que necesita make para operar. En concreto:

En los casos sencillos, un makefile contiene una regla por cada fichero que haya que regenerar automáticamente. Cada regla empieza por una línea de texto que indica el nombre del fichero objetivo y los ficheros de los que depende. A continuación se indican las órdenes a ejecutar para reconstruir el objetivo.

El formato general del makefile será:

objetivo: dependencia dependencia ...
 accion
 accion
 . . .
objetivo: dependencia ...
 accion
 . . .

La primera línea de una regla es la línea de dependencias. Debe empezar con el nombre del fichero objetivo, seguido del carácter dos puntos (:) y la lista de ficheros de los que depende, separados por espacio en blanco.

Las siguientes líneas de la regla son las órdenes para regenerar el objetivo, y dejan cierto margen a la izquierda. Por razones históricas deben empezar necesariamente con una carácter de tabulación. No es suficiente dejar el margen en blanco sólo con espacios. El analizador léxico del makefile usa precisamente el carácter de tabulación para identificar las líneas de órdenes que forman parte de la regla.

Ejemplo de ‘makefile

El makefile correspondiente al ejemplo anterior (programa ajustar) podría ser el siguiente:

ajustar: ajustar.o parrafo.o linea.o palabra.o
    g++ -o ajustar ajustar.o parrafo.o linea.o palabra.o

palabra.o: palabra.cpp palabra.h
    g++ -c palabra.cpp -o palabra.o

linea.o: linea.cpp linea.h palabra.h
    g++ -c linea.cpp -o linea.o

parrafo.o: parrafo.cpp parrafo.h linea.h palabra.h
    g++ -c parrafo.cpp -o parrafo.o

Cómo invocar ‘make

La utilidad make se invoca mediante la orden:

make [ -f makefile ] [ opciones ] [ objetivos ]

Los parámetros son:

En los casos sencillos no hace falta ningún parámetro. La orden:

make

busca un fichero que se llame makefile o Makefile y lo utiliza para actualizar el objetivo de la primera regla que haya en dicho fichero. En el ejemplo de la sección anterior la primera regla corresponde al programa ejecutable, que es el objetivo global.

Variables

En un makefile se pueden utilizar variables de manera similar a los lenguajes de programación imperativos. Las variables permiten almacenar valores de texto. Las variables no necesitan ser declaradas. Se crean automáticamente cuando se les asigna valor por primera vez. La asignación de valor a una variable se hace mediante la sentencia:

variable = valor

Estas sentencias aparecen fuera de las reglas de construcción de objetivos. Las variables pueden usarse en cualquier parte del makefile (dependencias, acciones, o asignación de variables), usando la notación:

... $(variable) ...

Reglas implícitas

Son plantillas genérica de reglas para reconstruir determinados tipos de ficheros. La línea de dependencias de la regla hace referencias a los tipos de ficheros por la extensión del nombre. El formato habitual es:

%.ext1: %.ext2

Esta notación corresponde a que un fichero objetivo que tenga la extensión .ext1 depende de otro fichero con el mismo nombre y la extensión .ext2

Las líneas de acciones pueden contener códigos especiales (macros) para hacer referencia a determinados fragmentos de información que varían al aplicar la regla:

Las reglas se invocan con una línea de dependencias que se ajuste al patrón de dependencias. Sólo se escriben las dependencias, pero no las acciones, que son suministradas por la regla implícita. Lo que sí se puede hacer es indicar dependencias adicionales, además de las que se ajustan al patrón de dependencias de la regla implícita.

Ejemplo de makefile con variables y reglas implícitas

El makefile correspondiente al programa de ejemplo ajustar puede reescribirse utilizando variables y reglas implícitas de la siguiente manera:

OBJS = ajustar.o parrafo.o linea.o palabra.o

ajustar: $(OBJS)
   g++ -o ajustar $(OBJS)

%.o : %.cpp
   g++ -c $< -o $@

palabra.o: palabra.cpp palabra.h
linea.o: linea.cpp linea.h palabra.h
parrafo.o: parrafo.cpp parrafo.h linea.h palabra.h
ajustar.o: ajustar.cpp parrafo.h linea.h palabra.h

Comparando esta versión con la que se había dado inicialmente se puede observar que:

Objetivos ficticios (.PHONY)

Los objetivos reales de make son ficheros a generar o regenerar. A veces resulta interesante especificar objetivos que no son realmente ficheros, sino nombres que se les dan a cierta operaciones. Por ejemplo, la siguiente regla serviría para eliminar ficheros temporales (ficheros objeto) producidos durante la compilación.

clean:
   rm *.o ...

La acción puede ser invocada mediante:

make clean

y funcionará correctamente mientras no exista un fichero o directorio real que se llame clean. Si se quiere que la acción se ejecute aunque exista un fichero con ese nombre bastará declarar el objetivo como ficticio, mediante:

.PHONY : clean
clean:
   rm *.o ...

Operaciones frecuentes

Los objetivos ficticios permiten usar make como ejecutor o coordinador de operaciones interesantes sobre el proyecto. En determinados ámbitos (organización, empresa, ...) se emplean por convenio nombres fijos para dichas operaciones. Por ejemplo, en los proyecto de software libre GNU de la FSF (Free Software Foundation) y en otras muchas aplicaciones Unix/Linux encontraremos habitualmente reglas .PHONY como las siguientes:

all:       <--- suele ser el primer objetivo
   compilar y montar todos los ejecutables del proyecto
check:
   ejecutar serie de pruebas
clean:
   eliminar ficheros intermedios de la compilación
install:
   instalar la aplicación ya compilada en directorios estándar
dist:
   generar archivos de distribución de la aplicación
distclean:
   eliminar los archivos de distribución
etc...

Estas operaciones se podrían haber programado como guiones de órdenes (scripts) independientes. La ventaja de hacerlo como reglas del makefile es que de esta manera se pueden reutilizar listas de ficheros, y tener agrupadas órdenes relacionadas entre sí en un mismo documento, lo cual facilita su mantenimiento

Otras aplicaciones

Ejemplo: ‘Backup’/‘Mirror’ incremental

Este ejemplo es poco realista, ya que existen herramientas especializadas para realizar el proceso, pero servirá para ilustrar algunas de las posibilidades de la herramienta make. Se trata de mantener actualizada una copia completa de los ficheros de un directorio (en un subdirectorio BACKUP).

Dependencias automáticas

La preparación manual del makefile es laboriosa y propensa a errores. Es fácil olvidarse de algunas dependencias, sobre todo las indirectas. Por esta razón resulta interesante disponer de herramientas de ayuda que faciliten en todo o en parte la creación del makefile para un proyecto dado.

En concreto, existen herramientas capaces de analizar los ficheros fuente y detectar las dependencias entre ellos y los ficheros objeto que se producirán al compilarlos. Ejemplos de posible alternativas:

Herramienta makedepend

Una de las primeras herramientas para análisis de dependencias fue makedepend para el lenguaje C. Esta utilidad:

Compilador gcc/g++

La herramienta makedepend ha quedado algo anticuada. Por ejemplo, los compiladores gcc/g++ para los lenguajes C/C++ tienen una capacidad equivalente a makedepend para generar automáticamente las líneas de dependencias. Para ello se pueden invocar de la siguiente manera:

gcc -MM ficheros-fuente ...  (e igual para g++)

Las líneas de dependencias aparecen escritas por la salida estándar del compilador, que puede ser redirigida para añadirla al makefile, si es apropiado.

Como ejemplo, se muestra aquí su aplicación al programa ajustar mencionado anteriormente:

...> g++ -MM *.cpp
ajustar.o: ajustar.cpp palabra.h parrafo.h linea.h
linea.o: linea.cpp linea.h palabra.h
palabra.o: palabra.cpp palabra.h
parrafo.o: parrafo.cpp parrafo.h palabra.h linea.h

Desventajas de ‘make

La herramienta make ha demostrado ser muy útil para automatizar las tareas de reconstrucción o regeneración de ficheros. Sin embargo tiene ciertos inconvenientes y limitaciones que conviene conocer. Entre ellos:

Otras herramientas de construcción

Hay muchas herramientas similares a ‘make’, algunas muy similares y otras más elaboradas. En bastantes casos tratan de subsanar alguna de las deficiencias mencionadas. Entre otras se pueden citar las siguientes: