Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.com

Conceptos básicos de make y los Makefile

Los que no tengan paciencia para leer, pueden ir al resumen final, aunque posiblemente no se enteren del todo.

El comando de linux make nos ayuda a compilar nuestros programas. Presenta muchas ventajas para programas grandes, en los que hay muchos ficheros fuente (muchos .c y muchos .h) repartidos por varios directorios. Principalmente aporta dos ventajas:

Dicho esto, vamos poco a poco con lo más básico. Hazte un programita de HolaMundo.c para ir probando. Puede ser como este

#include <stdio.h>
main()
{
    printf ("Hola mundo\n");
}

Lo compilaremos de la forma habitual.

$ cc HolaMundo.c -o HolaMundo

Lo probamos y ¡funciona!. Nada espectacular hasta ahora. Vuelve a compilarlo con el mismo comando. Se vuelve a compilar y sigue funcionando.

Veamos ahora con make. Si haces

$ make HolaMundo

Pues make te dirá que no hay nada que hacer. Primera diferencia con compilar a mano. Como el programa ya está hecho, make no hace nada. Esto, en un programa de muchas líneas de código que tarda varios minutos en compilar, es una gran ventaja.

Borra el ejecutable y vuelve a hacer make

$ rm HolaMundo
$ make HolaMundo

Esta vez si se ha compilado. Misteriosamente se ha escrito sólo el comando de compilación y el programa se ha compilado. Toca ahora el fichero fuente (edítalo, ponle un comentario en algún sitio y sálvalo, o símplemente, cámbiale la fecha con touch HolaMundo.c ). Al hacer make, se vuelve a compilar.

Esto quiere decir que make es más listo de lo que pensabamos. No sólo mira si el ejecutable ya está hecho, sino que compara además las fechas del fuente (el HolaMundo.c) y el ejecutable, compilando o no el programa en función de estas fechas.

make entiende además de extensiones y compiladores. Fíjate, al hacer make HolaMundo, el fichero HolaMundo.c se compila con el compilador cc. Si ahora le cambias la extensión y lo llamas HolaMundo.cpp, al hacer make se compilará con el compilador de c++, el g++. También entiende las extensiones de fortran y de ensamblador. Las versiones más modernas (al menos las de Solaris, el unix de Sun) incluso son capaces de compilar java. Si pones make HolaMundo.o, también sabrá generar adecuadamente el fichero objeto (el .o).

Todo esto se debe a que make tiene unas reglas implícitas que le indican cómo obtener un ejecutable. Estas reglas, al menos en Solaris, están escritas en un fichero make.rules que está perdido por los directorios del sistema operativo. En linux debe andar por /usr/share/lib/make/make.rules, aunque supongo que depende de la distribución.

Variables de entorno

Con esto ya podemos compilar cualquier programa simple. Vamos ahora a complicar un poco más el make. Creamos dos directorios. Uno de ellos lo llamamos PRINCIPAL y el otro FUNCION1. En PRINCIPAL ponemos nuestro HolaMundo.c. En FUNCION1 vamos a crear un fichero .h que llamaremos texto.h y su contenido es este:

#define TEXTO "Hola Mundo"
 

Nuestro HolaMundo.c lo cambiaremos un poco para que contenga lo siguiente:

#include <stdio.h>
#include <texto.h>
main()
{
    printf ("%s\n", TEXTO);
}

Ahora nos metemos en el directorio PRINCIPAL y hacemos, como antes make HolaMundo. Como es de esperar, obtenemos un error. No sabe encontrar el fichero texto.h, puesto que no está en los directorios por defecto de búsqueda de ficheros .h.

El comando make, además de saber hacer ciertos ejecutables a partir de sus fuentes, también es capaz de mirar ciertas variables de entorno para buscar ficheros de cabecera (.h), librerías, etc. Estas variables de entorno están definidas en el make.rules que mencionamos anteriormente.

Una variable bastante interesante es CFLAGS (CPPFLAGS para el compilador de C++). Esta variable puede contener las opciones que queramos que se pasen al compilador. Por ejemplo, si hacermos

$ CFLAGS=-g; export CFLAGS
$ make HolaMundo

