Los Makefiles son archivos de script para el programa Make, y sirven para realizar acciones sobre archivos cuando se cumplan unas dependencias. Pueden usarse para muchas tareas, aunque la mas comúnmente usada es la actualización de programas ejecutables de un proyecto e incluso de su documentación asociada cuando se producen cambios en el código.
El funcionamiento de
Make se basa en un archivo llamado Makefile (si deseamos usar otro nombre tendremos que invocar a make después con "make -f nombre_makefile") en el que se disponen las reglas de dependencia entre los archivos y los objetivos(targets) del proyecto.
Supongamos que tenemos un proyecto en C que se compone de un archivo
main.c en el que tenemos la función principal del programa, y luego tenemos un archivo
funciones.c en el que tenemos las funciones que se llaman desde main. Supongamos también que queremos q nuestro programa se llame prueba (si estuvieramos en windows deberia ser prueba.exe, pero en UNIX no es necesario la extensión).
El formato del archivo es:
objetivo: dependencia1 dependencia2 .... dependenciaN
órdenes a ejecutar para la generación de "objetivo"
Para nuestro caso, el objetivo es
prueba y sus dependencias son
main.c y
funciones.c, por lo tanto crearemos un archivo Makefile que contenga:
prueba: main.o funciones.o
gcc main.o funciones.o -o prueba
Lo guardamos y ejecutamos
make. Como resultado, se creará el archivo
prueba y además veremos que se ha llamado al compilador de c para generar
main.o y funciones.o a partir de sus homólogos .c. Esto es debido a que el programa Make tiene cierta inteligencia y ya sabe como generar código objeto a partir de código fuente en C (también es capaz de hacer cosas parecidas para otros lenguajes). Podemos indicar nosotros todos los pasos a costa de que el Makefile sea mas complicado, pero eso si mas claro. El Makefile con todos los pasos sería:
prueba: main.o funciones.o
gcc main.o funciones.o -o prueba
main.o: main.c
gcc -c main.c
funciones.o: funciones.c
gcc -c funciones.c
En los makefiles podemos usar variables, por ejemplo para almacenar todas las dependencias, se suelen poner en mayúsculas para distinguirlas bien:
OBJS:= main.o funciones.o
prueba: $(OBJS)
gcc $(OBJS) -o prueba
El funcionamiento es el siguiente:
Make comprueba la lista de dependencias para
prueba, comprueba si estas dependencias tienen a su vez dependencias (por ejemplo cuando hemos creado el Makefile poniendo que
prueba dependía de
main.o y
funciones.o y estos a su vez dependían de
main.c y
funciones.c). Si alguna de las dependencias se incumple (entendiendo incumplir como que los archivos de los que depende un objetivo no existen, o su fecha de modificación es posterior a la del objetivo) se ejecutan las órdenes para dicho target.
Para ilustrarlo con nuestro ejemplo, supongamos que tenemos nuestros archivos fuente y nuestro Makefile, y que aún no hemos generado el ejecutable prueba, entonces, al ejecutar make, en primer lugar se determina si las dependencias de prueba existen y están actualizadas (comprueba que
main.o y funciones.o existen y su fecha de modificación es anterior o igual que la de prueba) , en este caso los archivos no existen. A continuación se comprueban las dependencias de
main.o (que es
main.c) como
main.o no existe, se ejecutan las ordenes para crearlo, esto es
gcc -c main.c y se obtiene
main.o, y lo mismo con
funciones.o. Una vez cumplidas las dependencias de "2º nivel" se vuelve sobre las de "1er nivel" y dado que
prueba no existe, se ejcuta la orden para crearlo a partir de
main.o y funciones.o.
Imaginemos ahora que modificamos funciones.c porque había un error en el funcionamiento de una de las funciones. Naturalmente, queremos actualizar el ejecutable
prueba para que incluya el cambio, pues bien aqui viene la potencia de los makefiles.
ejecutamos make y lo que ocurre es lo siguiente:
- Se intentan comprobar las dependencias de 1er nivel(prueba), pero como hay dependencias de 2º nivel se comprueban en primer lugar éstas últimas.
- Se comprueba las dependencias de 2º nivel y se determina que funciones.o es anterior a funciones.c, por lo que es necesario actualizar.
- Se ejecuta la orden para la dependencia de funciones.o
- Si no hay mas dependencias de 2º nivel, se pasa a las de 1er nivel.
- Se determina que prueba es anterior a una de sus dependencias (funcinoes.o) por lo que es necesario actualizar.
- Se ejecuta la orden para las dependencias de prueba, generándose de nuevo el ejecutable.
Ya tendríamos actualizado el programa. Podemos comprobar como únicamente se ha compilado la parte que ha cambiado respecto a la versión anterior. Si esto lo extrapolamos a un proyecto con muchos archivos, y con dependencias de varios niveles, la complejidad aumenta pero el funcionamiento es el mismo.
Continuaremos con cuestiones mas avanzadas sobre los makefiles en próximas entradas.