Files
printer-driver/docs/06-make.md
T
2026-10-02 11:36:59 +02:00

8.3 KiB

Make

Qué hacía Go por mí

go build hace tres cosas sin que las pidas:

  1. Lee los import y deduce qué paquetes dependen de cuáles.
  2. Recompila solo lo que cambió, usando la caché de $GOCACHE, que va por hash del contenido.
  3. Llama al compilador y al enlazador con los flags correctos para cada plataforma (GOOS/GOARCH).

En C no pasa nada de eso. gcc compila lo que le pases y nada más. make es la herramienta clásica para escribir a mano ese grafo de dependencias.

El modelo: un grafo de ficheros con fechas

Un Makefile es una lista de reglas:

objetivo: prerrequisitos
	receta        # OJO: la línea empieza por TABULADOR, no por espacios
  • Objetivo: el fichero que se quiere producir, por ejemplo build/escpos.o.
  • Prerrequisitos: los ficheros de los que depende.
  • Receta: los comandos de shell que producen el objetivo.

Es como una receta de cocina: plato: ingredientes y, debajo, los pasos.

build/hello: build/hello.o build/escpos.o
	$(CC) build/hello.o build/escpos.o -o build/hello
Parte Qué es Pregunta que responde
build/hello (antes de :) Objetivo: el fichero que sale. No es el nombre de un comando. ¿Qué quiero tener?
build/hello.o build/escpos.o (después de :) Prerrequisitos: los ficheros que necesita. ¿Qué hace falta antes?
línea con tabulador Receta: el comando. ¿Cómo se fabrica?

Make rehace un objetivo si no existe o si algún prerrequisito tiene una fecha de modificación más reciente que él. No usa hashes como Go: solo compara fechas (mtime). Por eso un touch basta para forzar una recompilación.

Make no entiende la receta. Para Make, la receta es texto que pasa a la shell sin leerlo. Que ponga -Iinclude no le dice nada: -I es un flag de gcc, no de Make. Make solo mira la línea objetivo: prerrequisitos. Si un fichero no aparece ahí, sus cambios no provocan ninguna recompilación.

El objetivo debe llamarse como el fichero que produce la receta. Si la regla se llama link/all pero genera build/hello, Make busca un fichero link/all. Como nunca existe, ejecuta la receta siempre.

Se declaran dependencias, no pasos. No hace falta que una receta llame a make para construir otras cosas (make[1]: Entering directory... indica que se está lanzando otro Make). Basta con escribir de qué depende cada objetivo, y Make deduce el orden:

all: build/hello                           # sin receta: solo "depende de"
build/hello: build/hello.o build/escpos.o  # enlazar
build/hello.o: examples/hello.c include/escpos.h
build/escpos.o: src/escpos.c include/escpos.h

En Go es la diferencia entre llamar a go build paquete por paquete desde un script y dejar que go build resuelva los imports.

Make va hacia atrás

Make no ejecuta el Makefile de arriba abajo como un script. Empieza por el objetivo que le pides (o el primero del fichero si no pides ninguno) y va tirando de lo que necesita:

all
 └─ build/hello                  (enlazar)
     ├─ build/hello.o            ← examples/hello.c + escpos.h
     └─ build/escpos.o           ← regla patrón: src/escpos.c + escpos.h
  1. Le pides all, que necesita build/hello.
  2. build/hello necesita build/hello.o y build/escpos.o.
  3. Para cada prerrequisito busca una regla que lo fabrique y repite lo mismo, hasta llegar a ficheros que ya existen (examples/hello.c, src/escpos.c).
  4. Después construye de las hojas hacia arriba. Antes de cada receta compara fechas: si el objetivo existe y es más nuevo que todos sus prerrequisitos, no lo rehace.

El orden de las reglas en el fichero da igual. Solo importa que all sea la primera, porque es la que se construye por defecto.

Nadie le dice a Make "compila escpos.o": llega ahí porque build/hello lo pide. Si un objetivo no aparece como prerrequisito de nada, Make no lo construye.

Piezas que vas a necesitar

