← Programación en C para Sistemas Operativos Bloque 8 · Compilación, enlazado y bibliotecas · Tema 35 de 42

Tema 35

Makefiles

En los últimos temas la construcción de un programa se volvió una secuencia de órdenes: compilar cada fuente con -c, armar una biblioteca, enlazar con -L y -l. Escribirlas a mano en cada cambio es lento y propenso a errores, y recompilar todo siempre es un desperdicio cuando solo cambió un archivo. La herramienta clásica para resolverlo es make: lee un archivo llamado Makefile que describe qué se construye a partir de qué, y ante cada ejecución recompila únicamente lo que quedó desactualizado. Este tema enseña la sintaxis esencial (reglas, objetivos, requisitos y recetas), las variables y las reglas de patrón que evitan repetirse, y cómo depurar un Makefile cuando no hace lo que se espera. El tema 36 lo lleva a proyectos reales, con dependencias automáticas y directorios de compilación.

1. Qué hace make

Un programa en C de tamaño mediano son varios archivos .c, sus encabezados y las órdenes de gcc para convertirlos en un ejecutable. La compilación separada (tema 31) ya permite no repetir trabajo: si solo se tocó numeros.c, basta regenerar numeros.o y enlazar de nuevo. Pero hay que acordarse de qué archivo cambió y de qué depende cada cosa. make automatiza esa memoria con una idea simple: cada archivo generado (objetivo) se declara junto con los archivos de los que se obtiene (sus requisitos) y la orden que lo produce (la receta). Al ejecutarse, compara las fechas de modificación: si algún requisito es más nuevo que el objetivo, o el objetivo no existe, ejecuta la receta. Si no, lo deja como está.

Todo el tema usa un proyecto pequeño, el mismo de los dos temas anteriores: un encabezado inc/util.h, dos módulos (numeros.c y texto.c) y un main.c que los usa. Los compiladores y salidas son los de siempre: gcc -std=c17 -Wall -Wextra, probado con GNU Make 4.4.1 en Linux.

inc/util.h, numeros.c, texto.c y main.c
/* inc/util.h */
#ifndef UTIL_H
#define UTIL_H
#include <stddef.h>

int maximo(const int *v, size_t n);
double promedio(const int *v, size_t n);
void invertir(char *s);
#endif

/* numeros.c */
#include "util.h"

int maximo(const int *v, size_t n)
{
    int m = v[0];
    for (size_t i = 1; i < n; i++)
        if (v[i] > m)
            m = v[i];
    return m;
}

double promedio(const int *v, size_t n)
{
    long s = 0;
    for (size_t i = 0; i < n; i++)
        s += v[i];
    return (double)s / (double)n;
}

/* texto.c */
#include <string.h>
#include "util.h"

void invertir(char *s)
{
    size_t n = strlen(s);
    for (size_t i = 0; i < n / 2; i++) {
        char t = s[i];
        s[i] = s[n - 1 - i];
        s[n - 1 - i] = t;
    }
}

/* main.c */
#include <stdio.h>
#include "util.h"

int main(void)
{
    int v[] = {4, 9, 2, 7};
    char s[] = "enlace";

    printf("maximo=%d promedio=%.2f\n", maximo(v, 4), promedio(v, 4));
    invertir(s);
    printf("%s\n", s);
    return 0;
}

2. La regla: objetivo, requisitos y receta

Un Makefile es una lista de reglas. Cada regla tiene esta forma:

forma de una regla
objetivo: requisito1 requisito2 ...
<TAB>orden que construye el objetivo
<TAB>otra orden, si hace falta

La primera línea dice qué se construye y de qué depende. Las siguientes, la receta, dicen cómo, y cada una debe empezar con un carácter de tabulación (no espacios). Es la regla de sintaxis que más tropieza al principio; en los ejemplos de este tema, el sangrado de las recetas es un tabulador real. Para el proyecto, una primera versión con todo explícito:

