1.4.0a Utilidad make (construir proyectos grandes)
"The art of constructin makefiles is arcane; fortunately, Microsoft development environment constructs these automatically for you. Other development environments provide similar tools and facilities, either as explicit control files or as part of a "proyect description file"". Brent E. Rector y Joseph M. Newcomer. "Win32 Programming". Addison-Wesley. Abril 2005.
§1 Presentación del problema:
En la práctica los programas C++ no son tan simples que ocupen un solo fichero fuente, lo normal es que ocupen varios, y los proyectos grandes pueden contener decenas de fuentes, a los que hay que sumar las librerías estándar o específicas; los ficheros de recursos ("resource files"); los de cabecera, etc. Estos proyectos pueden dar lugar a múltiples ficheros ejecutables y a librerías de distinto tipo. En estas circunstancias, si un proyecto se compone de una serie de n fuentes, digamos por ejemplo: p1.c; p2.c; p3.c; ... pn.c; para construir la aplicación es necesario compilar individualmente (cada uno quizás con sus propias opciones de compilación), con lo que se obtienen n objetos: p1.obj; p2.obj; p3.obj; ... pn.obj, que posteriormente son enviados al enlazador para construir los ejecutables y/o librerías.
Cada vez que se realiza una modificación en un fuente,
o se cambia la versión de una librería, un fichero de recursos, un fichero de
cabecera, etc. es necesario recompilar los nuevos objetos y volver a enlazar. En proyectos grandes es un gran consumo de tiempo rehacer toda la aplicación
cada vez, incluyendo los módulos que no han sufrido modificación (como hemos hecho en el ejemplo anterior
1.4.0),
por lo que se intenta que los cambios, recompilaciones, enlazados, etc. solo
afecten a los módulos estrictamente necesarios. Además, pueden existir
decenas de comandos involucrados en la operación, cada uno con multitud de
argumentos. Todo lo cual es difícil de mantener y memorizar.
§2 Make
Con objeto de facilitar el trabajo de controlar qué objetos, están desfasados respecto a otros módulos, y qué partes de la aplicación deben ser reconstruidas, bien porque son de fecha anterior a la última modificación del fuente, o porque sencillamente no existen (fuentes nuevos que no han sido compilados ninguna vez), los entornos de programación C++ disponen de una utilidad especial denominada Make (en Borland C++ es make.exe; en MS Visual C++ es nmake.exe).
Make es una utilidad que simplifica los ciclos de compilación y enlazado; ayuda a construir rápidamente grandes proyectos porque permite compilar solo aquellos fuentes que han sufrido modificación desde la última compilación y reconstruir los ejecutables que dependen de los objetos resultantes. Además, permite establecerse una serie de reglas que especifican como debe procederse con las circunstancias especiales de cada caso. Pero make es algo más que una utilidad ligada a la compilación y enlazado, se trata de una herramienta genérica para ejecutar comandos en base a ciertas dependencias, y aunque generalmente está ligado a las operaciones de compilar y enlazar ficheros, puede especificarse prácticamente cualquier comando aceptado por el Sistema Operativo.
Nota: en cierta forma, Make es una utilidad un
tanto extraña, o cuando menos "peculiar". Por sus
características no se parece a nada que hayamos visto antes; de forma que
cuesta un poco "meterse" en su filosofía de trabajo. A pesar
de ser cierto lo dicho -que es una herramienta genérica-, en realidad su
diseño está orientado a resolver un problema muy concreto (descrito
brevemente en el punto anterior),
lo que es sin duda el origen de su singularidad. Como suele
ocurrir con este tipo de aplicaciones de diseño muy específico, hace bien su
trabajo (para propósitos generales existen lenguajes Script mucho más
adecuados). Además no es una herramienta exclusiva de C++, otros
compiladores utilizan la misma o muy parecida, ya que resuelve un problema que
es general a todos los lenguajes que dependen de un proceso de compilación y enlazado.
El lector que se enfrenta por primera vez al asunto, puede pensar que las definiciones y explicaciones que
se exponen a continuación son un "rollo" difícil de
entender. De momento no tiene imágenes mentales previas sobre las que
construir las ideas que le ayuden a comprender de que se está hablando
(tampoco se pueden poner ejemplos hasta que se tienen un mínimo de
definiciones y explicaciones), pero tened paciencia, en cuanto se llega a los
ejemplos concretos se empieza a entender "de que va el asunto" y cual es su intríngulis.
La filosofía de trabajo de Make se basa en una sencilla frase: "Ejecutar comandos en base a ciertas dependencias para conseguir determinados objetivos". Adelantemos aquí que las palabras: comandos, dependencias y objetivos ("targets") deben tomarse en un sentido amplio. Comandos son acciones que pueden responder cualquier orden aceptada por el SO y/o específicas que puede realizar Make; dependencias son una serie de relaciones que puede establecer el programador; suelen referirse a relaciones entre ficheros. Generalmente del tipo "la última modificación del fichero XXX debe ser posterior a la del fichero YYY; en caso contrario, se ejecutará el comando zzz". Cuando las dependencias se refieren a la existencia de ciertos ficheros, que son necesarios para crear otro, también es frecuente designarlas como prerrequisitos. Los objetivos o resultados son generalmente la construcción de ejecutables; ficheros objeto; ficheros de recursos, o librerías de cualquier tipo, aunque un target puede ser también el nombre que se le da a una determinada acción. Por ejemplo, borrar ciertos ficheros; moverlos de sitio; etc. En este último caso se denominan objetivos adicionales o falsos ("phony targets").
Observe que, en determinados casos, los prerrequisitos (existencia de ciertos ficheros como condición previa para la construcción de otros) son también objetivos para Make, por lo que deben incluirse los comandos necesarios para su construcción [1], pero estos objetivos no siempre son necesarios. Si tales ficheros existen y están actualizados, no son considerados como "targets". En este sentido, se denominan objetivos principales ("Goals") los que constituyen el fin último de Make. Los demás objetivos son accesorios y no siempre necesarios. Es evidente que en la invocación de Make se pretende siempre al menos un goal, aunque puede ser un phony goal.
§4 Makefiles
Make es en realidad un
intérprete de comandos con su propio lenguaje de "scripts" [3],
de forma que las instrucciones de operación se encuentran en un fichero de texto ASCII denominado makefile
(puede tener distintas terminaciones, por ejemplo .mak). Un makefile contiene reglas que
en cada caso indican al intérprete
qué resultados
("goals") se pretenden; qué acciones son necesarias, y qué
condiciones deben presentarse para que tales acciones sean ejecutadas.
Make analiza este escript para determinar el orden en que deben ser construidos los distintos
componentes. Como resultado se pueden obtener uno o varios ficheros (generalmente ejecutables) a los que
denominamos fichero resultado. Por ejemplo, si un ejecutable se obtiene a partir de tres
ficheros-objeto, estos
deben ser construidos antes de ser entregados al enlazador para que forme con
ellos un solo fichero.
Esto puede deducirlo Make automáticamente, sin necesidad de que coloquemos las
instrucciones que generan los ficheros-objeto antes de las que generan el
ejecutable. Los makefiles pueden incluso contener
instrucciones para instalar el programa resultante en la máquina cliente.
Distribución e instalación de aplicaciones. Generalmente las aplicaciones son distribuidas en forma de ficheros comprimidos que contienen los ejecutables y sus librerías. Por ejemplo, DLLs, así como los ficheros de datos; de ayuda; de configuración, etc. Generalmente un programa de instalación es el encargado de desempaquetar los módulos e instalarlos en los lugares adecuados de la máquina del cliente, y en caso necesario. Por ejemplo, en muchas aplicaciones Windows, incluir las entradas correspondientes en el Menú de Inicio, y realizar las modificaciones pertinentes en el Fichero de Registro. En ocasiones se incluye también un programa de desinstalación que, en su caso, además de deshacer los cambios efectuados en el fichero de registro, se encarga de borrar los ficheros y/o directorios instalados.
En el mundo Linux es muy frecuente que las aplicaciones sean distribuidas mediante los fuente, delegándose en el usuario la tarea de construir la aplicación e instalarla [6]. Los ficheros necesarios suelen venir también en formato comprimido, de los que existen varios tipos, aunque los más comunes son los conocidos tarball; ficheros obtenidos con una utilidad (tar) que los empaqueta en un único fichero .tar. A continuación otra utilidad comprime el fichero resultante, con lo que se obtiene un fichero .tar.gz o algo similar.
Para facilitar la construcción, junto con los fuentes y cualquier fichero auxiliar necesario, suele incluirse un makefile. Aunque también es frecuente que el makefile sea construido por el propio usuario en base un script de nombre configure, que puede ser ejecutado (mejor diríamos interpretado) por el shell del Sistema [7]. configure está diseñado de forma que genere un makefile adecuado a las características del sistema anfitrión, para lo que comprueba el tipo de procesador; si es de 32 o 64 bits. Etc. El sistema permite que la aplicación resultante pueda ser confeccionada "a medida" del anfitrión. Además, en ocasiones se permite que el usuario personalice algunas opciones de la aplicación según sus preferencias o necesidades. Por ejemplo, la localización preferida para los ficheros de la aplicación, selección del idioma, etc.
Los makefile son ficheros ASCII (texto plano) que contienen las instrucciones,
que al ser interpretadas por make, permiten construir
el proyecto. Esto supone que para cada fichero a
construir, el makefile incluya información del siguiente tenor:
- Objetivos: definen nombre y extensión del fichero que se desea construir. Tanto el principal ("goal") como los dependientes.
- Camino ("path"): lista de directorios que informan al compilador donde encontrar los ficheros necesarios (librerías, includes, etc).
- Dependencias: fichero/s cuyo "timestamp" (fecha y hora de creación) debe comprobarse para ver si son más modernos que el del fichero a construir, y en tal caso, proceder a la construcción o reconstrucción.
- Comandos: órdenes que se trasladan directamente al "Shell" del SO o representan la invocación de determinadas utilidades (compilador, enlazador, etc). Son los responsables de alcanzar los objetivos propuestos.
Naturalmente, la redacción de las líneas de órdenes del makefile requiere el
conocimiento del lenguaje "script" de la versión de Make utilizada,
así como del compilador, enlazador, compilador de recursos y cualquier otra
herramienta necesaria para construir el proyecto. Recordemos que, tanto
las peculiaridades de Make como del resto de herramientas, dependen de la
plataforma (Windows, Unix, Linux, Solaris, etc.) y que las "suites"
de programación C++ actuales permiten construir el makefile
de un proyecto de forma automática [5]. Sin embargo,
aun utilizando entornos de programación avanzados, no está de más un mínimo
conocimiento de las peculiaridades de Make, ya que, a semejanza de lo que ocurre
con la edición de páginas Web, donde de vez en cuando es necesario manejar
directamente el código HTML, también aquí es a veces necesario hacer retoques
manuales en el makefile o utilizar "includes" de nuestra propia
cosecha.
En general las líneas del fichero makefile son de cuatro tipos:
-
Comentarios: Cualquier cosa que se ponga detrás del carácter almohadilla #. Por ejemplo:
bcc32 hola.cpp # compilar el fichero hola.cpp
# la línea anterior produce un ejecutable
-
-
Implícitas: se refieren a dependencias y comandos que atañen a la compilación o enlazado de ficheros específicos; vienen preconstruidas en la lógica interna de Make y no necesitan ser explicitadas por el programador
.
-
Explícitas: instrucciones para la consecución de los objetivos redactadas por el programador
.
-
-
Macros : Se trata de expansiones/sustituciones parecidas a las del propio lenguaje C++
. Representan una comodidad sintáctica, y al igual que sus homónimas del C++, presentan la ventaja de poder agrupar determinadas definiciones en un solo punto, lo que hace más fácil el mantenimiento de los makefiles.
-
Directivas: Recuerdan las de Pascal o las del propio lenguaje C++
.
§5 Reglas:
Como se ha señalado, en principio Make tiene sus propias opciones por defecto,
pero el fichero de instrucciones makefile
puede contener reglas específicas para cada caso. Observe que aquí, "regla" puede tomarse en un sentido muy amplio; como una acción que
ocurre solo bajo determinadas condiciones. Como se ha indicado, las reglas pueden ser de dos tipos: explícitas
e implícitas
, y se componen de dos o más
líneas con la siguiente sintaxis:
resultado : prerrequisitos ... # línea de dependencias
comando
# línea de comando
...
...
Las líneas de comando deben tener un sangrado (al menos un
espacio) respecto a las líneas de dependencias correspondientes [2]. Por ejemplo, una regla
podría tener el siguiente diseño:
fuente.exe: fuente.o # Línea de dependencias
bcc32 -P -RT fuente.cpp # línea de comando (ojo al sangrado)
Aquí fuente.exe es el objetivo o resultado ("target") deseado y fuente.o es el prerrequisito. La regla debe interpretarse como sigue: sifuente.exe no existe, o su timestamp es anterior al de fuente.o, o fuent.o no existe, ejecutar el comando indicado en la segunda línea (obtener el fuente.exe a partir de la compilación y enlazado de fuente.cpp).
También es destacable que una regla puede carecer de prerrequisitos, lo que ocurre cuando no se requieren ningunas condiciones especiales para obtener el resultado (ejecutar el/los comandos asociados). Sería el siguiente caso:
cleanAll:
rm -f pr1.o pr2.o pr3.o main.exe
Mientras que la línea de dependencias utiliza diversas sintaxis, según se trate de reglas explícitas
o implícitas, la línea de comando siguen siempre la misma.
Make soporta múltiples líneas de dependencia para un solo resultado, y un resultado puede tener múltiples líneas de comando. Sin embargo, solo una línea de dependencia debe contener una línea de comando correspondiente. Por ejemplo:
Destino1: dependent1 dep2 dep3 dep4 dep5
Destino1: dep6 dep7 dep8
bcc32 -c $**
Cualquier línea del makefile puede continuar en la línea siguiente utilizando la barra inclinada ( \ ) al final de la primera línea. Ejemplo:
MIFUENTE.EXE: FILE1.OBJ \ # Línea de dependencia
FILE2.OBJ # continuación de la anterior
bcc32 file1.obj file2.obj # Línea de comando
§5.1 Reglas explícitas
Las reglas explícitas son las instrucciones contenidas en el makefile para conseguir cierto resultado. Como se ha indicado, tienen al menos dos líneas, de dependencia y de comando. Su sintaxis es la siguiente:
resultado [resultado...]:[:][{path}] [prerrequisito[s]...] # L. de dependencia
[prefijo] comando
# Línea de comando
Volvemos a insistir que la acción (indicada por el
comando
) no tiene que consistir necesariamente en construir un ejecutable, y que el resultado puede ser un simple nombre que identifica una serie de relaciones. Por ejemplo el fichero:# Makefile para copiar ficheros .htm en el directorio de objetos
copiahtm : {D:\ZWeb\Zhome\Cpp\}E1.htm E2.htm
© $** objetossería un makefile correcto para Make 5.2 de Borland, que no tendría ninguna relación con la construcción de un ejecutable. Aquí el resultado copiahtm sería lo que hemos denominado un phony goal
; una etiqueta para designar las relaciones señaladas en el resto de la línea. El comando expande el contenido de la etiqueta (gracias al indicador &) como si fuese una macro, y aplica al resultado de la expansión, el comando copy del "Shell" del SO. Como es de prever, el resultado es que los ficheros E1.htm y E2.htm del directorio D:\ZWeb\Zhome\Cpp, son copiados al directorio objetos.
Línea de
dependencia: se componen de uno o más nombres de fichero
resultado seguidos de ( :
) o dos dobles puntos ( ::
). Un doble punto ( :
) significa que una regla se refiere al resultado/s; dos dobles puntos
( ::
) significan que dos o más reglas son para el/los resultado/s.
resultado
: especifica el nombre del objetivo ("target")
a conseguir. Generalmente es un nombre de fichero.
Debe ser
un comienzo de línea en el fichero makefile, y no puede ser precedido con
espacios o tabulaciones. Para indicar más de un objetivo, deben
separarse sus nombres con espacios o tabulaciones. No puede utilizarse
un nombre de resultado más de una vez en una regla explícita.
path
: una lista de direcciones que indican a make
donde buscar los ficheros dependientes. Estas direcciones deben estar
separadas por punto y coma (;),
encerrando todo con corchetes { } (no dejar espacio
entre el corchete de cierre y el primer fichero !!).
Ejemplo :
copiahtm : {D:\ZWeb\Zhome\Cpp\}E1.htm E1_1.htm
prerrequisito/s
: nombre de los objetivos que deben
satisfacerse antes de ejecutar el comando señalado por la regla. Generalmente
son nombres de ficheros cuyas fecha y hora
deben ser comprobadas por make para comprobar si son más recientes que el fichero destino. Cada nombre debe
ser precedido por un espacio. Si un fichero dependiente
aparece en algún sitio como resultado
,
make actualiza o crea ese destino antes de utilizarlo como prerrequisito
en el resultado
original. Es lo que se conoce como dependencia
encadenada.
Línea de
comando: Puede ser cualquier comando aceptado por el
SO. Las líneas de
comando deben ser sangradas (indentadas) al menos con un espacio o tabulación
(
de lo contrario son interpretadas como líneas
dependencias
!!). Pueden indicarse múltiples órdenes separadas por espacios. La sintaxis geneal es:
[prefijo] comando
Ejemplos:
© $** objetos # representa el sangrado inicial; & es el prefijo
cd..
bcc32 -c mysource.c
COPY *.OBJ C:\PROJECTO
bcc32 -c $(SOURCE)
@bcc32 diff.obj
-bcc32 hola.cpp
El prefijo es opcional, y depende de la implementación. comando
puede ser prácticamente cualquiera aceptado por el SO, además, cada versión
de Make
acepta algunos comandos específicos.
§5.2 Reglas implícitas
Son reglas preconstruidas en la lógica interna de Make, que permiten
construir los tipos de ficheros más usuales sin que el programador tanga que
definir explícitamente los comandos necesarios. Estas reglas suelen utilizar algunas macros también predefinidas (ver a continuación
).
Por ejemplo, el Make de GNU dispone de una regla implícita que le permite construir ficheros .o a partir de ficheros .cpp (fuentes C++):
%.o: %.cpp
$(COMPILE.cpp) $(OUTPUT_OPTION) $<
a su vez, COMPILE.cpp y OUTPUT_OPTION son sendas macros también predefinidas:
COMPILE.cpp = $(COMPILE.cc)
COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
CXX = g++
OUTPUT_OPTION = -o $@
En consecuencia, la línea de comando de la regla resulta equivalente a
g++ $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o $@ $<
Aquí g++ representa la invocación del compilador GNU C++ (g++.exe); a suv vez $@ y $< son variables automáticas de Make, cuyos valores se calculan cada vez que se ejecuta la regla en base al contenido de la línea de dependencias. En concreto, $@ se traduce en el nombre del resultado (target) de la regla, mientras que $< es el nombre del primer prerrequisito. El resultado es que en esta regla implícita, pueden ser tomados como equivalentes al nombre del fichero objeto (target) y del fichero fuente (prerrequisito). En consecuencia, la línea de comando anterior es equivalente a:
g++ $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o %.o %.cpp
La utilidad de todo esto puede entenderse si consideramos un ejemplo.
Supongamos que compilamos un proyecto que depende de dos fuentes: main.cpp
y modulo.cpp. La construcción del proyecto puede ser encomendada a
un makefile adecuado a nuestro entorno:
INCLUDES = -I"C:/GNU/include" \
-I"C:/GNU/include/c++"
main.exe: main.o modulo.o
g++ main.o modulo.o -o "main.exe" -L"C:/GNU/lib"
main.o: main.cpp
g++ -c main.cpp -o main.o $(INCLUDES)
modulo.o: modulo.cpp
g++ -c modulo.cpp -o zstring.o $(INCLUDES)
El makefile anterior, aunque correcto, puede ser simplificado con mínimas modificaciones hasta quedar con el siguiente diseño:
CXXFLAGS = -I"C:/GNU/include" \
-I"C:/GNU/include/c++"
main.exe: main.o modulo.o
g++ main.o modulo.o -o "main.exe" -L"C:/GNU/lib"
main.o: main.cpp
modulo.o: modulo.cpp
La razón es que, ante reglas como las de las dos últimas líneas, para las que no existe ninguna línea de comando que informe sobre la forma de obtener los ficheros .o a partir de los fuente .cpp, Make hecha mano de sus reglas implícitas para ver si existe una forma de hacerlo. La regla comentada al principio del epígrafe es la adecuada, así que es aplicada con las sustituciones pertinentes en cada instancia, y el proyecto es construido sin dificultad. Observe que ha sido necesario renombrar la macro original INCLUDES, que ahora pasa a denominarse CXXFLAGS. La razón es la ya comentada de que Make utiliza ciertos nombres en sus reglas implícitas; nombres que deben ser respetados si queremos aprovechar las posibilidades de este tipo de reglas.
Como última observación, señalar que el proyecto podría ser construido con un makefile aún más simple:
CXXFLAGS = -I"C:/GNU/include" \
-I"C:/GNU/include/c++"
main.exe: main.o modulo.o
g++ main.o modulo.o -o "main.exe" -L"C:/GNU/lib"
La razón es que, aunque en este caso no se indica que main.o o modulo.o deban ser obtenidos de la compilación de sus homónimos .cpp, Make dispone de reglas implícitas para intentar la compilación en caso de que tales ficheros existan. La forma de hacerlo es análoga a la comentada.
§6 Macros
El lenguaje "script" de make
permite el uso de macros parecidas (aunque diferentes) a las del lenguaje C++ ( 4.9.10b
). Una macro tiene dos partes: La etiqueta identificativa y
el texto en que se expandirá. El proceso de usar la macro tiene dos pasos: definir la
macro y señalar el punto de expansión (donde de realiza la sustitución de
la macro -la etiqueta- por el texto).
La definición se realiza colocando una etiqueta (nombre de la macro); el símbolo de asignación ( = ) y el texto. Como en el caso de C++, suelen utilizarse mayúsculas en las etiquetas. La etiqueta debe aparecer en el primer carácter de la línea (sin espacio o sangrado previo de ningún tipo). Por ejemplo:
COMPILA = bcc32 -MProyecto.map # define la macro COMPILA
El sitio de la sustitución se señala encerrando en un paréntesis la etiqueta de la macro a expandir precedido del símbolo $. Por ejemplo, la línea de comando:
$(COMPILA) -eProyecto.exe pa.obj pb.obj # Expande la macro COMPILA
será expandida a:
bcc32 -MProyecto.map -eProyecto.exe pa.obj pb.obj
Recordar que, como todo en el lenguaje C++, la etiqueta de la macro es sensible a
mayúscula/minúscula; COMPILA y Compila
serían macros distintas. El texto en que se expandirá la macro puede
contener hasta 4096 caracteres (Borland C++ make), cada macro se define en una
línea diferente, aunque una macro puede contener más de una línea separadas
por la barra inclinada "\". Si existen dos macros distintas con la misma etiqueta, la
segunda sobreescribe a la primera definición.
§6.1 Macros predefinidas
Es digno de mención que Make tiene dispone de gran número de macros predefinidas (dependen de la implementación) que pueden ser aprovechadas para simplificar la redacción del texto y para las reglas implícitas. Si una línea del makefile redefine alguna de estas macros predefinidas, es válida la regla del párrafo anterior (la versión del makefile sobreescribe a la definición inicial).
Por ejemplo, la versión GNU de Make dispone de la siguiente macro predefinida:
RM = rm -f
en consecuencia, en dicha versión de Make es posible utilizar la siguiente regla sin necesidad de definir previamente la macro:
clean:
${RM} objeto.o cabecera.h ejecutable.exe
§7 Directivas
Los scripts de make permiten incluso directivas del tipo include, undef, ifdef,
if, elif etc. que recuerdan las del propio lenguaje C++
( 4.9.10).
§8 Invocación
La sintaxis para invocar make depende de la plataforma, pero generalmente es del tipo:
make [opciones] [resultado [resultado]]
Las opciones son indicaciones que controlan aspectos del funcionamiento de Make. Generalmente puede obtenerse una lista de ellas y su significado invocando make con el argumento -? o -h. La palabra "make", las opciones y los resultados deben estar separados por espacios.
resultado es el nombre del fichero/s que se pretende construir. Este nombre se ha indicado también en makefile (el fichero que lee Make para extraer las instrucciones).
En principio Make tiene sus propias opciones de operación por
defecto, pero la invocación puede imponer sus propias reglas. Por
ejemplo, en ausencia del argumento resultado en la invocación, Make establece que el goal es el primer
resultado que aparece reseñado en el makefile, pero invocándolo con un
argumento puede establecerse que el goal sea otro. Por ejemplo,
teniendo el makefile mk1.txt:
# Malefile mk1.txt
all: main.exe
clean:
erase main.o
erase main.h
erase main.exe
main.exe: main.o main.h
g++.exe main.o -o "main.exe" -L"C:/DEV-CPP/lib"
La invocación
make -f mk1.txt
obliga a Make a utilizar el fichero mk1.txt en lugar del makefile establecido por defecto (por ejemplo make.mak para Borland 5). En este caso, el goal es all, el primer resultado que se encuentra, lo que conduce a que se ejecute la regla de las líneas 10-11, con el resultado de que se enlaza el fichero main.o para obtener el ejecutable main.exe. En cambio, la invocación
make
utilizaría el makefile por defecto. En caso de que utilizáramos Borland 5 y que no existiera el fichero make.mak, se obtendría un error. Finalmente, la invocación
make -f mk1.txt clean
señala a Make que se pretende el resultado señalado por la regla clean; lo que conduce a que se ejecuten los comandos de las líneas 6 a 8, sin que tenga lugar ninguna otra acción.
§9 Secuencia de ejecución
Después de cargar el makefile, make trata de construir solamente el primer resultado explícito incluido en el makefile. Para esto comprueba la fecha y hora de los ficheros dependientes del primer resultado ("goal"). Si son más recientes que las del fichero resultado, make ejecuta el comando asociado para actualizarlo.
Si alguno de los ficheros dependientes del primer resultado es utilizado a su vez como resultado en algún otro punto del makefile, make comprueba dichas dependencias y construye ese resultado antes que construir el primero. Este comportamiento se denomina dependencia encadenada.
Ejemplo:
ejecutable.exe : objeto.o # L.1: línea de dependencias
bcc32 objeto.o # L.2: línea de comando
...
objeto.o : cabecera.h # L.3: línea de dependencias
trigraph cabecera.h # L.4: línea de comando
En este makefile, el goal es ejecutable.exe; se supone que este fichero se construye con el comando señalado en L.2, y está relacionado con objeto.o. Si la fecha y hora de objet.o son posteriores a las de ejecutable.exe (o sencillamente ejecutable.exe no existe), se ejecuta el comando. No obstante, como el prerrequisito objeto.o aparece a su vez como resultado de la regla L.3, deben satisfacerse primero las condiciones de este fichero, para lo que se invoca el comando señalado en L.4 (si objeto.o no existe o su "timestamp" es anterior que el de cabecera.h). Por último se ejecutaría L.2.
Si se produce algún fallo durante el proceso, make borra los ficheros que estuviese construyendo. Si desea que make conserve el fichero destino a pesar de un posible fallo, debe utilizarse la directiva .precious. Los comandos Ctrl+Break o Ctrl+C permiten abortar el trabajo de make una vez iniciado.
Nota: la utilidad Make es más potente y compleja de lo que se ha señalado someramente en este capítulo [4], en especial las opciones relativas a macros y las directivas de proceso. No pretendemos aquí reproducir el manual del Make de Borland, de GNU, ni de ningún otro compilador específico, solo indicar las líneas generales de lo que puede esperarse de esta utilidad y su filosofía de funcionamiento. No obstante, esta introducción le permitirá comenzar practicar con sus propios makefiles, entender mejor los capítulos que siguen dedicados a versiones específicas de Make, e ir explorando sus posibilidades con ayuda del manual de su compilador.
[1] Más adelante, al tratar de las reglas implícitas, veremos que esta especificación no siempre es necesaria, ya que Make dispone de determinadas reglas implícitas que le permiten conocer como construir los ficheros de tipo más corriente. Por ejemplo, como obtener un fichero .o a partir de uno .cc.
[2] La versión GNU de Make necesita una tabulación (TAB) como primer carácter. Si se utilizan solo uno o dos espacios, interpreta que son destinos, mostrándose un mensaje de error:
makefile....:nn: *** multiple target patterns. Stop.
En cambio a la de Borland C++ 5.5 le basta con un espacio. Esta desafortunada circunstancia me hizo perder toda una tarde con un makefile sencillísimo hasta que finalmente, cuando estaba apunto de marcharme a limpiar cochineras, por pura casualidad pude percatarme de la diferencia.
[3] Se denominan "Scripts" un tipo de de ficheros, cuyo texto son instrucciones que son leídas por un intérprete (que utiliza un lenguaje de comandos en formato texto). Existen "scripts" para Java (Jscript); JavaScript (incluido en los navegadores); Visual Basic; Access; make, etc. En este sentido, un fichero .BAT de MS-DOS es un "script" para command.com.
[4] Últimamente está cobrando importancia una
utilidad derivada de Make que es particularmente potente y sofisticada. Es bjam
( Boost.Jam),
la utilidad utilizada para construir las librerías Boost. Es también un
intérprete de scripts que utiliza un lenguaje específico y sus propios
ficheros de órdenes (jamfiles). Es propiedad intelectual (copyright) de Christopher Seiwald
y Perforce Software, aunque la versión utilizada por Boost es muy generosa en
cuanto a sus posibilidades: "License is hereby granted to use this software and distribute it
freely, as long as this copyright notice is retained and modifications are clearly
marked".
[5] En estos casos el makefile se suele denominar fichero de proyecto ("Proyect file") o un nombre similar, y el proceso de construcción de las dependencias está más o menos velado al usuario, pero la base de funcionamiento es la que aquí se analiza.
[6] Naturalmente, lo anterior exige que el usuario disponga del compilador adecuado, lo que es relativamente frecuente en este tipo de Sistemas. Además se tiene la comodidad de que el compilador GNU GCC permite compilar indistintamente C y C++.
[7] Generalmente configure es un script, es bastante complejo, pero se dispone de una utilidad, autoconf, que permite generarlo a partir de un fichero de especificación (template-file) más sencillo.