Files
2026-10-06 13:23:15 +02:00

250 lines
9.8 KiB
Markdown

# 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.
Es como una receta de cocina: *plato: ingredientes* y, debajo, los pasos.
```make
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:
```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 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`.
```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.
### Los avisos solo salen al compilar
Un *warning* aparece **solo cuando ese `.c` se compila**. Como el `.o` sí se
genera, el siguiente `make` lo ve al día, no recompila, y el aviso no vuelve a
salir. Es fácil no verlo nunca. Hay tres formas de evitarlo:
- `make clean && make` para verlo todo otra vez.
- `touch src/buffer.c && make` para recompilar solo ese fichero.
- Añadir `-Werror` a `CFLAGS`: convierte los avisos en errores. Así no se
genera el `.o` y el aviso sale en cada `make` hasta que lo arreglas.
**En Go** no pasa: lo que en C son avisos importantes, en Go son errores.
### 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:
```make
build/%.o: src/%.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
```
```go
// 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.
```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.
### La «magia .exe» de MSYS2
Sin `EXE`, el Makefile con objetivo `build/hello` parece funcionar en Windows:
el segundo `make` dice `Nothing to be done`. No es mérito del Makefile:
- `make`, `ls` y la shell de `/usr/bin` son programas de MSYS2, que emula
POSIX encima de Windows.
- Si un programa de MSYS2 pregunta por `build/hello` y solo existe
`build/hello.exe`, la capa de emulación responde que existe y le da la fecha
del `.exe`.
| Comando | Qué ve |
|---|---|
| `ls -l build/hello` | Pasa por la emulación: «existe». |
| `cmd //c dir build` | Pregunta a Windows: solo hay `hello.exe`. |
Con un make nativo (`mingw32-make`) esa emulación no existe: no encuentra
`build/hello` y vuelve a enlazar cada vez. Por eso se pone el nombre real con
`$(EXE)`.
**Comparación con Go:** `go build` añade `.exe` por sí mismo cuando
`GOOS=windows`. En C, el nombre del ejecutable lo decides tú en el Makefile.