# Capítulo 21 — Makefiles: de "compilar a mano" a proyectos profesionales
> Capítulo avanzado, pero **escrito para que se entienda de verdad**. Se apoya en funciones (`05`),
> arreglos y cadenas (`06`), punteros (`07`), structs (`08`), memoria dinámica (`09`) y archivos
> (`10`). También asume que ya compilaste un programa con `gcc`.
>
> Este capítulo no es solo sobre `make`: es sobre **automatizar tu flujo de trabajo**. Cada vez que
> repetís el mismo comando dos veces, algo anda mal. Acá vas a aprender a dejar que una herramienta
> se encargue de compilar, enlazar y construir bibliotecas.
---
## Cómo leer este capítulo
Este es el capítulo donde "compilar a mano" se convierte en "**la máquina compila por mí**". Ya no vas
a escribir la línea de gcc cada vez. Vas a escribir **las reglas** y dejar que una herramienta lo haga.
Está dividido en **ocho partes**. Después de cada concepto importante hay uno o varios ejercicios
cortos. **No los saltees**: son la mitad del capítulo.
| Parte | De qué habla | La idea en una frase |
|---|---|---|
| **1** | El problema: compilar a mano | Cada cambio obliga a recompilar todo con el mismo comando largo: rápido, repetitivo, propenso a error. |
| **2** | Makefile básico | `make` ejecuta tus reglas: variables, targets, dependencias y recipes. |
| **3** | Patrones y automático | `%`, `$@`, `$^`: escribir una regla que sirve para cualquier `.c`. |
| **4** | Variables avanzadas | `+=`, `-=` , `?=` : configurar tu build sin copiar-pastear. |
| **5** | Bibliotecas estáticas | `ar` y `ranlib`: empaquetar objetos en un archivo que el linker usa como si fuera código. |
| **6** | Bibliotecas dinámicas | `gcc -shared -fPIC`: crear `.so`/`.dll` que se cargan al ejecutar, no al compilar. |
| **7** | Makefiles profesionales | Include condicional, sanitizers, targets implícitas y variables integradas de gcc. |
| **8** | Integrador multi-subdirectorio | Proyectos con `lib/`, `tests/` y `$(MAKE) -C
` para orquestar builds anidados. |
| **9** | Instalación y configuraciones | `PREFIX`, `make install/uninstall`, feature toggles (`RENDERER=sdl|opengl|vulkan`), recursos propios. |
Los **ejercicios** se numeran en orden: `21.1`, `21.2`, `21.3`, y así. Cada uno trae su **escenario de
prueba** (qué hacés y qué tiene que pasar) para que lo ejecutes y compruebes.
> **Regla de oro del libro:** el capítulo explica y muestra **cómo funciona** cada cosa (con comandos
> y Makefile reales), pero **los ejercicios no traen la solución**. El enunciado te da el problema; la
> respuesta la escribís vos.
---
# PARTE 1 — El problema: compilar a mano
Empecemos por lo que ya hacés (o harías) hoy. Y por qué eso se vuelve un problema rápido.
## 1.1 "Compilar con gcc" no es una vez, es siempre
La primera vez que compilas algo, el comando parece inocente:
```bash
$ gcc main.c -o programa
```
Funciona. Pero ahora imaginá esto: tenés un proyecto con **diez archivos `.c`** y tres de ellos
cambiaron desde la última compilación. ¿Qué hacés?
Opción A — recompilar todo:
```bash
$ gcc main.c file1.c file2.c utils.c helper.c parser.c config.c network.c io.c mathops.c \
-o programa -Wall -Wextra -I./include -L./lib -lmylib -lm
```
Opción B — recompilar solo lo que cambió (si te acordás cuáles):
```bash
$ gcc main.c file1.c file2.c utils.c helper.c parser.c config.c network.c io.c mathops.c \
-o programa -Wall -Wextra -I./include -L./lib -lmylib -lm
```
Sí, misma opción. Porque **gcc no sabe qué archivos cambiaron**: vos tenés que decirle cuáles
recompilar y cuáles reusar. Y la línea de arriba tiene **doce archivos**, **seis banderas**, **tres
flags de linker**. Si te olvidás de uno, el programa no compila bien. Si escribís mal `-Wall`, no
ves advertencias nuevas.
Y esto es lo peor: **cada vez que hagas un cambio**, tenés que ejecutar esa línea otra vez. O una
variante si querés optimizar para producción (`-O2`). O con sanitizers para debugging
(`-fsanitize=address,undefined`). El comando se multiplica y el error humano entra por la puerta grande.
> **La analogía del chef.** Compilar un programa a mano es como ser chef, lavaplatos y mozo al mismo
> tiempo. Cada vez que cocinás algo nuevo, tenés que recordar exactamente qué ingredientes usaste,
> en qué orden, con qué fuego, y repetir el proceso cada vez que alguien pida lo mismo. Un sistema de
> build es como un **chef asistente** que se acuerda de todo: "solo los platos que cambiaron necesitan
> prepararse de nuevo".
## 1.2 ¿Por qué `make`? ¿No hay alternativas?
Sí las hay: `cmake`, `ninja`, `bazel`, `meson`, `just`. Pero `make` es **universal**: está en Linux,
macOS (con un `brew install make`), Windows (MSYS2/MinGW). Es el **estándar mínimo** que todo programador
C debería conocer. Las otras herramientas son capas encima de `make` o para proyectos más grandes:
```text
cmake → genera Makefiles (o Ninja, o Xcode, o Visual Studio projects)
ninja → ejecuta reglas muy rápido (usa un formato propio)
bazel → multiproyecto, multi-lenguaje, con cache de build
just → reemplazo moderno de make con syntax más limpia
```
Conocer `make` no significa que siempre uses `make`. Significa que **entendés el concepto de
"reglas de build"**, que es la idea central. Las otras herramientas comparten esa misma idea: tenés
**dependencias**, un **target** y una **receta**. Si entendés eso, podés migrar a cualquier otra cosa.
## 1.3 La filosofía de `make`
Antes de escribir la primera regla, entiende la **pregunta central** que `make` se hace:
> **"¿Este target necesita reconstruirse?"**
Y responde con un solo algoritmo:
```text
¿el archivo del target existe? → no → ejecutar la receta
¿la fecha de alguna dependencia es más reciente que el target? → sí → ejecutar la receta
¿todas las dependencias son más viejas? → no → SKIP (ya actualizado)
```
Es **todo o nada**: si una dependencia cambió, reconstruís. Si no, no hacés nada. Este "no hacer
nada" es lo que hace a `make` tan rápido en proyectos grandes: solo se toca lo necesario.
> **Eso de "fecha más reciente"** es la clave. `make` no sabe qué código cambiaste; mira los **timestamps**
> de los archivos. Si modificaste `main.c`, su timestamp se actualizó. Si el ejecutable tiene un
> timestamp anterior, `make` decide que hay que recompilar. Simple y efectivo.
---
# PARTE 2 — Makefile básico: tu primera regla
Empezamos con lo más simple posible. Un solo archivo `.c`, un comando de compilación. Sin optimizaciones,
sin sanitizers. El equivalente a `gcc main.c -Wall -o programa` pero dentro de un Makefile.
## 2.1 La estructura mínima de un Makefile
Un **Makefile** (o `makefile`, que es lo mismo) es un archivo de texto con una sintaxis muy estricta:
```text
target : dependencias
← ¡ESTE SPACIO ES UN TABULADOR, NO ESPACIOS!
```
Cada línea de receta **debe empezar con un carácter TAB**. No espacios. Si usás espacios, `make` va a
decirte:
```text
Makefile:3: *** missing separator. Stop.
```
Ese error es el primero que todo el mundo ve y **no significa** "tenés un error de sintaxis": significa
"la línea de arriba empieza con espacios, poné un TAB".
Acá tenés tu primer Makefile, llamado `Makefile` (sin extensión):
```makefile
CC = gcc
CFLAGS = -Wall -Wextra
TARGET = programa
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJS)
[TAB]$(CC) $(OBJS) -o $(TARGET)
%.o: %.c
[TAB]$(CC) $(CFLAGS) -c $< -o $@
clean:
[TAB]rm -f $(OBJS) $(TARGET)
```
Vamos a desglosar cada línea. **No es mucho**: son cuatro conceptos y seis líneas de receta.
### Concepto 1: variables
```makefile
CC = gcc
CFLAGS = -Wall -Wextra
```
`CC` es la variable del compilador (por convención; `make` ya trae `CC = gcc` como default, pero es
bueno declararlo explícitamente). `CFLAGS` es donde van las banderas del compilador. La razón por la que
las declarás como variables: **podés cambiarlas todas desde un solo lugar**, y también **sobreescribirlas
desde la línea de comandos**:
```bash
$ make CFLAGS="-Wall -Wextra -O2" # sobreescribís CFLAGS para este invocation
$ CC=clang make # usás clang en vez de gcc
```
### Concepto 2: target `all`
```makefile
all: $(TARGET)
```
`all` es el **target por defecto**. Cuando ejecutás `make` sin argumentos, ese es el primer target que
se construye. Es una convención universal: si alguien ve tu Makefile, espera encontrar un `all`.
### Concepto 3: la regla de compilación
```makefile
$(TARGET): $(OBJS)
[TAB]$(CC) $(OBJS) -o $(TARGET)
```
Esta regla dice: "**para construir** `$TARGET` (que es el ejecutable), necesito los objetos en `$OBJS`.
Si cualquier `.o` está más nuevo que el `.o`, ejecutá la receta." La receta es el linker: junta todos
los `.o` y produce el ejecutable.
### Concepto 4: la regla de patrón
```makefile
%.o: %.c
[TAB]$(CC) $(CFLAGS) -c $< -o $@
```
Esta es la **regla de patrón**: dice "cualquier `.o` se construye a partir de un `.c` del mismo nombre,
con estas banderas". `%` es un comodín: si el target es `main.o`, `%` es `main`; si es `utils.o`, `%`
es `utils`.
Y `$<` y `$@` son **variables automáticas** (las vemos en detalle más abajo):
| Variable | Qué significa |
|---|---|
| `$@` | El target de la regla (`$@` = `main.o`) |
| `$<` | La primera dependencia (`$<` = `main.c`) |
| `$^` | Todas las dependencias separadas por espacio |
Con estas tres variables automáticas, **una sola regla de patrón te sirve para cualquier archivo `.c`** del
proyecto. No necesitás escribir una regla por cada uno.
### Concepto 5: `clean`
```makefile
clean:
[TAB]rm -f $(OBJS) $(TARGET)
```
`clean` **no es un target real**: no produce ningún archivo que se llame `clean`. Es un target "de
mantenimiento", una orden manual. Por eso existe `.PHONY` (lo vemos en la Parte 7): le dice a `make`
que `clean` no genera un archivo, así si alguien crea un archivo llamado `clean` por error, `make` igual
ejecuta la receta.
### La regla de los archivos: `SRCS` y `OBJS`
```makefile
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)
```
La segunda línea usa **sustitución de patrones**: reemplaza `.c` por `.o` en la lista de `SRCS`. El
resultado es:
```text
OBJS = main.o utils.o parser.o
```
Esto es **automático**: si agregás un nuevo archivo a `SRCS`, `OBJS` se actualiza solo. No tenés que
tocar nada más.
## 2.2 Cómo se usa `make`
```bash
$ make # construye el primer target (all → programa)
$ make clean # borra objetos y ejecutable
$ make CFLAGS="-Wall -O2" # sobreescribís las banderas solo para este invocation
```
Cuando ejecutás `make`, ve qué archivos cambiaron, recompila lo que hace falta y enlaza todo. Si no
cambió nada:
```bash
$ make
make: 'programa' is up to date.
```
> **Momento ajá.** Esa frase —"is up to date"— es el momento más satisfactorio de la programación. Es
> la máquina diciéndote "no hice nada porque no había que hacer nada". En proyectos grandes, eso puede
> significar "30 segundos de ahorro en vez de 5 minutos de compilación".
## Práctica de la Parte 2
### Ejercicio 21.1 — El Makefile mínimo
**Conceptos** · variables (`CC`, `CFLAGS`), target `all`, regla de patrón, `$@`, `$<`, `clean`
**Contexto** · Tenés un programa simple con tres archivos: `main.c`, `utils.c` y `parser.c`. Cada uno
tiene su propia función. El ejecutable se llama `palabra`. Es una simplificación: no hay includes ni
librerías externas.
**Consigna** · Creá un archivo `Makefile` en el mismo directorio que tus fuentes, con:
1. Variables `CC`, `CFLAGS = -Wall -Wextra`, `TARGET = palabra`, y `SRCS` listando los tres archivos `.c`.
2. Target `all` que depende de `$(TARGET)`.
3. La regla de enlace (`$(TARGET): $(OBJS)`).
4. La regla de patrón para `.o → .c`.
5. El target `clean`.
Después:
```bash
$ make clean # borra cualquier cosa vieja
$ make # compila todo desde cero
$ ./palabra # ejecutá el resultado
```
**Entrada** · Ninguna (el programa imprime algo al arrancar).
**Salida** · El Makefile funciona: `make` compila y enlaza sin errores, y `make clean` borra todo.
**Escenario de prueba**
```text
$ make
gcc -Wall -Wextra -c main.c -o main.o
gcc -Wall -Wextra -c utils.c -o utils.o
gcc -Wall -Wextra -c parser.c -o parser.o
gcc main.o utils.o parser.o -o palabra
$ ./palabra
Hola desde palabra
$ make # segunda vez: nada que hacer
make: 'palabra' is up to date.
$ make clean
rm -f main.o utils.o parser.o palabra
```
**Identificadores sugeridos** · `Makefile`, `TARGET`, `SRCS`, `OBJS`
**Restricciones/pistas**
- Usá `$@` y `$<` en las reglas de patrón. No escribas los nombres a mano.
- `clean` debe borrar todos los `.o` **y** el ejecutable.
**Criterios de aceptación**
- [ ] `make` compila los tres archivos y genera el ejecutable sin errores.
- [ ] `make` segunda vez dice "is up to date" (no recompila nada).
- [ ] `make clean` borra todos los `.o` y el ejecutable.
- [ ] Se usa `$@` y `$<` en la regla de patrón.
---
### Ejercicio 21.2 — El error del espacio vs tabulador
**Conceptos** · sintaxis de Makefile, caracteres de tabulación, diagnóstico del error "missing separator"
**Contexto** · Este es el primer y más común error al escribir un Makefile. Si lo entendés desde ya,
no te va a frustrar la primera vez (que sí va a pasar).
**Consigna** · Creá un Makefile con **espacios en vez de TABs** en las recetas (copiá el ejercicio 21.1
pero reemplazá los TAB por cuatro espacios). Compilalo con `make` y **leé el mensaje de error completo**.
Después corregilo cambiando los espacios por un TAB real y comprobá que funcione.
Escribí un párrafo corto explicando:
1. Cuál fue el mensaje exacto.
2. Por qué `make` exige un TAB y no permite espacios (una razón histórica o de parsing).
3. Cómo podés asegurarte de que tu editor usa TABs en los Makefiles.
**Entrada** · Ninguna.
**Salida** · El Makefile corregido + párrafo explicativo.
**Escenario de prueba**
```text
$ make
Makefile:8: *** missing separator. Stop.
```
**Restricciones/pistas**
- La mayoría de los editores modernos permiten configurar "insert spaces instead of tabs" por tipo
de archivo. Usá esa configuración para `Makefile`.
- Si tu editor no lo soporta, podés verificar el Makefile con:
```bash
$ cat -A Makefile | grep '^\s' # los TAB aparecen como ^I
```
**Criterios de aceptación**
- [ ] El mensaje de error está citado textualmente.
- [ ] La explicación del "por qué TAB" es correcta (historia o parsing).
- [ ] Se documentó al menos una forma de prevenir el error en tu editor.
- [ ] El Makefile corregido funciona: `make` compila sin errores.
---
# PARTE 3 — Patrones y variables automáticas
Ya escribiste un Makefile que funciona para tres archivos. Pero ¿qué pasa cuando tenés **cincuenta**?
Acá entran las reglas de patrón y las variables automáticas, que son la razón por la que `make` no se
vuelve un archivo de 2000 líneas.
## 3.1 La regla de patrón: una para todas
La regla de patrón usa `%` como comodín:
```makefile
%.o: %.c
[TAB]$(CC) $(CFLAGS) -c $< -o $@
```
Esto le dice a `make`: "**siempre** que necesite construir un archivo que termine en `.o` a partir de
un archivo con el mismo nombre pero `.c`, usá esta receta." No importa cuántos archivos `.c` tengas:
una regla de patrón los cubre todos.
Y hay reglas de patrón para **cualquier extensión**:
```makefile
%.o: %.s # ensamblador → objeto
[TAB]$(CC) $(ASFLAGS) -c $< -o $@
%.html: %.md # markdown → html (para documentación, por ejemplo)
[TAB]pandoc $< -o $@
```
## 3.2 Las variables automáticas completas
`$@`, `$<` y `$^` son las tres más usadas. Pero hay más:
| Variable | Qué significa | ¿Cuándo usarla |
|---|---|---|
| `$@` | El target de la regla | Siempre que necesites saber "¿qué estoy construyendo?" |
| `$<` | La primera dependencia (ordenada) | Cuando necesitás solo una dependencia (ej. un `.c`) |
| `$^` | **Todas** las dependencias, ordenadas y sin duplicados | Cuando necesitás listar todo (ej. varios `.o` para enlazar) |
| `$?` | Dependencias **más recientes** que el target | Para compilar solo lo que cambió dentro de la misma receta |
| `$*` | La parte del comodín (`%`) | Cuando necesitás saber "¿qué archivo es?" (ej. `main.o` → `main`) |
Ejemplo con `$^` en la regla de enlace:
```makefile
$(TARGET): $(OBJS)
[TAB]$(CC) $^ -o $@ $(LDLIBS)
```
### Ejemplo con múltiples dependencias
Imaginate una regla donde el target tiene **varias** dependencias:
```makefile
programa: main.o utils.o parser.o libmath.a
[TAB]$(CC) $^ -o $@ -lm # $^ = main.o utils.o parser.o libmath.a
```
`$^` expande a `main.o utils.o parser.o libmath.a` — **todas las dependencias**. `$?` sería solo las
más recientes. Y `$@` es `programa`.
## 3.3 Reglas implícitas: `make` ya sabe mucho
Antes de escribir reglas, sabé que `make` tiene **reglas implícitas** (también llamadas "built-in rules")
para compilaciones comunes. Por ejemplo, `make` **ya sabe** que para construir `main.o` necesita `main.c`:
```text
Regla implícita de make:
%.o: %.c
$(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
```
Esto significa que, en su forma más simple, podés tener un Makefile **vacío**:
```makefile
# ¡Makefile vacío! make igual compila todos los .o y enlaza.
all: programa
programa: main.o utils.o parser.o
[TAB]$(CC) $^ -o $@
```
O incluso, si solo tenés un `main.c`:
```bash
$ touch Makefile # Makefile vacío
$ make # make usa la regla implícita para main.o y después enlaza
```
Pero las reglas implícitas **no usan** tus variables personalizadas (`CFLAGS`, etc.) ni tus banderas
custom. Por eso siempre es mejor escribir las reglas explícitamente: tenés control total.
> **Para guardar.** Las reglas implícitas son un "escape hatch" para proyectos triviales de un solo
> archivo, pero en cualquier proyecto real **escribí las reglas explícitas**. Porque necesitás `-Wall`,
> `-Wextra`, `-I./include` y `$(LDFLAGS)`.
## 3.4 El doble punto: dependencias sin archivo
Ya vimos `.PHONY`. Pero hay otros "targets especiales" que empiezan con `.` y no generan un archivo:
| Target | Qué significa |
|---|---|
| `.PHONY` | Lista de targets que **no son archivos** (como `clean`, `all`, `install`) |
| `.SUFFIXES` | Lista de extensiones reconocidas (para reglas implícitas) |
| `.DEFAULT` | Target por defecto si no se encuentra ninguno (raro, casi nunca usado) |
`.PHONY` es importante: le dice a `make` que esos targets **siempre deben ejecutarse**, incluso si
existe un archivo con ese nombre. Si no lo declarás y alguien crea un archivo llamado `clean`, `make`
no va a ejecutar la receta (porque cree que ya está "actualizado").
```makefile
.PHONY: all clean install
```
> **La trampa del Makefile sin `.PHONY`.** Es el bug silencioso más común: creás un archivo llamado
> `clean`, `all` o `install` (por accidente, por un script, por copiar-paste), y de repente `make
> clean` no hace nada. La solución: **declarar siempre** `.PHONY`.
## Práctica de la Parte 3
### Ejercicio 21.3 — Reglas de patrón para ensamblador
**Conceptos** · reglas de patrón, `$@`, `$<`, `$^`, variables automáticas
**Contexto** · A veces necesitás compilar código en ensamblador (por performance o porque una librería
externa lo distribuye así). Hacés las reglas necesarias para que `make` compile `.s → .o`.
**Consigna** · Tenés un proyecto con:
- `main.c` — código C principal
- `utils.c` — código C auxiliar
- `vector.s` — una función en ensamblador (asumí que tiene una función exportada `vec_add`)
Creá las reglas necesarias para compilar todo y generar el ejecutable `mixto`:
1. Regla de patrón `%.o: %.c`
2. Regla de patrón `%.o: %.s` con una variable `ASFLAGS`
3. Regla de enlace que usa `$^`
Asegurate de declarar `.PHONY: all clean`.
**Entrada** · Los archivos fuente (`main.c`, `utils.c`, `vector.s`).
**Salida** · El Makefile completo y el ejecutable `mixto` funcionando.
**Escenario de prueba**
```text
$ make
gcc -Wall -Wextra -c main.c -o main.o
gcc -Wall -Wextra -c utils.c -o utils.o
nasm -f elf64 -g -F dwarf vector.s -o vector.o
gcc main.o utils.o vector.o -o mixto
$ ./mixto
Vector sum result: 42
```
**Identificadores sugeridos** · `Makefile`, `ASFLAGS`, `MIXTO`
**Restricciones/pistas**
- No necesitás escribir el ensamblador real: asumí que existe y tiene la función `vec_add`.
- Usá `$^` en la regla de enlace.
**Criterios de aceptación**
- [ ] Existe una regla de patrón para `.s → .o`.
- [ ] Se usa `$^` en la regla de enlace.
- [ ] `.PHONY` incluye `all` y `clean`.
- [ ] El Makefile compila todo y genera el ejecutable.
---
### Ejercicio 21.4 — Entender las variables automáticas
**Conceptos** · `$@`, `$<`, `$^`, `$?`, `$*`, diagnóstico con `echo`
**Contexto** · Las variables automáticas son poderosas, pero si no sabés qué devuelve cada una,
escribís recetas que parecen magia. Este ejercicio te obliga a **ver lo que hacen**.
**Consigna** · Escribí un Makefile con **una regla de patrón simple** y otra regla con múltiples
dependencias. En cada receta, usá `echo` para imprimir el valor de **cada variable automática**:
```makefile
%.o: %.c
[TAB]@echo "[$@] target"
[TAB]@echo "[$<] first prerequisite"
[TAB]@echo "[$^] all prerequisites"
[TAB]@echo "[$?] newer prerequisites"
[TAB]@echo "[$*] stem (pattern match)"
[TAB]$(CC) $(CFLAGS) -c $< -o $@
programa: main.o utils.o parser.o
[TAB]@echo "[$@] target: programa"
[TAB]@echo "[$^] all prereqs: $(words $^) items"
[TAB]$(CC) $^ -o $@
```
El `@` al inicio de la línea evita que `make` imprima el comando antes de ejecutarlo (solo ves el
resultado del `echo`).
Ejecutá `make clean && make` y **documentá la salida completa**: qué imprimió cada variable,
y por qué.
**Entrada** · Un Makefile con `echo` en las recetas + algunos `.c`.
**Salida** · La salida de `make`, línea por línea, explicada.
**Escenario de prueba**
```text
$ make clean && make
rm -f *.o programa
gcc -Wall -Wextra -c main.c -o main.o
[main.o] target
[main.c] first prerequisite
[main.c] all prerequisites
[main.c] newer prerequisites
[main] stem (pattern match)
...
```
**Restricciones/pistas**
- El `@` al inicio de la línea hace que `make` **no imprima el comando**, solo el resultado del `echo`.
Sin el `@`, cada `echo` se imprime dos veces: una vez el comando mismo y otra su salida.
**Criterios de aceptación**
- [ ] Cada variable automática (`$@`, `$<`, `$^`, `$?`, `$*`) se imprime y explica.
- [ ] La regla con múltiples dependencias muestra cómo `$^` los junta todos.
- [ ] Se nota la diferencia entre `$?` (solo las nuevas) y `$^` (todas).
---
# PARTE 4 — Variables avanzadas de Makefile
Ya sabés que `CC = gcc` y `CFLAGS = -Wall`. Pero un Makefile real tiene **decenas** de variables, y saber
cuáles usar y cómo combinarlas es lo que separa un Makefile básico de uno profesional.
## 4.1 Tipos de asignación
Hay cuatro formas de asignar variables, y cada una tiene un comportamiento distinto:
| Operador | Qué hace | ¿Se puede sobreescribir desde la CLI? |
|---|---|---|
| `=` | Asignación simple (expandida cuando se usa) | Sí (`make CC=clang`) |
| `:=` | Asignación inmediata (expandida en el momento) | Sí |
| `?=` | Condicional: solo asigna si **no está definida** | Solo si no viene de la CLI |
| `+=` | Concatena al final del valor actual | Sí |
### `=` vs `:=`: cuándo importa la diferencia
```makefile
A = $(B) # expande $(B) cuando se usa A (lazy)
B = hello
C := $(B) # expande $(B) ahora mismo, aunque B cambie después
# Si B cambia después...
B = world
all:
[TAB]@echo "A = $(A)" # A = world (expandió B cuando se usó A)
[TAB]@echo "C = $(C)" # C = hello (ya estaba fijada con :=)
```
En la práctica, **`:=` es más seguro** para variables que dependen de otras: sabés exactamente qué valor
van a tener. Pero `=` es más flexible cuando querés que las variables se resuelvan al final, después de
que todas fueron definidas.
### `?=` : el default configurable
```makefile
CC ?= gcc # Usá gcc por defecto, pero si el usuario pasó CC=clang, respetalo
CFLAGS ?= -Wall -Wextra # mismo principio
```
¿Por qué importa? Porque la CLI tiene prioridad sobre `?=`:
```bash
$ make CC=clang # clang gana porque viene de la línea de comandos
$ make # gcc es el default (no vino nada por CLI)
```
### `+=` : construir listas
```makefile
CFLAGS += -std=c11
CFLAGS += -pedantic
# CFLAGS = -Wall -Wextra -std=c11 -pedantic
```
Cada `+=` **agrega al final**. Es perfecto para ir construyendo una lista de banderas sin reescribir
todas las anteriores.
## 4.2 Variables que ya vienen con `make` (y las que gcc espera)
Hay variables que `make` conoce por defecto, y otras que el **compilador** conoce por convención. Es
bueno distinguir ambas:
### Variables de `make`
| Variable | Default | Descripción |
|---|---|---|
| `CC` | `gcc` | El compilador C |
| `CXX` | `g++` | El compilador C++ |
| `AR` | `ar` | El archivador (para bibliotecas estáticas) |
| `RM` | `rm -f` | El comando de borrado (`clean` usa esto) |
### Variables que gcc espera (por convención)
| Variable | Qué va adentro | Ejemplo |
|---|---|---|
| `CFLAGS` | Banderas del compilador C | `-Wall -Wextra -O2` |
| `CPPFLAGS` | Banderas del preprocesador | `-I./include -DDEBUG=1` |
| `LDFLAGS` | Banderas del linker | `-L./lib -rpath=/opt/lib` |
| `LDLIBS` | Librerías para el linker | `-lm -lpthread -lmylib` |
> **Fijate la diferencia entre `LDFLAGS` y `LDLIBS`.** `LDFLAGS` son opciones del linker (dónde buscar
> librerías, dónde buscar paths). `LDLIBS` son las librerías en sí (el `-l...`). Es una convención de GNU:
> separás la **ubicación** (`-L`) de los **nombres** (`-l`). Si mezclás ambas cosas en una sola variable,
> el Makefile se vuelve confuso.
## 4.3 La receta del linker: el orden importa
Cuando enlazás un programa con bibliotecas, el **orden** de los argumentos del linker importa:
```bash
$ gcc main.o utils.o -lm -o programa # correcto: bibliotecas AL FINAL
$ gcc -lm main.o utils.o -o programa # a veces funciona, a veces no
```
La regla general es simple: **las bibliotecas van al final**. El linker procesa los archivos de
izquierda a derecha: si una biblioteca aparece antes que el código que la necesita, no la encuentra.
### La receta completa del linker
En un Makefile profesional, la regla de enlace se ve así:
```makefile
$(TARGET): $(OBJS)
[TAB]$(CC) $^ -o $@ $(LDFLAGS) $(LDLIBS)
```
Desglosado:
| Parte | Qué es | Ejemplo de contenido |
|---|---|---|
| `$(CC)` | El compilador (gcc, clang, etc.) | `gcc` |
| `$^` | Todos los objetos | `main.o utils.o parser.o` |
| `-o $@` | Nombre del ejecutable.
| `$^` | Todos los archivos objeto y dependencias | `main.o utils.o parser.o libfoo.a` |
| `$(LDFLAGS)` | Opciones del linker | `-L./lib -rpath=/opt/lib` |
| `$(LDLIBS)` | Librerías para el linker | `-lm -lpthread -lmylib` |
> **La receta profesional completa.** Un Makefile de producción se parece a esto:
```makefile
CC = gcc
CFLAGS := -Wall -Wextra -std=c11 -O2 -g
CPPFLAGS += -I./include -DVERSION="\"1.0.0\""
LDFLAGS += -L./lib
LDLIBS += -lm -lpthread
TARGET = miapp
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
.PHONY: all clean release debug
all: $(TARGET)
build/%.o: src/%.c | build
[TAB]$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
$(TARGET): $(OBJS)
[TAB]$(CC) $^ -o $@ $(LDFLAGS) $(LDLIBS)
build:
[TAB]mkdir -p build
debug: CFLAGS += -fsanitize=address,undefined -DDEBUG
debug: clean all
release: CFLAGS += -O3 -flto
release: clean all
clean:
[TAB]$(RM) -r build $(TARGET)
```
Este Makefile incluye: `:=` para variables principales, `+=` para banderas adicionales, regla de patrón genérica
que compila cualquier `.c` de `src/` a un `.o` en `build/`, order-only prerequisite (`| build`) para crear
directorios, targets condicionales (`debug` y `release`), y sanitizers.
> **Truco de laboratorio: `make -n` o `make --dry-run`.** Antes de ejecutar cualquier receta, ejecutá
> `make -n`. Imprime los comandos que SE EJECUTARÍA sin ejecutarlos. Úsalo antes de cambiar recetas.
## 4.4 El Makefile ideal: resumen completo
```makefile
CC ?= gcc
CFLAGS ?= -Wall -Wextra -std=c11
AR ?= ar
RM ?= rm -f
TARGET = programa
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst src/%.c, build/%.o, $(SRCS))
CPPFLAGS += -I./include
LDFLAGS += -L./lib
LDLIBS += -lm
.PHONY: all clean install
all: $(TARGET)
build/%.o: src/%.c
[TAB]$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
$(TARGET): $(OBJS)
[TAB]$(CC) $^ -o $@ $(LDFLAGS) $(LDLIBS)
clean:
[TAB]$(RM) -rf build/ $(TARGET)
```
Las claves: `?=` para CC/CFLAGS (sobreescribible), `:=` para TARGET/SRCS/OBJS, `+=` para banderas extra,
regla de patrón genérica y `.PHONY`. Este esqueleto es tu punto de partida — en las siguientes partes
añadimos bibliotecas estáticas, dinámicas y subdirectorios.
---
# PARTE 5 — Bibliotecas estáticas
Una biblioteca estática (`.a` en Linux/macOS, `.lib` en Windows) es un **archivo ZIP** que contiene
múltiples archivos objeto (`.o`) con un índice para que el linker encuentre cualquier símbolo. Al
enlazar contra una biblioteca estática, el linker **copia** el código necesario en tu ejecutable.
## 5.1 ¿Qué es una biblioteca estática?
Si escribiste `mathops.c`, `vector.c` y `matrix.c` con funciones reutilizables, querés que otros
programadores las usen **sin recompilar tu código cada vez**. La solución: empaquetar los `.o` en un `.a`:
```bash
gcc -c mathops.c vector.c matrix.c -I./include
ar rcs libmath.a mathops.o vector.o matrix.o
ar t libmath.a # muestra: mathops.o vector.o matrix.o
```
La herramienta `ar` (archiver) usa tres opciones siempre juntas:
| Opción | Qué hace |
|---|---|
| `r` | Reemplaza o agrega archivos (crea si no existe) |
| `c` | No genera mensaje si el archivo es nuevo |
| `s` | Escribe el índice (esencial para que el linker encuentre símbolos) |
> **Para guardar.** `ar rcs` es siempre las mismas cuatro letras. Sin `s`, el linker no encuentra símbolos.
## 5.2 Cómo usar una biblioteca estática
```bash
gcc main.c -o programa -L./lib -lmath # forma con -L y -l
gcc main.o -o programa ./lib/libmath.a # equivalente directo
```
| Opción | Qué significa | Ejemplo |
|---|---|---|
| `-L` | Agrega `` al path del linker | `-L./lib` |
| `-l` | Busca `lib.a` o `.so` | `-lmath` busca `libmath.a` |
> **La convención:** las bibliotecas se llaman siempre `libNOMBRE.a`. El linker busca `lib` + nombre + `.a`/`.so`.
## 5.3 Biblioteca estática en el Makefile
```makefile
STATIC_LIB = libmath.a
LIB_SRCS := mathops.c vector.c matrix.c
LIB_OBJS := $(patsubst %.c,build/%.o,$(LIB_SRCS))
.PHONY: all clean static
all: $(TARGET) $(STATIC_LIB)
$(STATIC_LIB): $(LIB_OBJS)
[TAB]$(AR) rcs $@ $^
build/%.o: src/%.c
[TAB]$(CC) $(CFLAGS) -c $< -o $@
clean:
[TAB]$(RM) -rf build/ $(TARGET) $(STATIC_LIB)
```
> **Fijate que `$(AR)` usa `$^`.** Esto ejecuta `ar rcs libmath.a a.o b.o c.o` en una línea.
## Práctica de la Parte 5
### Ejercicio 21.5 — Crear una biblioteca estática
**Conceptos** · `ar`, `ar rcs`, bibliotecas estáticas, `-L`, `-l`
**Consigna** · Con archivos `mathops.c`, `vector.c`, `matrix.c`:
1. Regla de patrón para compilar `.c` → `.o` en `build/`.
2. Target `$(STATIC_LIB)` con `ar rcs $@ $^`.
3. Target `all` que construye ejecutable + biblioteca.
4. Target `clean` que borra todo.
**Escenario de prueba**
```text
$ make clean && make
gcc -Wall -Wextra -c src/mathops.c -o build/mathops.o
gcc -Wall -Wextra -c src/vector.c -o build/vector.o
gcc -Wall -Wextra -c src/matrix.c -o build/matrix.o
ar rcs libmath.a build/mathops.o build/vector.o build/matrix.o
$ ar t libmath.a
build/mathops.o
build/vector.o
build/matrix.o
```
**Identificadores sugeridos** · `STATIC_LIB`, `LIB_SRCS`, `LIB_OBJS`
**Criterios de aceptación**
- [ ] `libmath.a` se crea con `ar rcs`.
- [ ] `ar t libmath.a` muestra los tres objetos.
- [ ] Un programa externo enlaza con `-L. -lmath`.
---
# PARTE 6 — Bibliotecas dinámicas
Una biblioteca dinámica (`.so` en Linux/macOS, `.dll` en Windows) se **carga en memoria** al ejecutar.
El código no se copia en tu ejecutable: se comparte entre todos los procesos que lo usan.
## 6.1 ¿Qué es una biblioteca dinámica?
```text
BIBLIOTECA ESTÁTICA (.a) BIBLIOTECA DINÁMICA (.so)
Código copiado en el ejecutable Código fuera, en un .so
Mayor tamaño del binario Menor tamaño del binario
No necesita el .a al ejecutar Necesita el .so al ejecutar
Un solo proceso por copia Compartida entre procesos
```
## 6.2 Compilar una biblioteca dinámica
```bash
gcc -fPIC -c mathops.c vector.c matrix.c # Paso 1: objetos position-independent
gcc -shared -o libmath.so mathops.o vec.o # Paso 2: enlazar como .so
```
La bandera `-fPIC` (Position Independent Code) es **obligatoria** para `.so`. Sin ella, el linker falla con
"relocation R_X86_64_32 against `...` can not be used when making a shared object".
## 6.3 Ejecutar con bibliotecas dinámicas
```bash
LD_LIBRARY_PATH=. ./miapp # Forma 1: variable de entorno
gcc main.c -o miapp -L. -lmath -Wl,-rpath,'$ORIGIN' # Forma 2: rpath embebido
ldd miapp # Ver dependencias
```
> **`$ORIGIN`** = directorio donde vive el ejecutable. La forma más robusta de rutas relativas.
## 6.4 Biblioteca dinámica en el Makefile
```makefile
DYNAMIC_LIB = libmath.so
LIB_OBJS := $(patsubst %.c,build/%.o,$(LIB_SRCS))
$(DYNAMIC_LIB): $(LIB_OBJS)
[TAB]$(CC) -shared -o $@ $^
build/%.o: src/%.c
[TAB]$(CC) $(CFLAGS) -fPIC -c $< -o $@ # -fPIC obligatorio para .so
```
Sin `-fPIC` y `-shared`, no tenés una biblioteca dinámica.
## Práctica de la Parte 6
### Ejercicio 21.6 — Enlazar contra una biblioteca estática vs dinámica
**Conceptos** · `-L`, `-l`, `.a` vs `.so`, `ldd`, resolución de símbolos
**Contexto** · Ya sabés crear bibliotecas estáticas (`.a`) y ahora vas a entender cómo cambia el
enlace cuando usás una biblioteca dinámica (` .so`). Este ejercicio te obliga a compilar y ejecutar
el mismo programa contra ambas versiones.
**Consigna** · Tenés dos bibliotecas: `libmath.a` (estática) y `libmath.so` (dinámica). Escribí un
Makefile con dos targets que generen el **mismo ejecutable** enlazado de forma distinta:
1. `./miapp-static` → enlazado contra `libmath.a`
2. `./miapp-dynamic` → enlazado contra `libmath.so` con rpath
Después, usá `ls -lh`, `ldd` y `time` para comparar ambos ejecutables.
**Entrada** · Un solo `main.c` que llama a funciones de `libmath`.
**Salida** · Dos ejecutables con tamaños distintos, y un análisis comparativo.
**Escenario de prueba**
```text
$ make static dynamic
gcc -Wall -Wextra -c main.c -o main.o
gcc main.o libmath.a -o miapp-static
gcc main.o -L. -lmath -Wl,-rpath,'$ORIGIN' -o miapp-dynamic
$ ls -lh miapp-*
-rwxr-xr-x 1 user user 20K miapp-static # Más grande (código copiado)
-rwxr-xr-x 1 user user 16K miapp-dynamic # Menor (enlace diferido)
$ ldd miapp-dynamic | grep math
libmath.so => ./libmath.so (0x...)
$ ldd miapp-static | grep math
(ninguno — no depende de nada)
```
**Identificadores sugeridos** · `STATIC_LIB`, `DYNAMIC_LIB`, `miapp-static`, `miapp-dynamic`
**Restricciones/pistas**
- No escribas la librería ni el main: asumí que ya existen.
- El rpath es clave para que el ejecutable dinámico encuentre la `.so`.
**Criterios de aceptación**
- [ ] `miapp-static` se construye con `gcc main.o libmath.a -o miapp-static`.
- [ ] `miapp-dynamic` usa `-L. -lmath -Wl,-rpath,'$ORIGIN'`.
- [ ] `ldd miapp-static` no muestra dependencias de libmath.
- [ ] `ldd miapp-dynamic` muestra `libmath.so => ...`.
---
### Ejercicio 21.7 — Crear una biblioteca dinámica
**Conceptos** · `-fPIC`, `-shared`, `.so`, `ldd`, `LD_LIBRARY_PATH`
**Consigna** · Con el proyecto del ejercicio 21.5, modificá el Makefile para:
1. Genere objetos con `-fPIC`.
2. Cree `libmath.so` con `-shared`.
3. El ejecutable enlaza contra la `.so` con rpath (`-Wl,-rpath,'$ORIGIN'`).
**Escenario de prueba**
```text
$ make clean && make
gcc -Wall -Wextra -fPIC -c src/mathops.c -o build/mathops.o
gcc -shared -o libmath.so build/mathops.o ...
ldd miapp # muestra: libmath.so => ./libmath.so (0x...)
```
**Criterios de aceptación**
- [ ] Se compila con `-fPIC`.
- [ ] `libmath.so` existe y `ldd` lo reporta como dependencia.
- [ ] El programa ejecuta sin error "cannot open shared object file".
---
### Ejercicio 21.8 — Estática vs dinámica: medir el impacto
**Consigna** · Con la misma librería compilada como `.a` y como `.so`:
1. Medí tamaño de `libmath.a` vs `libmath.so`.
2. Medí tamaño del ejecutable enlazado contra cada una.
3. Cronometrá ambos programas.
4. Usá `strings`, `nm`, `ldd`.
**Criterios de aceptación**
- [ ] El ejecutable estático es más grande (código copiado).
- [ ] Se explicó el "linker selectivo" (copy-on-link).
- [ ] Ambos programas miden correctamente con `time` o similar.
# PARTE 7 — Makefiles profesionales
Acá juntamos todo en un Makefile que se parece a los de proyectos reales: sanitizers, targets debug/release,
dependencias automáticas y variables consultadas al compilador.
## 7.1 Sanitizers en desarrollo, optimización en producción
```makefile
.PHONY: all clean debug release test coverage
debug: CFLAGS += -fsanitize=address,undefined -g -O0 -DDEBUG
debug: $(TARGET)
release: CFLAGS += -O3 -flto -DNDEBUG
release: clean all
test: debug
[TAB]./$^ --test # asumiendo que el programa soporta --test
coverage: CFLAGS += --coverage -fprofile-arcs -ftest-coverage
coverage: $(TARGET)
[TAB]gcov *.c
```
Cada target redefine `CFLAGS` **solo para sí mismo** con `target: CFLAGS += ...`.
## Variables consultadas al compilador
```makefile
COMPILER_VERSION := $(shell $(CC) -dumpversion)
info:
[TAB]@echo "Compiler: $(CC) ($(COMPILER_VERSION))"
[TAB]@echo "CFLAGS: $(CFLAGS)"
[TAB]@echo "TARGET: $(TARGET)"
```
> **Truco: `make info`.** Imprime toda la configuración del build. El primer target que todo Makefile serio
> debería tener.
## Generación automática de dependencias
Modificar un `.h` no recompila los `.c` que lo incluyen. Solución: generar reglas de dependencia automáticas:
```bash
gcc -MMD -MP -c main.c -o main.o # genera main.d además de main.o
```
```makefile
DEPS := $(OBJS:.o=.d)
-include $(DEPS)
```
`-MMD` genera el archivo `.d` con dependencias. `-MP` agrega reglas vacías para cabeceras (evita errores si borrás un header).
> **Para guardar.** `gcc -MMD -MP` soluciona: "compilé un `.c` que depende de un `.h`, pero el `.h` cambió y make no lo supo".
---
### Ejercicio 21.9 — Makefile profesional completo
**Consigna** · Construí un Makefile con targets: `all`, `debug`, `release`, `clean`, `info`, `test`.
Debe incluir bibliotecas estática Y dinámica, rpath, `-MMD -MP`, y sanitizers en debug.
**Criterios de aceptación**
- [ ] `make debug` compila con sanitizers activados.
- [ ] `make info` imprime toda la configuración del build.
- [ ] Los archivos `.d` se generan e incluyen con `-include $(DEPS)`.
- [ ] Bibliotecas estática y dinámica se crean correctamente.
---
### Ejercicio 21.10 — Depuración de dependencias rotas
**Consigna** · Con el Makefile del ejercicio 21.9:
1. Compilá con `make debug`.
2. Borrá los `.d` generados (`rm build/*.d`).
3. Modificá un header incluido.
4. ¿Qué pasó al ejecutar `make`? ¿Por qué?
5. Restablecé y verificá que make detecta el cambio en `.h`.
**Criterios de aceptación**
- [ ] Se explicó el efecto de borrar los `.d`.
- [ ] Se explica `-MP` con ejemplo concreto.
---
# PARTE 8 — Integrador final: proyecto multi-subdirectorio
Acá juntamos todo en un proyecto real: múltiples subdirectorios, bibliotecas estáticas, dinámicas, y un
Makefile que construye todo desde cero con `make`.
## 8.1 Estructura del proyecto
```text
mi-proyecto/
├── Makefile # Makefile raiz (orquesta todo)
├── include/ # Headers publicos
│ ├── mathops.h
│ ├── vector.h
├── src/ # Codigo fuente principal
│ ├── main.c
│ └── utils.c
├── lib/ # Bibliotecas de la aplicacion
│ ├── Makefile # Makefile para compilar librerias
│ ├── mathops.c
│ ├── vector.c
├── tests/ # Pruebas unitarias
│ ├── Makefile
│ └── test_mathops.c
└── build/ # Objetos compilados (generado)
```
Cada subdirectorio tiene su propio Makefile. El raiz orquesta todo con `$(MAKE) -C `.
## 8.2 Makefile de la biblioteca (lib/Makefile)
```makefile
CC = gcc
CFLAGS := -Wall -Wextra -std=c11 -I../include
%.o: %.c
[TAB]$(CC) $(CFLAGS) -fPIC -c $< -o $@
libmath.a: mathops.o vector.o
[TAB]$(AR) rcs $@ $^
libmath.so: mathops.o vector.o
[TAB]$(CC) -shared -o $@ $^
clean:
[TAB]$(RM) -f *.o libmath.a libmath.so
```
## 8.3 Makefile raiz (Makefile)
```makefile
CC ?= gcc
CFLAGS ?= -Wall -Wextra -std=c11 -O2 -g
TARGET = miapp
LIB_DIR = lib
BUILD_DIR = build
.PHONY: all clean debug release libs test
all: $(TARGET)
$(BUILD_DIR)/%.o: src/%.c
[TAB]@mkdir -p $(BUILD_DIR)
[TAB]$(CC) $(CFLAGS) -Iinclude -c $< -o $@
libs:
[TAB]$(MAKE) -C $(LIB_DIR)
$(TARGET): libs
[TAB]$(CC) $(BUILD_DIR)/*.o -o $@ -L$(LIB_DIR) -lmath -Wl,-rpath,'$ORIGIN'
debug: CFLAGS += -fsanitize=address,undefined -g -O0
debug: clean all
release: CFLAGS += -O3 -flto -DNDEBUG
release: clean all
test: libs
[TAB]$(MAKE) -C tests
info:
[TAB]@echo "CC = $(CC)"
[TAB]@echo "CFLAGS = $(CFLAGS)"
[TAB]@echo "TARGET = $(TARGET)"
clean:
[TAB]$(RM) -rf $(BUILD_DIR)/ $(TARGET)
[TAB]$(MAKE) -C $(LIB_DIR) clean || true
[TAB]$(MAKE) -C tests clean || true
```
> **`$(MAKE) -C `** cambia al directorio y ejecuta `make`. Es la forma estándar de builds recursivos.
> El `-C` es una bandera de GNU make, no del compilador.
## 8.4 Makefile de tests (tests/Makefile)
```makefile
CC = gcc
CFLAGS := -Wall -Wextra -std=c11 -g
TESTS = test_mathops
all: $(TESTS)
test_%: %.c ../lib/libmath.a
[TAB]$(CC) $(CFLAGS) $< -L../lib -lmath -o $@
clean:
[TAB]$(RM) -f $(TESTS)
```
---
# PARTE 9 — Instalación, configuraciones y recursos
Ya construiste bibliotecas estáticas, dinámicas, targets de debug/release y dependencias automáticas.
Pero un proyecto profesional **se instala** en el sistema del usuario: binarios en `/usr/local/bin`,
bibliotecas en `/usr/local/lib`, headers en `/usr/local/include` y recursos (iconos, datos) en
`/usr/local/share`. Además, suele ofrecer **opciones de compilación**: "¿querés soporte para Vulkan o
SDL? Elegí al hacer `make`".
Esta parte cubre todo eso: cómo instalar, desinstalar, elegir backends desde la línea de comandos y
distribuir recursos propios.
## 9.1 Variables de directorio (`PREFIX`, `BINDIR`, etc.)
La convención de Unix define variables estándar que todos los Makefiles serios respetan. Tu usuario las
puede sobreescribir desde la línea de comandos:
```makefile
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/lib
INCLUDEDIR?= $(PREFIX)/include
DATADIR ?= $(PREFIX)/share/miapp
MANPAGEDIR?= $(PREFIX)/share/man/man1
.PHONY: all install uninstall clean
```
El `?=` significa que el usuario puede cambiar cualquier directorio sin modificar el Makefile:
```bash
$ make PREFIX=/opt/miapp BINDIR=/opt/miapp/bin install
$ make PREFIX=$HOME/.local install # instalación local, sin root
```
## 9.2 El target `install`
El target `install` copia los archivos a sus destinos. Usa `mkdir -p` para crear directorios y
`\cp` (copia segura) o `install` (el comando del sistema):
```makefile
install: $(TARGET)
[TAB]@mkdir -p $(BINDIR)
[TAB]@mkdir -p $(LIBDIR)
[TAB]@mkdir -p $(INCLUDEDIR)
[TAB]@mkdir -p $(DATADIR)
[TAB]$(INSTALL_BIN) $(TARGET) $(BINDIR)/$(TARGET)
[TAB]$(INSTALL_LIB) libmath.a $(LIBDIR)/libmath.a
[TAB]$(INSTALL_LIB) libmath.so $(LIBDIR)/libmath.so
[TAB]@install -m 644 include/*.h $(INCLUDEDIR)/
[TAB]@cp -r resources/* $(DATADIR)/
```
> **`$(INSTALL_BIN)`, `$(INSTALL_LIB)`**. No uses `cp` a secas. GNU Make define por defecto:
> `INSTALL = cp`. Pero el target `install` del sistema (el comando `/usr/bin/install`) maneja
> permisos y tipos correctamente. Puedes usarlo así:
> ```makefile
> INSTALL_BIN ?= $(INSTALL) -D -m 755
> INSTALL_LIB ?= $(INSTALL) -D -m 644
> ```
> La bandera `-D` crea directorios padres si no existen. `-m 755` = ejecutable, `-m 644` = lectura
> universal.
## 9.3 El target `uninstall` y limpieza de recursos
Cada cosa que `install` crea, `uninstall` la elimina:
```makefile
uninstall:
[TAB]$(RM) $(BINDIR)/$(TARGET)
[TAB]$(RM) $(LIBDIR)/libmath.a
[TAB]$(RM) $(LIBDIR)/libmath.so
[TAB]$(RM) -rf $(DATADIR)
[TAB]@echo "Uninstalled from $(PREFIX)"
```
> **Para guardar.** Si `install` hace algo (ej. actualiza cachés del sistema con `ldconfig`),
> `uninstall` debe revertirlo. El desinstalador es tan importante como el instalador.
## 9.4 Opciones de compilación: feature toggles
Imaginate una aplicación que puede renderizar gráficos usando **múltiples backends**: SDL2, OpenGL o
Vulkan. En vez de obligar al usuario a editar el Makefile a mano, le das opciones desde la CLI:
```makefile
# Por defecto usa SDL2 si no se especifica nada
RENDERER ?= sdl
ifeq ($(RENDERER),sdl)
[TAB]CFLAGS += -DRENDERER_SDL -I/usr/include/SDL2
[TAB]LDLIBS += -lSDL2
[TAB]@echo ">> Building with SDL2 backend"
endif
ifeq ($(RENDERER),opengl)
[TAB]CFLAGS += -DRENDERER_OPENGL -I/usr/include
[TAB]LDLIBS += -lGL -lm
[TAB]@echo ">> Building with OpenGL backend"
endif
ifeq ($(RENDERER),vulkan)
[TAB]CFLAGS += -DRENDERER_VULKAN -I/usr/include/x86_64-linux-gnu
[TAB]LDLIBS += -lvulkan-1
[TAB]@echo ">> Building with Vulkan backend"
endif
```
El usuario elige al invocar `make`:
```bash
$ make # SDL2 (por defecto)
$ make RENDERER=opengl # OpenGL
$ make RENDERER=vulkan clean # Vulkan (con limpieza previa)
```
### Más ejemplos de feature toggles
Esto no es solo para backends gráficos. Podés activar/desactivar:
| Toggle | Qué habilita | Ejemplo de uso |
|---|---|---|
| `RENDERER=opengl` | Backend gráfico OpenGL | Aplicaciones visuales |
| `ENABLE_TESTS=yes` | Compila y corre tests | Desarrollo activo |
| `LTO=yes` | Link-time optimization | Producción máxima performance |
| `DEBUG_SYMBOLS=yes` | Símbolos de depuración | Debugging |
| `FEATURE_X=yes` | Una funcionalidad concreta | Plugins, módulos opcionales |
```makefile
# Ejemplo: habilitar LTO si el usuario lo pide
ifeq ($(LTO),yes)
[TAB]CFLAGS += -flto -ffat-lto-objects
[TAB]@echo ">> LTO enabled"
endif
# Ejemplo: tests compilados pero no ejecutados por defecto
ifeq ($(ENABLE_TESTS),yes)
[TAB]CFLAGS += -DTESTING_ENABLED
[TAB]OBJS += tests/main.o
[TAB]test-targets: $(TARGETS)
[TAB][TAB]$(MAKE) -C tests
endif
```
### Headers condicionales
Las opciones de compilación se reflejan en el código con `#ifdef`:
```c
#ifdef RENDERER_SDL
renderer_init_sdl();
#elif defined(RENDERER_OPENGL)
renderer_init_opengl();
#elif defined(RENDERER_VULKAN)
renderer_init_vulkan();
#else
#error "No renderer selected. Use: make RENDERER=sdl|opengl|vulkan"
#endif
```
El `#error` es la forma en que el Makefile fuerza al usuario a elegir un backend si no especificó
ninguno (cuando `RENDERER` viene vacío).
## 9.5 Recursos propios: iconos, datos, configuración
Una aplicación instalada suele necesitar archivos adicionales: íconos, temas, archivos de configuración,
modelos 3D, texturas, sonidos, etc. Estos se copian a un directorio de datos durante `make install`:
```makefile
DATADIR ?= $(PREFIX)/share/miapp
install:
[TAB]@mkdir -p $(DATADIR)
[TAB]@cp -r resources/* $(DATADIR)/
[TAB]@echo "Resources installed to $(DATADIR)"
```
Y en el programa, la ruta a los recursos se construye con una macro del preprocesador:
```makefile
CPPFLAGS += -DRES_DIR="\"$(DATADIR)\""
```
```c
const char *resource_path(const char *filename) {
static char path[1024];
snprintf(path, sizeof(path), "%s/%s", RES_DIR, filename);
return path;
}
```
> **Para guardar.** Si los recursos cambian después de la instalación, el usuario solo necesita
> volver a ejecutar `make install` (no `make` completo). El Makefile debe distinguir entre "compilar"
> y "instalar datos".
## 9.6 El Makefile de producción: resumen final
```makefile
# ─── Variables configurables por el usuario ───
CC ?= gcc
PREFIX ?= /usr/local
RENDERER ?= sdl
ENABLE_TESTS ?= no
LTO ?= no
BINDIR := $(PREFIX)/bin
LIBDIR := $(PREFIX)/lib
INCLUDEDIR:= $(PREFIX)/include
DATADIR := $(PREFIX)/share/miapp
MANPAGEDIR:= $(PREFIX)/share/man/man1
INSTALL_BIN ?= $(INSTALL) -D -m 755
INSTALL_LIB ?= $(INSTALL) -D -m 644
# ─── Banderas del compilador ───
CFLAGS := -Wall -Wextra -std=c11
CPPFLAGS += -I./include -DRES_DIR="\"$(DATADIR)\""
ifeq ($(RENDERER),sdl)
[TAB]CFLAGS += -DRENDERER_SDL -I/usr/include/SDL2
[TAB]LDLIBS += -lSDL2
[TAB]@echo ">> Backend: SDL2"
endif
ifeq ($(RENDERER),opengl)
[TAB]CFLAGS += -DRENDERER_OPENGL
[TAB]LDLIBS += -lGL -lm
[TAB]@echo ">> Backend: OpenGL"
endif
ifeq ($(RENDERER),vulkan)
[TAB]CFLAGS += -DRENDERER_VULKAN
[TAB]LDLIBS += -lvulkan-1
[TAB]@echo ">> Backend: Vulkan"
endif
ifeq ($(LTO),yes)
[TAB]CFLAGS += -flto -ffat-lto-objects
[TAB]@echo ">> LTO enabled"
endif
# ─── Archivos ───
TARGET = renderer
SRCS := $(wildcard src/*.c)
OBJS := $(patsubst %.c,build/%.o,$(SRCS))
.PHONY: all install uninstall clean debug release test info
all: $(TARGET)
build/%.o: src/%.c
[TAB]@mkdir -p build
[TAB]$(CC) $(CFLAGS) -c $< -o $@
$(TARGET): $(OBJS)
[TAB]$(CC) $^ -o $@ $(LDFLAGS) $(LDLIBS)
install: all
[TAB]@mkdir -p $(BINDIR) $(DATADIR)
[TAB]$(INSTALL_BIN) $(TARGET) $(BINDIR)/$(TARGET)
[TAB]@cp -r resources/* $(DATADIR)/
[TAB]@echo "Installed to $(PREFIX)"
uninstall:
[TAB]$(RM) $(BINDIR)/$(TARGET)
[TAB]$(RM) -rf $(DATADIR)
[TAB]@echo "Uninstalled from $(PREFIX)"
debug: CFLAGS += -g -O0 -fsanitize=address,undefined
debug: clean all
release: CFLAGS += -O3 -DNDEBUG
release: clean all
test:
[TAB]$(MAKE) -C tests run
info:
[TAB]@echo "CC = $(CC)"
[TAB]@echo "PREFIX = $(PREFIX)"
[TAB]@echo "RENDERER = $(RENDERER)"
[TAB]@echo "CFLAGS = $(CFLAGS)"
[TAB]@echo "TARGET = $(TARGET)"
clean:
[TAB]$(RM) -rf build/ $(TARGET)
[TAB]$(MAKE) -C tests clean || true
```
Con este Makefile tenés un proyecto que el usuario puede:
1. **Configurar** (`make RENDERER=opengl LTO=yes PREFIX=$HOME/.local`)
2. **Compilar** en debug o release
3. **Instalar** con recursos y binarios en los directorios correctos
4. **Desinstalar** limpiamente
---
## Práctica de la Parte 8
### Ejercicio 21.11 — Proyecto multi-subdirectorio con bibliotecas anidadas
**Conceptos** · `$(MAKE) -C `, `.PHONY`, recursividad, `|| true` en limpieza
**Contexto** · Necesitás un proyecto con tres componentes independientes: una librería matemática (`lib/`),
un programa principal (`src/`) y tests unitarios (`tests/`). Cada componente tiene su propio Makefile.
**Consigna** · Construí la estructura:
```text
mi-proyecto/
├── Makefile # Raíz (orquesta todo)
├── src/
│ ├── main.c
│ └── utils.c
├── lib/
│ ├── Makefile # Compila libmath.a y libmath.so
│ ├── mathops.c
│ └── vector.c
├── include/
│ ├── mathops.h
│ └── vector.h
└── tests/
├── Makefile
└── test_mathops.c
```
El Makefile raíz:
1. Construye la librería con `$(MAKE) -C lib`
2. Compila el programa principal vinculando contra la `.a` con rpath
3. Ofrece targets `debug`, `release`, `test`, `clean` que se propagan a subdirectorios
**Escenario de prueba**
```text
$ make clean && make
make -C lib clean || true
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc build/main.o -Llib -lmath -Wl,-rpath,'$ORIGIN' -o miapp
./miapp
Procesando datos...
$ make test
make -C tests
gcc -Wall -Wextra -std=c11 -g test_mathops.c -L../lib -lmath -o test_mathops
./test_mathops
Running test_add: OK (0.00s)
```
**Identificadores sugeridos** · `miapp`, `LIB_DIR`, `BUILD_DIR`
**Criterios de aceptación**
- [ ] Cada subdirectorio (`lib/`, `tests/`) tiene su Makefile independiente.
- [ ] El Makefile raíz usa `$(MAKE) -C ` para orquestar.
- [ ] `make clean` en el raiz limpia todos los subdirectorios (con `|| true`).
- [ ] El programa ejecuta correctamente contra la librería local.
---
## Práctica de la Parte 9
### Ejercicio 21.12 — Makefile instalable con feature toggles (ejercicio jefe)
**Conceptos** · `PREFIX`, `BINDIR`, `DATADIR`, `make install/uninstall`, `ifeq`, `-D`, `#ifdef`, recursos,
`INSTALL_BIN`, `INSTALL_LIB`, variables de directorio configurables
**Contexto** · Trabajás en un equipo que desarrolla una herramienta de línea de comandos llamada `vox`.
No necesitás implementar la herramienta: su trabajo es crear un Makefile **completamente funcional** para
construir e instalar el proyecto. Los archivos fuente (`.c`, `.h`) y los recursos (`.txt`, `.dat`) ya existen
en la estructura del proyecto; tu objetivo es dominar `make` a nivel de distribución profesional.
La aplicación tiene tres "backends" posibles de procesamiento que se activan con una variable de make:
| Variable | Valor | Qué hace |
|---|---|---|
| `ENGINE` | `ref` | Motor básico en software (por defecto) |
| `ENGINE` | `simd` | Usa instrucciones SIMD (SSE2/AVX2) |
| `ENGINE` | `gpu` | Usa OpenCL para aceleración por GPU |
Además, el programa usa dos bibliotecas externas: `libmath.a` (estática) y `libspectrum.so` (dinámica).
Al instalarse, copia un binario, un header público y recursos de datos a directorios configurables.
**Consigna** · Escribí **solo el Makefile**. No escribas los `.c`, ni las librerías, ni los recursos.
Asumí que existen:
- `src/main.c` — incluye `vox.h` y usa `#ifdef ENGINE_REF` / `#ifdef ENGINE_SIMD` / `#ifdef ENGINE_GPU`
- `src/engine_ref.c`, `src/engine_simd.c`, `src/engine_gpu.c` — implementación de cada motor (condicional)
- `include/vox.h` — header público a instalar
- `lib/libmath.a` — biblioteca estática externa (ya compilada, en el proyecto)
- `lib/libspectrum.so` — biblioteca dinámica externa (ya compilada)
- `resources/config.txt`, `resources/defaults.dat` — archivos de datos
Tu Makefile debe:
1. **Variables configurables**: `PREFIX ?= /usr/local`, `ENGINE ?= ref`. El usuario debe poder hacer
`make ENGINE=simdx86 PREFIX=$HOME/.local install`.
2. **Selección de backend**: usar `ifeq` para activar las banderas correctas:
```makefile
ifeq ($(ENGINE),simd)
CFLAGS += -DENGINE_SIMD -msse2 -mavx2
LDLIBS += -lm
endif
ifeq ($(ENGINE),gpu)
CFLAGS += -DENGINE_GPU
LDLIBS += -lOpenCL
endif
# ENGINE=ref no necesita flags extra (código nativo)
```
3. **Compilación condicional**: compilar solo el engine seleccionado:
```makefile
ifeq ($(ENGINE),simd)
OBJS := src/main.o src/engine_simd.o
else ifeq ($(ENGINE),gpu)
OBJS := src/main.o src/engine_gpu.o
else
OBJS := src/main.o src/engine_ref.o
endif
```
4. **Enlazado**: contra `libmath.a` (estática, ruta directa) y `libspectrum.so` (dinámica, con rpath):
```makefile
vox: $(OBJS) lib/libmath.a
$(CC) $^ -o $@ -Llib -lspectrum -Wl,-rpath,'$ORIGIN/../lib' $(LDLIBS)
```
5. **Instalación**: con `PREFIX` configurable, copiando binario, header y recursos:
```makefile
PREFIX ?= /usr/local
BINDIR := $(PREFIX)/bin
INCLUDEDIR:= $(PREFIX)/include/vox
LIBDIR := $(PREFIX)/lib/vox
DATADIR := $(PREFIX)/share/vox
MANPAGEDIR:= $(PREFIX)/share/man/man1
install: vox
@mkdir -p $(BINDIR) $(INCLUDEDIR) $(LIBDIR) $(DATADIR) $(MANPAGEDIR)
install -D -m 755 vox $(BINDIR)/vox
install -D -m 644 include/vox.h $(INCLUDEDIR)/vox.h
cp lib/libmath.a $(LIBDIR)/libmath.a
cp lib/libspectrum.so $(LIBDIR)/libspectrum.so
cp resources/* $(DATADIR)/
```
6. **Desinstalación**: revertir la instalación:
```makefile
uninstall:
$(RM) $(BINDIR)/vox
$(RM) -rf $(INCLUDEDIR) $(LIBDIR) $(DATADIR) $(MANPAGEDIR)/vox.1*
@echo "Uninstalled from $(PREFIX)"
```
7. **Target `info`**: imprimir toda la configuración activa:
```makefile
info:
@echo "CC = $(CC)"
@echo "PREFIX = $(PREFIX)"
@echo "ENGINE = $(ENGINE)"
@echo "CFLAGS = $(CFLAGS)"
@echo "OBJS = $(OBJS)"
```
8. **Debug / Release**: targets condicionales que redefinen `CFLAGS`:
```makefile
debug: CFLAGS += -g -O0 -fsanitize=address,undefined
debug: clean vox
release: CFLAGS += -O3 -flto -DNDEBUG
release: clean vox
```
**Escenario de prueba esperado** (solo el Makefile se evalúa):
```text
$ make ENGINE=simd info
>> Backend SIMD seleccionado
CC = gcc
PREFIX = /usr/local
ENGINE = simd
CFLAGS = -Wall -Wextra -std=c11 -DENGINE_SIMD -msse2 -mavx2
OBJS = src/main.o src/engine_simd.o
$ make ENGINE=simd PREFIX=$HOME/.local install
gcc ... -c src/main.c -o src/main.o
gcc ... -c src/engine_simd.c -o src/engine_simd.o
gcc src/main.o src/engine_simd.o lib/libmath.a -o vox \
-Llib -lspectrum -Wl,-rpath,'$ORIGIN/../lib' -lm
install -D -m 755 vox $HOME/.local/bin/vox
install -D -m 644 include/vox.h $HOME/.local/include/vox/vox.h
cp lib/libmath.a $HOME/.local/lib/vox/
cp lib/libspectrum.so $HOME/.local/lib/vox/
cp resources/* $HOME/.local/share/vox/
$ make ENGINE=gpu uninstall
rm -f /usr/local/bin/vox
rm -rf /usr/local/include/vox /usr/local/lib/vox /usr/local/share/vox /usr/local/share/man/man1/vox.1*
Uninstalled from /usr/local
```
**Identificadores sugeridos** · `Makefile`, `VOX`, `ENGINE`, `OBJS`
**Restricciones**
- **Solo escribís el Makefile.** No implementes los `.c`, ni compiles librerías externas, ni crees recursos.
- El evaluador revisará que las reglas sean correctas y que todas las variables sean sobreescribibles
desde la CLI.
- Usá `$@`, `$^`, `$<` donde aplique — no escribas nombres de archivo a mano en las recetas.
**Criterios de aceptación**
- [ ] `PREFIX ?= /usr/local` y se respeta en `install`.
- [ ] `ENGINE ?= ref` selecciona el engine correcto con `ifeq`.
- [ ] Se compila solo el `.c` del engine seleccionado (no los tres).
- [ ] El enlace usa `libmath.a` directo + `libspectrum.so` con rpath.
- [ ] `make install` copia binario, header y recursos a los directorios correctos.
- [ ] `make uninstall` elimina todo lo que `install` creó.
- [ ] `make info` imprime configuración activa completa.
- [ ] `make debug` y `make release` redefinen `CFLAGS` correctamente.
- [ ] El usuario puede hacer `make ENGINE=gpu PREFIX=$HOME/.local install`.
---
### Ejercicio 21.13 — Hacer que un header roto no rompa todo
**Conceptos** · `-MMD`, `-MP`, dependencias implícitas, `#include` indirectos, `make clean -a`
**Contexto** · Tu Makefile tiene `-MMD -MP` para generar dependencias automáticas. Pero alguien borra un
archivo `.h` que fue generado por un script externo y no se regeneró. Hacés `make`, el compilador falla,
y ahora tenés que limpiar manualmente.
**Consigna** · Con un Makefile que tenga `-MMD -MP`:
1. Creá un archivo `generated.h` simulado (un `echo "int fake = 42;" > generated.h`).
2. Compilá un `main.c` que incluya ``.
3. Borrá `generated.h` y ejecutá `make` otra vez — ¿qué pasa?
4. Usá `-MP` para agregar una regla vacía a `generated.h.d` (si existe).
5. Intentá `make generated.h` como target phony que regera el archivo.
**Criterios de aceptación**
- [ ] Se explica por qué `make` falla al borrar un header dependiente.
- [ ] Se muestra cómo agregar `generated.h:` como target en el Makefile para regenerarlo.
- [ ] El Makefile permite `make generated.h && make` sin errores.
---
> **Has llegado hasta aquí.** El capítulo más largo del libro. Aprendiste a pasar de "compilar con gcc"
> a tener un sistema de build profesional con bibliotecas estáticas, dinámicas, sanitizers, tests,
> instalación configurada y opciones de compilación. En el próximo capítulo (`22`), vamos a ver cómo
> hacer esto aún mejor con **CMake**.