Files
printer-driver/docs/06-make.md
T
2026-09-30 11:49:39 +02:00

5.4 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.

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 recorre el grafo desde el objetivo que le pides (o el primero del fichero) hacia sus prerrequisitos, en profundidad.

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.

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.