Makefile (versión 1: todo escrito a mano)
prog: main.o numeros.o texto.o
	gcc -o prog main.o numeros.o texto.o

main.o: main.c inc/util.h
	gcc -std=c17 -Wall -Wextra -Iinc -c main.c

numeros.o: numeros.c inc/util.h
	gcc -std=c17 -Wall -Wextra -Iinc -c numeros.c

texto.o: texto.c inc/util.h
	gcc -std=c17 -Wall -Wextra -Iinc -c texto.c

Se ejecuta con la orden make, que busca un archivo llamado Makefile (o makefile) en el directorio actual. Sin argumentos construye el primer objetivo del archivo, el «objetivo por defecto» (aquí, prog):

terminal
$ make
gcc -std=c17 -Wall -Wextra -Iinc -c main.c
gcc -std=c17 -Wall -Wextra -Iinc -c numeros.c
gcc -std=c17 -Wall -Wextra -Iinc -c texto.c
gcc -o prog main.o numeros.o texto.o
$ make
make: 'prog' is up to date.

La primera vez construye todo, en el orden que dictan las dependencias: para hacer prog hacen falta los tres .o, y cada uno se hace a partir de su .c. make imprime cada receta antes de ejecutarla. La segunda vez no hay nada que hacer, y lo dice (is up to date: está al día). Veamos qué pasa cuando cambia algo:

terminal · solo se rehace lo afectado
$ touch numeros.c; make
gcc -std=c17 -Wall -Wextra -Iinc -c numeros.c
gcc -o prog main.o numeros.o texto.o
$ touch inc/util.h; make
gcc -std=c17 -Wall -Wextra -Iinc -c main.c
gcc -std=c17 -Wall -Wextra -Iinc -c numeros.c
gcc -std=c17 -Wall -Wextra -Iinc -c texto.c
gcc -o prog main.o numeros.o texto.o
$ make main.o
make: 'main.o' is up to date.

(touch actualiza la fecha de un archivo sin cambiarle el contenido, una forma rápida de simular una edición.) Al tocar numeros.c solo se rehace numeros.o y, como cambió, también el ejecutable. Al tocar el encabezado, que figura como requisito de los tres objetos, se rehace todo. Y se puede pedir cualquier objetivo por nombre: make main.o construye solo eso (y lo que necesite).

3. Cómo decide qué reconstruir

Internamente, las reglas forman un grafo de dependencias. Para construir un objetivo, make primero construye (recursivamente) cada uno de sus requisitos, y después compara fechas:

Grafo de dependencias del proyecto. El ejecutable prog depende de main.o, numeros.o y texto.o. Cada objeto depende de su archivo .c y de inc/util.h. make recorre el grafo de abajo hacia arriba y rehace cada objetivo cuyo requisito es más reciente prog main.o numeros.o texto.o main.c numeros.c inc/util.h texto.c Las flechas van del requisito al objetivo. Si un requisito es más nuevo, el objetivo se rehace y el cambio sube.
El grafo del Makefile de la versión 1; las líneas punteadas son el encabezado, requisito de los tres objetos.

La opción --debug=b (basic) hace que make explique cada decisión. Con numeros.c modificado:

terminal · las decisiones de make (se omite el encabezado de versión)
$ touch numeros.c; make --debug=b
Reading makefiles...
Updating makefiles....
Updating goal targets....
  Prerequisite 'numeros.c' is newer than target 'numeros.o'.
 Must remake target 'numeros.o'.
gcc -Iinc -std=c17 -Wall -Wextra -c -o numeros.o numeros.c

Otra consecuencia es el objetivo por defecto: si no se indica ninguno, se construye el primero del archivo. Por eso en un Makefile el objetivo principal (por convención, all o el nombre del programa) va arriba. Hay además dos mensajes de error que conviene reconocer:

terminal
$ make nada
make: *** No rule to make target 'nada'.  Stop.
$ mkdir sin; cd sin; make
make: *** No targets specified and no makefile found.  Stop.