veremos cómo al compilar el fichero HolaMundo se le pasa al compilador la opción -g (para poder meter luego el debugger).

Una de las opciones que se puede pasar al compilador es la opción -I, que nos permite poner paths de busqueda para ficheros de cabecera (.h). En nuestro ejemplo, y usando un path relativo, deberíamos poner algo así como -I../FUNCION1. Vamos a ello:

$ CFLAGS=-I../FUNCION1; export CFLAGS
$ make HolaMundo

Esta vez sí debería ir todo correctamente.

El fichero Makefile

Vamos a complicarle la vida un poco más al comando make. Pongamos ahora en el directorio FUNCION1 dos ficheros. Un funcion1.h y un funcion1.c. El contenido de estos ficheros sería:

funcion1.h
void escribeHolaMundo();

funcion1.c
#include <stdio.h>
void escribeHolaMundo()
{
   printf ("Hola Mundo\n");
}

El fichero texto.h podemos borrarlo porque ya no nos servirá más.

En cuanto al programa en el directorio PRINCIPAL, lo modificamos para que ponga esto:

#include <funcion1.h>
main()
{
    escribeHolaMundo ();
}

Desde el directorio PRINCIPAL ponemos nuestra variable CFLAGS como antes y hacemos el make.

$ CFLAGS=-I../FUNCION1; export CLFAGS
$ make HolaMundo

Obtenemos otra vez un error. make compila el fichero HolaMundo.c, no compila el fichero funcion1.c y obtenemos un error. Esto ya es demasiado para resolverlo con variables de entorno. En el momento que tenemos dos ficheros .c para construir un único ejecutable, necesitamos decirle a make cuáles son los ficheros que debe compilar.

Podemos hacer que make sea más listo escribiendo un fichero, uno de cuyos nombres por defecto es Makefile, y decirle en él qué cosas queremos que haga y cómo. En ese fichero pondremos cosas como

objetivo: dependencia1 dependencia2 ...
<tab>comando1
<tab>comando2
<tab>...
 

HolaMundo: HolaMundo.c ../FUNCION1/funcion1.c ../FUNCION1/funcion1.h
    cc -I../FUNCION1 HolaMundo.c ../FUNCION1/funcion1.c -o HolaMundo

Ahora, después de borrar el ejecutable, si hacemos make (a secas, sin parámetro), se volverá a compilar nuestro programa. Si a make no le ponemos parámetro, buscará un fichero Makefile y dentro de él hará el primer objetivo que encuentre. En nuestro caso, el único que hay es HolaMundo.

Bueno, una cosa que tengo que advertir es que esta NO es la forma correcta de hacer un Makefile, pero lo hacemos así para que quede más claro y no introducir demasiados conceptos de golpe. Con lo que debes quedarte es con la sintaxis (objetivo, dependencias y comando) y el cómo funciona. El qué cosas hay que poner y cómo lo iremos explicando poco a poco.

Fíjate que en el comando de compilación, el cc, hemos puesto una opción -I../FUNCION1 para que sea capaz de encontrar los ficheros .h. Si ponemos un comando de compilación, ya no valen las reglas implícitas que conoce make. Al poner un comando de compilación, nuestro ejecutable se generará con nuestra comando (regla explícita) y no se cogerá nada de otro sitio (ni siquiera la variable de entorno CFLAGS).

Mejorando el Makefile

Este incorrecto Makefile funciona correctamente. Si tocamos cualquier fichero (con touch o editándolo y salvando), se recompilara el programa. ¿Por qué es incorrecto entonces?. Es incorrecto porque si tocamos cualquier fichero, recompila absolutamente todo. Perdemos la gran ventaja de make para proyetos grandes, que consiste en que compila sólo aquellas partes que son necesarias. Lo ideal, por ejemplo, es que si tocamos funcion1.h, sólo recompile HolaMundo.c, que incluye funcion1.h, pero que no recompile funcion1.c, que no incluye para nada a funcion1.h. (otra cosa que que funcion1.c implementa una función cuyo prototipo está en funcion1.h y si tocamos dicho prototipo, debemos tocar también el código de funcion1.c)

