8.9 KiB
Make
Qué hacía Go por mí
go build hace tres cosas sin que las pidas:
- Lee los
importy deduce qué paquetes dependen de cuáles. - Recompila solo lo que cambió, usando la caché de
$GOCACHE, que va por hash del contenido. - 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
- Le pides
all, que necesitabuild/hello. build/hellonecesitabuild/hello.oybuild/escpos.o.- 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). - 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.
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 && makepara verlo todo otra vez.touch src/buffer.c && makepara recompilar solo ese fichero.- Añadir
-WerroraCFLAGS: convierte los avisos en errores. Así no se genera el.oy el aviso sale en cadamakehasta 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:
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:
- Busca una regla para
build/escpos.o. No hay ninguna escrita a mano. - Prueba la regla patrón
build/%.o. Encaja, con%=escpos. - Calcula el prerrequisito:
src/%.c→src/escpos.c. - "Llama a la función":
$@=build/escpos.o,$<=src/escpos.c. - 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:
-MMDhace que, al compilarx.c, gcc escriba tambiénx.d, un fragmento de Makefile con las cabeceras que incluyó.-MPañade reglas vacías para cada cabecera, para que borrar un.hno rompa el build.-include $(DEPS)carga esos.den 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.