El primero dice que no hay regla ni archivo con ese nombre. El segundo, que ni siquiera hay un Makefile en el directorio. Si falta un requisito que tampoco se puede construir, el mensaje nombra qué objetivo lo pedía: No rule to make target 'faltante.o', needed by 'prog'.

4. Los encabezados también son requisitos

En la versión 1 cada objeto lista inc/util.h entre sus requisitos, y no es un detalle: make solo sabe lo que el Makefile le dice. Los #include de los fuentes no los lee. Para ver qué pasa si se omite, un proyecto mínimo donde a.c usa una constante de inc/util.h, y la regla de a.o declara solamente a.c:

Makefile mínimo, sin el encabezado como requisito
CFLAGS = -Iinc

a: a.o
	$(CC) -o $@ $^

a.o: a.c
	$(CC) $(CFLAGS) -c $<
terminal · se cambia FACTOR de 2 a 3 en el encabezado
$ make
cc -Iinc -c a.c
cc -o a a.o
$ ./a
2
$ sed -i 's/2/3/' inc/util.h
$ make
make: 'a' is up to date.
$ ./a
2

El encabezado cambió, pero make afirma que todo está al día y el programa sigue imprimiendo el valor viejo. Es una de las fuentes de error más insidiosas del desarrollo en C: un binario que no refleja el código. Hay dos remedios. El simple, el de la versión 1, es listar a mano los encabezados de cada objeto. El correcto para proyectos de verdad es que el compilador mismo genere esa lista (gcc -MMD), lo que se hace en el tema 36. Mientras tanto, ante la duda, make clean y volver a construir.

5. La receta: cada línea es un shell

Las líneas de una receta las ejecuta /bin/sh, y detalles de cómo lo hace explican varios comportamientos curiosos:

err.mk (objetivo a: false y un echo; objetivo b: lo mismo con un guion), sh.mk y terminal
/* sh.mk */
x:
	@echo con arroba
	echo sin arroba
	cd /tmp; pwd
	pwd
	cd /tmp && pwd

$ make -f err.mk a; echo "rc=$?"
false
make: *** [err.mk:2: a] Error 1
rc=2
$ make -f err.mk b; echo "rc=$?"
false
make: [err.mk:5: b] Error 1 (ignored)
echo si llega
si llega
rc=0
$ make -f sh.mk x
con arroba
echo sin arroba
sin arroba
cd /tmp; pwd
/tmp
pwd
/home/ana/c/proy
cd /tmp && pwd
/tmp

En a, el false falla y la segunda línea nunca corre. En b, el guion hace que make siga. En sh.mk, la línea @echo con arroba se imprime sin repetir la orden; cd /tmp; pwd funciona porque el cd y el pwd están en la misma línea, y el pwd solo de la línea siguiente muestra que el directorio no cambió. Para encadenar, se escribe todo en una línea con && o ;, que puede partirse con \ al final.

Otra consecuencia: el símbolo $ lo interpreta make primero. Para que una variable llegue al shell hay que duplicarlo: $$f.

dd.mk y terminal
all:
	@for f in uno dos; do echo "archivo $$f"; done

$ make -f dd.mk
archivo uno
archivo dos

6. Variables y variables automáticas

La versión 1 repite gcc y las opciones en cada regla. Las variables permiten escribirlas una vez. Se definen con NOMBRE = valor y se usan con $(NOMBRE). Por convención, y porque las reglas implícitas de make las usan (sección 7), se emplean estos nombres:

VariableContenido
CCEl compilador de C.
CPPFLAGSOpciones del preprocesador: -Iinc, -DDEBUG.
CFLAGSOpciones del compilador: -std=c17 -Wall -Wextra -O2 -g.
LDFLAGSOpciones del enlazador: -L…, -Wl,-rpath,….
LDLIBSLas bibliotecas a enlazar: -lm, -lbasico. Van al final (tema 32).

