This commit is contained in:
2026-09-30 11:49:39 +02:00
parent 3b458fcd7f
commit 6e33293ea6
10 changed files with 382 additions and 11 deletions
+136
View File
@@ -0,0 +1,136 @@
# 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**:
```make
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:
```make
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`.
```make
.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.
```make
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.