Pieza Qué es
CC = gcc Variable. Se usa como $(CC).
CFLAGS, CPPFLAGS, LDFLAGS Nombres convencionales: flags de compilación, del preprocesador (-Iinclude) y del enlazado.
$@ Nombre del objetivo de la regla.
$< Primer prerrequisito.
$^ Todos los prerrequisitos.
build/%.o: src/%.c Regla patrón: sirve para cualquier .c → .o.
.PHONY: all clean Objetivos que no son ficheros. Sin esto, si existiera un fichero llamado clean, make clean no haría nada.
$(wildcard src/*.c) Lista de ficheros que existen.
$(patsubst src/%.c,build/%.o,$(SRCS)) Transforma una lista de nombres.

.PHONY: objetivos que no son ficheros

clean y all son nombres de acciones, no ficheros. Pero Make no lo sabe: si alguien crea un fichero llamado clean, la regla (sin prerrequisitos) queda "al día" y make clean responde 'clean' is up to date sin borrar nada. Comprobado con touch clean.

.PHONY: all clean

Con esto Make no busca ningún fichero con esos nombres y ejecuta la receta siempre.

En proyectos Go Make se usa como lanzador de tareas (migrate, test, lint, run...). Ninguna produce un fichero, y hasta build se delega en go build, que tiene su propia caché y conoce las dependencias mejor que Make. Por eso allí todo va en .PHONY y la comparación de fechas no se usa nunca. En C es al revés: Make es el sistema de compilación, y solo las acciones (all, clean) son phony.

Variables desde la línea de comandos

make CC=clang sobrescribe la variable CC del Makefile solo para esa ejecución. Pero Make no se da cuenta de que han cambiado el compilador o los flags, porque solo compara fechas de ficheros. Si los .o están al día, no hace nada, y si recompila alguno, mezcla .o de gcc con .o de clang. Cuando cambies compilador o flags: make clean y construir de cero.

En Go los flags y la versión del compilador forman parte del hash de la caché, así que go build sí recompila lo necesario.

Regla patrón = función con parámetros

Una regla patrón es como una función de Go, y $@ y $< son sus parámetros:

build/%.o: src/%.c
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
// Equivalente mental en Go
func buildObject(target, source string) { // target = $@, source = $<
    run("gcc -std=c17 ... -c " + source + " -o " + target)
}

Cuando Make necesita build/escpos.o:

  1. Busca una regla para build/escpos.o. No hay ninguna escrita a mano.
  2. Prueba la regla patrón build/%.o. Encaja, con % = escpos.
  3. Calcula el prerrequisito: src/%.c → src/escpos.c.
  4. "Llama a la función": $@ = build/escpos.o, $< = src/escpos.c.
  5. Sustituye las variables, imprime el comando ya sustituido y lo ejecuta.

Por eso en la salida de make nunca se ve $<: Make imprime el comando después de sustituir, igual que fmt.Printf imprime los valores y no %s.

Otro ejemplo, para build/buffer.o:

Valor Ojo
% buffer Solo la parte que varía: sin carpeta ni extensión.
$@ build/buffer.o El objetivo completo, con su carpeta.
$< src/buffer.c El prerrequisito completo, con su carpeta.

La regla patrón es una plantilla, no compila "todo lo que hay en src". Make solo la usa cuando algún objetivo pide un build/ALGO.o. Si creas src/buffer.c y nadie pide build/buffer.o, no se compila.

El problema de las cabeceras

Si escpos.c incluye escpos.h y cambias solo el .h, make no lo sabe: la regla build/%.o: src/%.c no menciona el .h. Resultado: un .o compilado con la versión vieja de la cabecera. Es un error silencioso que en Go no existe.

La solución es que gcc genere las dependencias por ti:

  • -MMD hace que, al compilar x.c, gcc escriba también x.d, un fragmento de Makefile con las cabeceras que incluyó.
  • -MP añade reglas vacías para cada cabecera, para que borrar un .h no rompa el build.
  • -include $(DEPS) carga esos .d en el Makefile. El - evita el error si todavía no existen.

Windows y Linux en el mismo Makefile

En MSYS2, la variable de entorno OS vale Windows_NT. En Linux no está definida.

ifeq ($(OS),Windows_NT)
    EXE = .exe
else
    EXE =
endif

Más adelante (Fase 4) aquí irán también las librerías que cambian según la plataforma, como -lws2_32 para Winsock en Windows.