Y las variables automáticas, que make rellena en cada regla según el objetivo y los requisitos, y que evitan repetir los nombres:

VariableSignificado
$@El nombre del objetivo.
$<El primer requisito.
$^Todos los requisitos, sin repetir.
$?Los requisitos más nuevos que el objetivo.

Con ellas, la versión 2 del Makefile se acorta y se vuelve más fácil de mantener. Usa una regla de patrón (sección 7) para no escribir tres veces la receta de los objetos:

Makefile (versión 2)
CC      = gcc
CFLAGS  = -std=c17 -Wall -Wextra -Iinc
OBJ     = main.o numeros.o texto.o

prog: $(OBJ)
	$(CC) -o $@ $(OBJ)

%.o: %.c inc/util.h
	$(CC) $(CFLAGS) -c $<

clean:
	rm -f prog $(OBJ)
terminal
$ make clean; make
rm -f prog main.o numeros.o texto.o
gcc -std=c17 -Wall -Wextra -Iinc -c main.c
gcc -std=c17 -Wall -Wextra -Iinc -c numeros.c
gcc -std=c17 -Wall -Wextra -Iinc -c texto.c
gcc -o prog main.o numeros.o texto.o

Para ver las variables automáticas en acción, un objetivo que solo las muestra. Con main.o y numeros.o recién creados, y luego con numeros.o más nuevo que el objetivo todo:

va.mk y terminal
todo: main.o numeros.o
	@echo "@=$@"
	@echo "<=$<"
	@echo "^=$^"
	@echo "?=$?"

$ make -f va.mk
@=todo
<=main.o
^=main.o numeros.o
?=main.o numeros.o
$ touch todo; sleep 1; touch numeros.o; make -f va.mk
@=todo
<=main.o
^=main.o numeros.o
?=numeros.o

La diferencia entre $^ (todos) y $? (solo los que provocaron la reconstrucción) se ve en la segunda ejecución.

Las cuatro formas de asignar

Hay varios operadores de asignación y se comportan distinto:

OperadorSignificado
=Asignación diferida: el valor se expande cada vez que se usa, con lo que valgan entonces las demás variables.
:=Asignación inmediata: se expande en el momento de definirla.
?=Asigna solo si la variable no estaba definida (valor por defecto).
+=Agrega al valor existente.
var.mk y terminal
A = $(B)
B = uno
C := $(B)
B = dos
D ?= defecto
E = base
E += mas
all:
	@echo "A=$(A) C=$(C) D=$(D) E=$(E)"

$ make -f var.mk
A=dos C=uno D=defecto E=base mas
$ D=entorno make -f var.mk
A=dos C=uno D=entorno E=base mas
$ make -f var.mk D=linea
A=dos C=uno D=linea E=base mas

A vale dos: con = se expande al usarla, y para entonces B ya cambió. C, definida con :=, conservó uno. D toma el valor por defecto solo si no viene de afuera, y la última ejecución muestra otra regla importante: una variable dada en la línea de órdenes (make D=linea) gana a la del Makefile. Es la manera de cambiar una opción sin editar el archivo:

terminal · mismo Makefile, otras opciones de compilación
$ make clean >/dev/null; make CFLAGS="-std=c17 -Iinc -O2 -g"
gcc -std=c17 -Iinc -O2 -g -c main.c
gcc -std=c17 -Iinc -O2 -g -c numeros.c
gcc -std=c17 -Iinc -O2 -g -c texto.c
gcc -o prog main.o numeros.o texto.o

Las variables de entorno también se convierten en variables de make, pero con menos prioridad que el Makefile: si el archivo define V, el entorno no la cambia, salvo que se use make -e. Se ve con un archivo que define V = del makefile:

terminal
$ V=entorno make -f env.mk
V=del makefile
$ V=entorno make -e -f env.mk
V=entorno

