Revista Informática

Cómo crear nuestro propio Makefile

Publicado el 15 octubre 2016 por Drassill
Si bien este blog no está especialmente orientado al área del desarrollo, existen varios conceptos que son convenientes conocer, ya sean en el área de sistemas o de desarrollo... El conocimiento siempre es útil, y si bien es imposible saber todo, hay cosas que son recomendables conocer, aunque sea de oídas o de pasada. Uno de esos conceptos que, en mi opinión, es útil tenerlo presente, es la creación de nuestro propio Makefile; tanto para crear uno en el futuro como para entender su funcionamiento.
tux_make
Para empezar es importante conocer la utilidad de este fichero tan peculiar, para lo cual lo mejor que podemos hacer es ponernos en una situación hipotética. Generalmente, cuando nos bajamos un paquete comprimido que queremos instalar en Linux, lo que hacemos es descomprimirlo, entrar en él y ejecutar los comandos make y make install... Esto no es debido a que el comando make instalado en el ordenador haga "magia", sino debido a que dicho comando coge como referencia el fichero Makefile presente en la carpeta en cuestión, que hace que un proceso largo y complicado, sea realmente sencillo de realizar, pero... ¿Qué hace dicho fichero? ¿Qué pasaría si no tuviesemos dicho fichero? El Makefile realiza las tareas de compilación que normalmente nosotros deberíamos ejecutar a mano, tareas tediosas que habría que realizar una por una. Además la carencia de un Makefile también hace que en caso de que se compartiese el código con otra persona, ésta tendría que compilar todo fichero por fichero; tareas muy incomoda para cualquiera.
La mejor forma de ver esto es con un sencillo ejemplo; imaginemos que tenemos estos tres trozos de código:
Un fichero llamado test.h:
  1. #ifndef H_TEST
  2. #define H_TEST
  3. void Test(void);
  4. #endif

Un fichero llamado test.c:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. void Test(void)
  4. {
  5.     printf ("Hello World\n");
  6. }

Y por último un fichero llamado main.c:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include "test.h"
  4. int main()
  5. {
  6. Test();
  7. return 0;
  8. }

Estos fichero están completamente separados y son en estos momentos inservibles, pues no puede ser ejecutado el código en estos momentos; es necesario que lo compilemos para obtener de ellos un binario (lo que en muchos casos es considerado como un programa) que ejecute dicho código. Este proceso se realizaría de la siguiente forma si lo hiciésemos a mano:
  1. gcc -o test.o -c test.c
  2. gcc -o main.o -c main.c
  3. gcc -o test test.o main.o

Luego únicamente tendríamos que ejecutar el fichero como un programa normal y corriente:
  1. ./test
  2. Hello World

Este proceso en sí ha sido muy sencillo, pues únicamente hemos tenido que tratar 3 ficheros... Pero imaginemos que son 20, o 100, o 1000... Y no solo eso, sino que además queremos que ese código lo puedan usar más personas... Eso no solo no es práctico, sino que es una locura y ahí es donde ganan una enorme importancia los Makefile; ahora bien... ¿Cómo podemos crear uno? Por ejemplo: ¿Cómo podemos crear un Makefile basado en el caso anterior? El Makefile en cuestión tomaría el siguiente aspecto (Es muy importante que la líneas que comienzan por gcc empiecen con una tabulación):
  1. test: test.o main.o
  2.         gcc -o test test.o main.o
  3. test.o: test.c
  4.         gcc -o test.o -c test.c
  5. main.o: main.c test.h
  6.         gcc -o main.o -c main.c

¿Éste pequeño apartado que que significa? Analicemos las líneas para tener una clara percepción de lo que representan:
  • Para obtener el binario test necesitamos que existan los ficheros test.o y main.o; en caso de ser así se realizaría la compilación mediante comando: "gcc -o test test.o main.o".
  • Para obtener el fichero test.o es necesario que exista el fichero test.c; en cuyo caso se ejecutaría el comando: "gcc -o test.o -c test.c".
  • Para obtener el fichero main.o, es necesario que existan tanto main.c como test.h; al cumplirse dichas condiciones ejecutaría el comando: "gcc -o main.o -c main.c".

Este Makefile que hemos creado es perfectamente funcional y si tuviésemos un directorio con los tres ficheros de antes y el fichero Makefile que hemos creado, con simplemente ejecutar el comando make, tendríamos nuestro binario... La cuestión está en que el este fichero es un poco simple, con lo que lo ideal sería entender un fichero más complejo y real, si bien con lo que hemos visto ya entenderíamos su esencia... Generalmente un makefile incluye dos "reglas" especiales llamadas all y clean que son ejecutadas cuando escribimos "make clean" o "make all"... Además los ficheros de cierta envergadura suelen usar variables parecidas a las usadas en bash, con lo que vamos a potenciar el anterior ejemplo para que, por un lado tenga dos nuevas reglas, y por otro lado use algunas variables.
  1. CC=gcc
  2. EXEC:test
  3. all: ${EXEC}
  4. ${EXEC}: test.o main.o
  5.         ${CC} -o test test.o main.o
  6. test.o: test.c
  7.         ${CC} -o test.o -c test.c
  8. main.o: main.c test.h
  9.         ${CC} -o main.o -c main.c
  10. clean:
  11.         rm -rf *.o

Las variables creadas son simplemente dos: Una para elegir el comando que se usa para la compilación y que en caso de realizar un cambio, se haga de forma global (gcc) y otro para elegir el nombre que adoptará el binario... Además si os fijais hay dos nuevas "reglas", tal y como he comentado antes que añadiríamos cuya función sería:
  • all: Ejecuta todo lo necesario para compilar el proyecto; generalmente no es necesario debido a que make ya lo hace, pero a veces conviene usarlo para estar seguros.
  • clean: Limpia todos los ficheros .o generados durante el proceso de compilación.

Con esto ya tendríamos un Makefile mucho más completo y realista, pudiendo ser capaces de, no solo crear un Makefile, sino sobre todo entenderlo para así cuando se nos presente uno cuyo comportamiento no nos convenzca, podamos comprender y modificar su comportamiento.
Espero que os haya resultado útil.
Saludos.

Volver a la Portada de Logo Paperblog