Para conseguir este comportamiento ideal no queda más remedio que tener "guardados" los ficheros objeto (los .o) que se generan. En un proyecto grande, estos .o se suelen guardar en forma de librerías. Para nuestro ejemplo lo dejaremos con los .o sueltos.

Nuestro ejecutable HolaMundo dependería únicamente de los dos .o correspondientes a los .c que tenemos. En el Makefile, deberíamos poner

HolaMundo: HolaMundo.o ../FUNCION1/funcion1.o
    cc HolaMundo.o ../FUNCION1/funcion1.o -o HolaMundo

Con esto ya no necesitamos la opción -I../FUNCION1 ya que los ficheros fuente ya están compilados y no necesitan los .h para nada.

Aunque make sabe hacer los .o de una forma por defecto (busca el .c con el mismo nombre y lo compila con las opciones adecuadas), sigue sin saber encontrar los .h que estén en otros directorios. Podemos poner más reglas en nuestro Makefile para que sepa hacer correctamente los .o. Las pondremos detrás, ya que make hace el primer objetivo que encuentre en el fichero y queremos que este sea nuestro ejecutable. El Makefile quedaría:

HolaMundo: HolaMundo.o ../FUNCION1/funcion1.o
    cc HolaMundo.o ../FUNCION1/funcion1.o -o HolaMundo

../FUNCION1/funcion1.o: ../FUNCION1/funcion1.c
    cc -c ../FUNCION1/funcion1.c -o ../FUNCION1/funcion1.o

HolaMundo.o: HolaMundo.c ../FUNCION1/funcion1.h
    cc -c -I../FUNCION1 HolaMundo.c -o HolaMundo.o

¡ Vaya pequeño gran lio !. Cuando ejecutemos make, se tratará de hacer nuestro primer objetivo, HolaMundo. Como este depende de dos .o, se harán primero estos dos. Cada uno de ellos depende de los fuentes (.c y .h) correspondientes. Si los fuentes son más modernos que el .o o si no hay .o, se compilará. Si el .o es más moderno que los fuentes, no se hará nada. Una vez construidos los .o, se construirá HolaMundo en función de que las fechas de estos .o sean más modernas o no que el ejecutable HolaMundo. Veamoslo por pasos con un ejemplo concreto.

Supongamos que ya teníamos todo compilado una vez, es decir, tenemos todos los .o y el ejecutable creados. Editamos y salvamos funcion1.h y ejecutamos make. El comando más o menos daría los siguientes pasos:

Con esto vemos que sólo se ha compilado uno de los .c, mientras que el otro no ha hecho falta.

Seguimos mejorando cosas. Si recordamos de antes, make tiene unas reglas implícitas que le permiten construir el solito los .o. Sólo necesita saber dónde encontrar los ficheros de cabecera (.h). Además, para un .o únicamente hace falta un .c, así que debería bastar con la regla implícita y la variable de entorno CFLAGS. El Makefile quedaría:

HolaMundo: HolaMundo.o ../FUNCION1/funcion1.o
    cc HolaMundo.o ../FUNCION1/funcion1.o -o HolaMundo

../FUNCION1/funcion1.o: ../FUNCION1/funcion1.c

HolaMundo.o: HolaMundo.c ../FUNCION1/funcion1.h
 

y para que funcione correctamente, antes de hacer make, debemos poner la variable de entorno

$ CFLAGS=-I../FUNCION1; export CFLAGS
$ make

En el Makefile sólo hemos puesto las dependencias de los .o con los ficheros fuente. Esto es necesario al estar los fuentes por varios directorios y haber ficheros .h. La regla implícita para los .o únicamente hará depender al .o de un .c con el mismo nombre y que esté en el mismo directorio.  

Variables en el Makefile

¿Y si me olvido de la variable de entorno? o como suele pasar en un proyecto grande ¿y si tengo 32 directorios de ficheros .h?. Afortunadamente es posible definir estas variables (y cualquier otra que deseemos) dentro del mismo fichero Makefile. De hecho y aprovechando esta posibilidad, vamos a meter en una variable también los .o de HolaMundo, que si no tenemos que escribirlos dos veces (una en las dependencias de HolaMundo y otra en el comando de compilacion). El fichero Makefile, después de estos cambios, quedaría:

OBJETOS=HolaMundo.o ../FUNCION1/funcion1.o
CFLAGS=-I../FUNCION1

HolaMundo: $(OBJETOS)
    cc $(OBJETOS) -o HolaMundo

../FUNCION1/funcion1.o: ../FUNCION1/funcion1.c

HolaMundo.o: HolaMundo.c ../FUNCION1/funcion1.h

Como puedes ver, para asignar la variable se pone el nombre de la variable, un igual y lo que queramos que tenga. Si la línea es muy larga, se puede partir con la el caracter \

OBJETOS=HolaMundo.o \
    ../FUNCION1/funcion1.o

eso sí, asegurate que inmediatamente detrás de la \ está el retorno de carro, que no haya ningún espacio ni nada parecido detrás de la \ u obtendrás un montón de errores extraños.

Para usar el contenido de la variable, se pone $ y entre paréntesis el nombre de la variable.  

La utilidad makedepend

Nos queda el tema de las dependencias del .o ¿Qué pasa si mis .c incluyen .h que a su vez incluyen otros y estos a su vez otros? ¿Tengo que saber y mantener todas las dependencias a mano?.

Afortunadamente no es necesario. Unix nos proporciona mecanismos para hacerlo más fácil.

En Solaris, basta que el Makefile tenga una "directiva" que le diga que se encargue de ello. No nos hace falta poner las dependencias ni ocuparnos de nada. El Makefile, para Solaris, quedaría

# Esta es la directiva que le hace mantener automáticamente las dependencias
.KEEP.STATE:

OBJETOS=HolaMundo.o ../FUNCION1/funcion1.o
CFLAGS=-I../FUNCION1

HolaMundo: $(OBJETOS)
    cc $(OBJETOS) -o HolaMundo

Ya está. Al encontrar .KEEP.STATE, make almacenará en un fichero oculto todas las dependencias de los .o con los .h y sabrá en cada momento si es necesario o no recompilar los .o. Por cierto, cualquier línea que empiece con #, make la considera como un comentario.

En linux esta directiva no da error, pero tampoco hace lo que se espera de ella. Simplemente, parece que la ignora (si alguien sabe algo del tema, agradezco la información chuidiang@gmail.com) . En linux, la opción que he encontrado, es generar el Makefile sin los objetivos de los .o, es decir

OBJETOS=HolaMundo.o ../FUNCION1/funcion1.o
CFLAGS=-I../FUNCION1

HolaMundo: $(OBJETOS)
    cc $(OBJETOS) -o HolaMundo

Una vez hecho esto, se ejecuta un comando makedepend, al que se le deben pasar todos los .c de nuestro proyecto y las opciones -I necesarias para que encuentre los .h

$ makedepend -I../FUNCION1 HolaMundo.c ../FUNCION1/funcion1.c

    Este comando mirará todas las depencias de los .c con los .h y añadirá las líneas de objetivos .o al final del Makefile. Es decir, estas dos líneas

../FUNCION1/funcion1.o: ../FUNCION1/funcion1.c
HolaMundo.o: HolaMundo.c ../FUNCION1/funcion1.h

Como puedes ver, algo más incomodo que el .KEEP.STATE de Solaris, pero mejor que escribirlo todo a mano, especialmente si unos .h incluyen a otros que a su vez incluyen a otros.

Bueno, lo de makedepend sigue siendo un poco pesado.  Cuando lo llamemos, debemos poner un montón de cosas detrás, sobre todo si nuestro proyecto es muy grande. Es bastante habitual, aprovechando variables que tenemos en Makefile, poner un objetivo nuevo que se llame depend y cuyo comando sea makedepend. De esta forma, luego ejecutaremos make depend y se hará solo. El Makefile quedaría

OBJETOS=HolaMundo.o ../FUNCION1/funcion1.o
FUENTES=HolaMundo.c ../FUNCION1/funcion1.c
CFLAGS=-I../FUNCION1

HolaMundo: $(OBJETOS)
    cc $(OBJETOS) -o HolaMundo

depend:
    makedepend $(CFLAGS) $(FUENTES)

Hemos añadido una variable FUENTES con los fuentes .c de nuestro proyecto y hemos reaprovechado la variable CFLGAS para el makedepend. Ahora, con