En resumen, el orden de prioridad (de mayor a menor) es: la línea de órdenes, el Makefile, el entorno y los valores por omisión de make. Por eso la convención de escribir CFLAGS ?= -O2 (valor por defecto que el usuario puede reemplazar desde afuera) es tan frecuente en proyectos de código abierto.

7. Reglas de patrón y reglas implícitas

La regla %.o: %.c inc/util.h de la versión 2 es una regla de patrón: el % representa cualquier «raíz» de nombre, igual en el objetivo y en los requisitos. Dice: para construir cualquier X.o hace falta X.c y el encabezado, y se hace con esta receta. En la receta, $< es el .c correspondiente (el primer requisito). Con tres módulos el ahorro es modesto, pero con cien es la diferencia entre un Makefile manejable y uno que nadie quiere tocar.

Lo curioso es que make ya trae reglas implícitas para los casos comunes, sin que se escriba nada. En un directorio sin Makefile, con solo un hola.c:

terminal · sin ningún Makefile en el directorio
$ make hola
cc     hola.c   -o hola
$ make hola
make: 'hola' is up to date.
$ make -B hola CFLAGS="-Wall -O2" LDLIBS=-lm
cc -Wall -O2    hola.c  -lm -o hola
$ make -n -B hola CPPFLAGS=-DDEBUG
cc  -DDEBUG   hola.c   -o hola
$ make hola.o
cc    -c -o hola.o hola.c

Sin saber nada del proyecto, make descubrió que para el objetivo hola existe hola.c y aplicó su receta incorporada. Las variables de la sección 6 son justamente las que esa receta consulta: CFLAGS y CPPFLAGS al compilar, LDFLAGS y LDLIBS al enlazar. (Los espacios de más en la salida son las variables vacías que se expanden.) El compilador por defecto es cc, el nombre genérico que en Linux suele ser gcc. Se pueden consultar las reglas y variables incorporadas con make -p (print database):

terminal · extracto de make -p -f /dev/null
$ make -p -f /dev/null 2>/dev/null | grep -A2 '^%.o: %.c'
%.o: %.c
#  recipe to execute (built-in):
	$(COMPILE.c) $(OUTPUT_OPTION) $<
$ make -p -f /dev/null 2>/dev/null | grep -E '^(CC|COMPILE.c|OUTPUT_OPTION) '
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
OUTPUT_OPTION = -o $@
CC = cc

Con esto se entiende el método habitual en proyectos de C: definir las variables correctas y dejar que las reglas implícitas hagan el trabajo, o escribir una regla de patrón propia cuando se necesite controlar la receta, como en la versión 2, donde los encabezados deben figurar como requisito, algo que la regla incorporada no sabe.

8. Listas de archivos: wildcard y patsubst

Mantener la lista OBJ = main.o numeros.o texto.o a mano se vuelve incómodo al agregar archivos. make tiene funciones para calcularla, que se invocan como $(función argumentos):

ExpresiónResultado
$(wildcard *.c)Los archivos que coinciden con el patrón: main.c numeros.c texto.c.
$(patsubst %.c,%.o,$(SRC))Cambia la terminación de cada elemento de la lista.
$(SRC:.c=.o)Forma abreviada de lo anterior (referencia con sustitución).
$(info texto)Imprime el texto al leer el Makefile, útil para depurar.
info.mk y terminal
SRC = $(wildcard *.c)
OBJ = $(patsubst %.c,%.o,$(SRC))
$(info SRC = $(SRC))
$(info OBJ = $(OBJ))
$(info DIR = $(notdir $(CURDIR)))
all: ;

$ make -f info.mk
SRC = main.c numeros.c texto.c
OBJ = main.o numeros.o texto.o
DIR = proy
make: 'all' is up to date.

(all: ; es una regla sin requisitos y con receta vacía, solo para que haya un objetivo.) Con estas funciones, la versión 3 del Makefile no necesita cambios al sumar un módulo: basta crear el .c nuevo. Además adopta la convención de separar CPPFLAGS (preprocesador) de CFLAGS (compilador), y usa $^ para enlazar con todos los objetos:

Makefile (versión 3)
CC       = gcc
CPPFLAGS = -Iinc
CFLAGS   = -std=c17 -Wall -Wextra
SRC      = $(wildcard *.c)
OBJ      = $(SRC:.c=.o)

prog: $(OBJ)
	$(CC) -o $@ $^

%.o: %.c inc/util.h
	$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

clean:
	rm -f prog $(OBJ)
terminal
$ make clean; make
rm -f prog main.o numeros.o texto.o
gcc -Iinc -std=c17 -Wall -Wextra -c -o main.o main.c
gcc -Iinc -std=c17 -Wall -Wextra -c -o numeros.o numeros.c
gcc -Iinc -std=c17 -Wall -Wextra -c -o texto.o texto.c
gcc -o prog main.o numeros.o texto.o
$ ./prog
maximo=9 promedio=5.50
ecalne

Hay que tener cuidado con una consecuencia de wildcard: toma todos los .c del directorio, incluidos los de prueba o los que no formen parte del programa. Para proyectos con subdirectorios y varios ejecutables, el tema 36 usa listas explícitas o patrones por carpeta.

9. Objetivos que no son archivos: clean

Un objetivo como clean no produce un archivo llamado «clean»: es un nombre para una acción. Funciona porque, mientras no exista un archivo con ese nombre, make lo considera siempre desactualizado y ejecuta la receta. Pero si un día aparece un archivo (o directorio) llamado clean, la regla deja de ejecutarse:

terminal · un archivo «clean» bloquea la regla; .PHONY lo evita
$ cat p.mk
clean:
	rm -f a a.o
$ touch clean; make -f p.mk clean
make: 'clean' is up to date.
$ cat p2.mk
.PHONY: clean
clean:
	rm -f a a.o
$ make -f p2.mk clean
rm -f a a.o

La declaración .PHONY: clean (de phony, falso) le dice a make que ese objetivo es solo una acción, que no corresponde a un archivo, y que debe ejecutarse siempre. Es el remedio estándar para clean, all, install y test; el tema 36 lo desarrolla junto con la estructura completa de un Makefile real. La convención de nombres entre los programadores de C es: all construye todo (y va primero), clean borra lo generado, install copia a su destino y test corre las pruebas.

10. Opciones útiles y cómo depurar

La mayoría de los problemas de un Makefile se resuelven viendo qué haría make sin hacerlo, o por qué decidió lo que decidió:

OpciónQué hace
-nMuestra las órdenes que ejecutaría, sin ejecutarlas (dry run).
-BReconstruye todo, ignorando las fechas.
-sSilencioso: no imprime las órdenes.
-qNo ejecuta nada; el código de salida dice si hay algo desactualizado (0 = al día, 1 = hay trabajo).
-f archivoUsa otro Makefile.
-j NEjecuta hasta N recetas en paralelo.
--debug=bExplica por qué se rehace cada objetivo.
-pImprime todas las reglas y variables que conoce (incorporadas y propias).
terminal · -q, -s y la comprobación con a.c sin cambios y después tocado
$ make -q; echo "rc=$?"
rc=0
$ touch a.c; make -q; echo "rc=$?"
rc=1
$ make -s; ./a
3

El caso de -j merece un ejemplo, porque es la mejor razón práctica para tener las dependencias bien escritas: make sabe qué objetivos son independientes y los puede construir a la vez. Tres objetivos que tardan un segundo cada uno:

j.mk y terminal
all: a b c
a:
	@sleep 1; echo a
b:
	@sleep 1; echo b
c:
	@sleep 1; echo c

$ time make -f j.mk
a
b
c

real	0m3.010s
$ time make -j3 -f j.mk
a
b
c

real	0m1.005s

En serie son tres segundos; con -j3, uno. En una compilación real, make -j$(nproc) usa todos los núcleos, y es la razón por la que un Makefile con dependencias incompletas puede funcionar «de casualidad» en serie y fallar en paralelo: si un requisito no está declarado, make no sabe que debe esperarlo.

