227 lines
8.9 KiB
Markdown
227 lines
8.9 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.
|