1809 lines
59 KiB
Markdown
1809 lines
59 KiB
Markdown
# 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 <dir>` 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
|
|
<receta> ← ¡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<dir>` | Agrega `<dir>` al path del linker | `-L./lib` |
|
|
| `-l<nombre>` | Busca `lib<nombre>.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 <dir>`.
|
|
|
|
## 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 <dir>`** 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 <dir>`, `.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 <dir>` 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 `<generated.h>`.
|
|
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**.
|
|
|