11. Errores frecuentes

ErrorQué pasaCorrección
Sangrar la receta con espaciosMakefile:2: *** missing separator. Stop.Un carácter tabulador al comienzo de cada línea de la receta. Configurar el editor para no convertirlo.
No listar los encabezados como requisitosmake dice up to date y el binario usa valores viejos.Listarlos, o generar las dependencias con gcc -MMD (tema 36).
Un cd en una línea de la receta y el comando en la siguienteCada línea es un shell nuevo: el cd no persiste.cd dir && orden en la misma línea.
Usar $VAR del shell en una recetamake lo interpreta como $V seguido de AR, y queda vacío.Duplicar el signo: $$VAR.
Objetivo clean sin .PHONYSi existe un archivo clean, la regla no corre..PHONY: clean.
Un Makefile donde el objetivo principal no es el primeromake sin argumentos ejecuta otra cosa (clean, por ejemplo).Poner all (o el programa) arriba.
Que la receta no cree el objetivo que declaraSe ejecuta en cada make, porque el archivo nunca aparece.La receta debe producir exactamente $@.
Poner las bibliotecas en CFLAGS o antes de los objetosundefined reference (tema 32).Las bibliotecas en LDLIBS, al final de la línea de enlace.
Esperar que el entorno cambie una variable ya definida en el MakefileSe usa el valor del Makefile.?= en el Makefile, o pasar la variable en la línea de órdenes.
Confundir = con :=Una variable se expande con un valor posterior, o una referencia circular provoca Recursive variable references itself.:= cuando se quiere el valor en ese momento.
Declarar un requisito que no existe ni se puede construirNo rule to make target 'x', needed by 'y'.Revisar el nombre y la ruta; make -n o --debug=b muestran el camino.
Usar -j con dependencias incompletasFallos intermitentes: un objetivo usa algo que otro todavía no generó.Declarar todos los requisitos; probar con make -j8 desde cero.
Mezclar wildcard *.c con archivos que no forman el programaSe compilan y se enlazan por error, con multiple definition de main, por ejemplo.Lista explícita de fuentes, o separar las pruebas en otro directorio.

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué tres partes tiene una regla de Makefile y qué hace cada una?

El objetivo es lo que se construye; los requisitos son los archivos de los que depende; la receta son las órdenes (cada una iniciada por un tabulador) que producen el objetivo. La receta se ejecuta si el objetivo no existe o algún requisito es más nuevo.

2. ¿Qué construye make si se ejecuta sin argumentos?

El primer objetivo del Makefile (el objetivo por defecto). Por eso el objetivo principal, all o el programa, se pone al principio.

3. ¿Cómo decide make si debe rehacer un objetivo?

Compara fechas de modificación: si el objetivo no existe, o alguno de sus requisitos tiene una fecha posterior, ejecuta la receta. Antes construye recursivamente los requisitos que a su vez sean objetivos. Si nada cambió, avisa is up to date.

4. El Makefile muestra missing separator. ¿Qué suele ser?

Una línea de la receta sangrada con espacios en lugar de un tabulador (o una línea que make no reconoce como regla ni como asignación). Se corrige poniendo un tabulador real al comienzo de cada línea de receta.

5. Se cambió un valor en util.h y make dice up to date. ¿Por qué, y cómo se arregla?

Porque make no analiza los #include, y el Makefile no declara el encabezado como requisito de los objetos que lo usan. Se arregla agregándolo a los requisitos o, mejor, generando las dependencias con el compilador (gcc -MMD, tema 36).

6. ¿Por qué un cd en una línea de la receta no afecta a la línea siguiente?

Porque make ejecuta cada línea en un shell distinto. Para que el directorio cuente, el cd y la orden deben ir en la misma línea (cd dir && orden).

7. ¿Qué hacen un @ y un - al comienzo de una línea de receta?

@ evita que make muestre la orden antes de ejecutarla; - hace que se ignore un fallo de esa orden y continúe con las siguientes.

8. ¿Cómo se usa una variable de shell, como $f de un bucle, dentro de una receta?

Duplicando el signo: $$f. make procesa primero los $; con dos, entrega uno solo al shell.

9. ¿Qué valen $@, $< y $^ en prog: main.o numeros.o texto.o?

$@ es prog, $< es main.o (el primer requisito) y $^ es main.o numeros.o texto.o (todos).

10. ¿Qué diferencia hay entre =, :=, ?= y +=?

= expande el valor cada vez que se usa; := lo expande en el momento de la asignación; ?= asigna solo si la variable no estaba definida; += agrega al valor existente.

11. ¿Qué gana: la variable definida en el Makefile, en el entorno o en la línea de órdenes?

La línea de órdenes (make CFLAGS=-O2) gana a todo; después, el Makefile; después, el entorno (salvo que se use make -e); y por último los valores incorporados de make.

12. ¿Qué es una regla de patrón y qué hace %.o: %.c?

Es una regla genérica donde % representa cualquier raíz de nombre. %.o: %.c dice que cualquier X.o se obtiene de X.c, con una misma receta; dentro, $< es el .c y $@ el .o.

13. Hay un hola.c y ningún Makefile. ¿Qué hace make hola y qué variables influyen?

Usa su regla implícita y ejecuta algo como cc hola.c -o hola. Consulta CC, CFLAGS, CPPFLAGS, LDFLAGS y LDLIBS, que pueden definirse en la línea de órdenes.

14. ¿Para qué sirve .PHONY: clean?

Para declarar que clean es una acción y no un archivo. Sin la declaración, si existe un archivo llamado clean, make lo considera actualizado y no ejecuta la receta.

15. ¿Cómo se ve qué haría make sin ejecutarlo, y cómo se ve por qué rehace un objetivo?

make -n imprime las órdenes sin ejecutarlas; make --debug=b explica, por ejemplo, Prerequisite 'numeros.c' is newer than target 'numeros.o'. Con -p se ven todas las reglas y variables.

16. ¿Por qué make -j puede revelar errores en un Makefile que funciona en serie?

Porque en paralelo ejecuta a la vez todo lo que no dependa entre sí según lo declarado. Si falta declarar un requisito, en serie el orden podía salir bien por casualidad, y en paralelo un objetivo corre antes de que exista lo que necesita.

Para practicar

  1. Escribe a mano el Makefile de la versión 1 para tu propio proyecto con tres módulos. Prueba make, make otra vez, y touch sobre cada archivo para ver qué se rehace.
  2. Quita un encabezado de los requisitos, cambia una constante en él y comprueba que el binario queda viejo. Después restáuralo y compáralo con make --debug=b.
  3. Pasa el Makefile a la versión 3 con variables, regla de patrón y wildcard. Agrega un cuarto módulo y verifica que no hay que tocar el Makefile.
  4. Usa make CFLAGS="-O2 -g" y make CC=clang (si lo tienes) sin editar el archivo. Comprueba con make -n las líneas resultantes.
  5. Agrega los objetivos all, clean y run (que ejecuta el programa), todos con .PHONY. Crea un archivo llamado run y verifica que sigue funcionando.
  6. Con el sleep del ejemplo de -j, mide el tiempo con -j1, -j2 y -j3. ¿Qué pasa con -j8 si hay solo tres objetivos?
  7. Agrega al Makefile la construcción de la biblioteca estática del tema 32 (ar rcs) y el enlace del programa con ella en LDLIBS. ¿Dónde tienen que ir -L y -l?
  8. Ejecuta make -p -f /dev/null | less y busca las reglas incorporadas para %.o: %.c, %: %.c y para archivos de lex/yacc. ¿Qué otros lenguajes conoce make sin que se le diga?