$ make depend

se generarían dentro de Makefile todas las dependencias de los .o. Basta con hacer esta llamada una sola vez y servirá mientras no cambiemos los include de nuestros fuentes o creemos fuentes nuevos.

En makefile.tar tienes en formato tar los dos directorios (PRINCIPAL y FUNCION1), con los ficheros que hay dentro de ellos. Puedes descargarlo en un directorio de trabajo, quítale la dichosa extensión .txt y descomprimirlo con tar -xf makefile.tar.  

Un último apunte

En un proyecto grande, los .o se suelen guardar en librerias. Lo habitual es que en cada directorio de fuentes se haga un Makefile específico para construir una librería con los fuentes de ese directorio. El Makefile principal llamará a los Makefile de cada directorio (porque ese será el comando de compilación que nosotros pongamos) y luego construya el principal.

Es decir, si con funcion1.c hacemos una librería libfuncion1.a y tiene su propio Makefile en su propio directorio FUNCION1, el Makefile de PRINCIPAL podría ser algo así como

CFLAGS=-I../FUNCION1
LDFLAGS=-L../FUNCION1

HolaMundo : HolaMundo.o ../FUNCION1/libfuncion1.a
    cc -o HolaMundo HolaMundo.o $(LDFLAGS) -lfuncion1

../FUNCION1/libfuncion1.a:
    make -C ../FUNCION1

Veamos con detalle algunas cosas

LDFLAGS es una variable igual que CFLAGS, pero que le dice a las reglas implícitas qué opciones usar para el linkado. En este caso la estamos usando para la opción -L, que dice dónde hay librerías. No hay ninguna regla implicita en nuestro Makefile que lo vaya a usar, pero hemos llamado a la variable de la misma manera.

Para hacer la librería, llamamos a make con la opción -C. Esta opción le dice a make que debe cambiarse al directorio ../FUNCION1 antes de ejecutarse. Allí encontrará el Makefile para hacer la librería. En el Makefile de PRINCIPAL no ponemos dependencias para la librería. Estas dependencias estarán puestas en el Makefile de FUNCION1 y será este el que decida si hay que reconstruir o no la librería.  

Resumen

Hazte el directorio donde vaya tu programa principal un fichero de nombre Makefile. Este fichero debe contener:

CFLAGS=-I<directorio1> -I<directorio2> ...
OBJETOS=<objeto1> <objeto2> ...
FUENTES=<fuente1> <fuente2> ...

MiEjecutable: $(OBJETOS)
<tab>cc $(OBJETOS) -o MiEjecutable

depend:
<tab>makedepend $(CFLGAS) $(FUENTES)

 <directorio1>, <directorio2>, ... son directorios donde tengas ficheros .h de tu proyecto. No hace falta que pongas los del sistema (stdio.h y demás), que esos se ponen solos.

<objeto1>, <objeto2>, ... son los .o de tu proyecto, es decir, todos y cada uno de los .c, pero cambiándoles la extensión por .o. Estos objetos deben incluir el path donde están los .c. Por ejemplo, si tengo /users/chuidiang/proyecto/directorio1/fichero.c, uno de los objetos será /users/chuidiang/proyecto/directorio1/fichero.o

<fuente1>, <fuente2>, ... son los fuentes, igual que los <objeto1>, <objeto2>, pero terminados en .c en vez de .o y también con el path.     <tab> es un tabulador. Es importante que esté ahí, si no obtendremos errores.

Cada vez que en tu proyecto generes .h o .c nuevos o cambies los includes de los fuentes ya existentes, actualiza el fichero Makefile (FUENTES, OBJETOS y CFLAGS) y ejecuta una vez

$ make depend

Cuando quieras compilar, simplemente escribe

$ make

Por supuesto, ambos comandos en el directorio donde esté el fichero Makefile.

En makefile.tar tienes un ejemplo con dos directorios. Descargalo, quítale la dichosa extensión .txt, descomprímelo con tar -xf makefile.tar, vete al directorio PRINCIPAL y haz make depend y luego make. Puedes jugar con ello para ver cómo funciona o modificarlo para tus propios proyectos.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: