Makefile
¿Qué es?:
Un Makefile es un archivo de texto, normalmente llamado Makefile y sin extensión,
que contiene un conjunto de reglas para compilar programas y manejar dependencias automáticamente.
Permite:
- Definir qué archivos dependen de otros.
- Especificar cómo generar objetos
(.o)y ejecutables. - Evitar recompilar archivos que no cambiaron.
- Facilitar la limpieza de archivos intermedios con
make clean.
En proyectos de C, se usa junto con el comando make para construir
programas sin tener que escribir manualmente todos los comandos gcc.
Normalmente, desde la terminal compilamos un programa con:
gcc main.c -o main
Y cuando nos adentramos en el lenguaje, aprendemos lo útil que es conocer advertencias que el compilador nos puede indicar o la elección de un estandar, es entonces que nuestro comando de compilación, se amplía a:
gcc -std=c11 -Wall -Wextra -pedantic main.c -o main
Obviamente luego usando un IDE estas instrucciones las pasamos a su configuración si ya no lo están. Pero tenemos otra herramienta importante y muy utilizada como Makefile.
Makefile básico
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic
PROG = main
OBJ = main.o
$(PROG): $(OBJ)
$(CC) $(OBJ) -o $(PROG)
$(OBJ): main.c
$(CC) $(CFLAGS) -c main.c
.PHONY: clean
clean:
del $(OBJ) $(PROG).exe
Se indica:
CCCompiladorCFLAGSOpciones de compilacionPROGNombre del ejecutableOBJ: Objetos que se generarán del source code$(PROG): $(OBJ): Target principal, genera el ejecutablemaina partir demain.o. Simain.ono está actualizado, se recompila primero.$(OBJ): main.c: Regla de compilación de objetos. Creamain.odesdemain.c. Gracias a$(CFLAGS)respeta-Wall -Wextra -pedanticy avisa de warnings importantes.-
Limpieza:
.PHONYAsegura que clean siempre se ejecute aunque haya un archivo llamado cleandel $(OBJ) $(PROG).exeBorra.oy.exepara empezar nuevamente una compilación limpia.
Entonces, la estructura que van a tener nuestros proyectos, por lo general sería la siguiente:
📁 Proyecto
└── 📁 Proyecto-01
├── 📄 main.c
└── 📄 Makefile
Si existen otros archivos .c o .h,
generalmente se ubican en el mismo directorio donde se encuentra
el archivo Makefile
Multiples source code
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic
OBJ = main.o suma.o
programa: $(OBJ)
$(CC) $(OBJ) -o programa
main.o: main.c suma.h
$(CC) $(CFLAGS) -c main.c
suma.o: suma.c suma.h
$(CC) $(CFLAGS) -c suma.c
.PHONY: clean
clean:
del *.o programa.exe
Dependencias automáticas
Declaración de variables, compilador, opciones de compilación, objetos, dependencias, se agrega -MMD y -MP
-MMD Genera un archivo .d con las dependencias reales (.c → .h)
-MP Evita errores si un header se borra.
Cada compilación genera un archivo .d con las dependencias reales del objeto (.c y .h)
De esta manera, al agregar nuevos .c o cambiar que archivos forman el programa,
solo hay que editar OBJ en el Makefile, los .h se detectan automáticamente.
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic -MMD -MP
OBJ = main.o suma.o resta.o
DEP = $(OBJ:.o=.d) # Lo que hace es .o a .d
# Declaracion del Target programa, indicando los objetos de los que dependen
# Si las dependencias estan cumplidas linkea y genera el ejecutable
programa: $(OBJ)
$(CC) $(OBJ) -o programa
# Incluir todos los .d si existen, pero no falles si aun no existen, el - evita error la primera vez
-include $(DEP)
# Para cualquier .o, compilado desde su .c correspondiente, $< significa primer prerequisito (%.c)
%.o: %.c
$(CC) $(CFLAGS) -c $<
# Declaracion del target especial .PHONY para borrar objetos, dependencias y ejecutable
.PHONY: clean
clean:
del *.o *.d programa.exe
Librería estática
Una librería estática es un archivo .a que contiene uno o más objetos, como un .zip
Para crear una librería estática, usamos el binario ar (archiver)
con las opciones rcs (r: replace/add, c: create sí no existe, s: índice de símbolos):
ar rcs liboperaciones.a suma.o resta.o
Llamamos al archiver, le pasamos las opciones, el nombre de la librería con la palabra lib antes del nombre, la extensión .a y los objetos que se incluirán en la libreria
Pero para tener esos objetos, debemos compilar sin linkear usando gcc:
gcc -std=c11 -Wall -Wextra -pedantic -c suma.c //La cual creara suma.o
gcc -std=c11 -Wall -Wextra -pedantic -c resta.c //La cual creara resta.o
Ahora si podemos crear la librería estática con archiver.
Tras tener la librería estática, la cual hará nuestro ejecutable de mayor tamaño, e incluirá sólo lo que nuestro main use de la librería, haciéndolo autónomo.
Compilaremos y linkearemos nuestro ejecutable, indicando el path donde se encuentra la librería con la opción -LPath
donde Path en este caso será . ya que la librería se encuentra en el
mismo directorio que el source code.
Y la opción -lNombre nombre de la libreria -l(library) pero pasamos el nombre sin la palabra lib:
gcc main.c -L. -loperaciones -o programa
Si main solo usa la función suma de la librería (contiene suma y resta),
entonces solo agregará el código de la función suma al ejecutable generado.
Ventajas:
- Ejecutable rápido
- No hay lookup dinámico
- Ideal para sistemas embebidos
- No depende del sistema
- Muy simple conceptualmente
- El
mainno se altera - Separación clara entre interfaz (
.h) e implementación (.a)
Desventajas:
- Ejecutable más grande que usando librerías dinámicas, aunque solo incluye los símbolos realmente usados.
- Si la librería cambia, se recompila todo, código duplicado en múltiples programas.
Opciones de archiver:
- Listar contenido:
ar t libNOMBRE.a - Extraer archivos:
ar x libNOMBRE.a - Agregar o reemplazar archivos:
ar r libNOMBRE.a - Crear la libreria si no exite:
ar c libNOMBRE.a - Generar índice de símbolos:
ar s libNOMBRE.a
nm es una herramienta que lista los símbolos del ejecutable:
.o,.a,.exe
Un símbolo es una funcion, una variable global, algo que el linker puede resolver.
Ejecutemos en la terminal los siguientes comandos y observemos la salida:
En Windows (MinGW):
nm suma.o
nm liboperaciones.a
nm programa.exe
En Linux:
nm suma.o
nm liboperaciones.a
nm programa
Un símbolo puede estar:
- Definido:
T,t,D,B - Indefinido:
Ulo debe resolver el linker
Makefile:
######################################
# Libreria estatica con Makefile:
######################################
# Declarar variable que indique el compilador y archiver a usar, y sus opciones
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic -MMD -MP
LDFLAGS = -L.
LDLIBS = -loperaciones
AR = ar
ARFLAGS = rcs
# Nombre de la libreria estatica
LIB = liboperaciones.a
# Objetos de la libreria
LIB_OBJ = suma.o resta.o
# Objeto principal
OBJ = main.o
# Dependencias automaticas
DEP = $(OBJ:.o=.d) $(LIB_OBJ:.o=.d)
# Target principal
programa: $(OBJ) $(LIB)
$(CC) $(OBJ) $(LDFLAGS) $(LDLIBS) -o programa
# Crear libreria estatica Target:Dependencias
$(LIB): $(LIB_OBJ)
$(AR) $(ARFLAGS) $(LIB) $(LIB_OBJ)
# Version avanzada para crear libreria estatica:
# $@ = target (liboperaciones.a)
# $^ = dependencias (suma.o resta.o)
#$(LIB): $(LIB_OBJ)
# $(AR) $(ARFLAGS) $@ $^
# Regla generica para objetos .o
%.o: %.c
$(CC) $(CFLAGS) -c $<
# Incluir dependencias si existen, evitando primer error con -
-include $(DEP)
# Limpieza
.PHONY: clean
clean:
del *.o *.d *.a programa.exe
Librería dinámica
.dll (Windows) / .so (Linux)
Es código compilado que NO se copia dentro del ejecutable; el ejecutable queda más chico.
Y en tiempo de ejecución (runtime), el sistema operativo carga la .dll o .so, resuelve los símbolos (funciones).
El programa llama a ese código externo.
Se usa cuando varios programas usan la misma librería y queremos actualizar la librería sin recompilar. Queremos reducir el ejecutable; estamos haciendo APIs, frameworks, plugins.
No es recomendado para sistemas embebidos, no es portable en un único .exe, no queremos depender del entorno.
Compilar objetos sin linkear
Se usa la opcion -fPIC (Position Independent Code). En Linux es obligatorio (.fPIC) para librerías dinámicas (.so), en Windows con MinGW no.
gcc -std=c11 -Wall -Wextra -fPIC -c suma.c
gcc -std=c11 -Wall -Wextra -fPIC -c resta.c
Una vez creados los objetos, creamos la librería dinámica .dll, en linux .so:
gcc -shared -o operaciones.dll suma.o resta.o
Esto generá:
operaciones.dll: La librería realliboperaciones.dll.a:import library(la usa el linker). En MinGW no se creó esta, pero funcionó.
La forma explícita que nos asegura la creación de liboperaciones.dll.a y operaciones.dll es:
gcc -shared -o operaciones.dll suma.o resta.o "-Wl,--out-implib,liboperaciones.dll.a"
En Powershell agregamos "" porque sino lo interpreta como un array
Compilamos y linkeamos como en librería estática:
gcc main.c -L. -loperaciones -o programa
Comprobamos con nm:
En Windows (MinGW):
nm programa.exe | findstr suma
En Linux:
nm programa | grep suma
Veremos:
00000001400111f8 I __imp_suma // Nos indica que es externa, esto es lo que buscamos ver.
0000000140001750 T suma // Simbolo local generado por MinGW (thunk), no es interesante.
Makefile:
# Declarar variable que indique el compilador y archiver a usar, y sus opciones
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic -MMD -MP -fPIC
LDFLAGS = -L.
LDLIBS = -loperaciones
SHAREDFLAGS = -shared
IMPLIBFLAGS = -Wl,--out-implib,$(IMPLIB) # En Makefile no hacen falta escapar las comas con las "" como en PowerShell
# Nombre de la libreria dinamica
DLL = operaciones.dll
IMPLIB = liboperaciones.dll.a
# Objetos de la libreria
LIB_OBJ = suma.o resta.o
# Objeto principal
OBJ = main.o
# Dependencias automaticas
DEP = $(OBJ:.o=.d) $(LIB_OBJ:.o=.d)
# Target principal
programa: $(OBJ) $(DLL)
$(CC) $(OBJ) $(LDFLAGS) $(LDLIBS) -o programa
# Crear libreria dinamica + import library para el linker. Target:Dependencias
$(DLL): $(LIB_OBJ)
$(CC) $(SHAREDFLAGS) -o $(DLL) $(LIB_OBJ) $(IMPLIBFLAGS)
# Version avanzada para crear libreria dinamica:
# $@ = target (operaciones.dll)
# $^ = dependencias (suma.o resta.o)
#$(DLL): $(LIB_OBJ)
# $(CC) $(SHAREDFLAGS) -o $@ $^ $(IMPLIBFLAGS)
# Regla generica para objetos .o
%.o: %.c
$(CC) $(CFLAGS) -c $<
# Incluir dependencias si existen, evitando primer error con -
-include $(DEP)
# Limpieza
.PHONY: clean
clean:
del *.o *.d *.dll *.a programa.exe
Librería dinámica universal con detección de sistema operativo
#############################################################################
# Libreria dinamica con Makefile para Windows y linux deteccion automatica:
#############################################################################
# Declarar variable que indique el compilador y sus opciones
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -pedantic -MMD -MP -fPIC
# Objetos de la libreria
LIB_OBJ = suma.o resta.o
# Objeto principal
OBJ = main.o
# Dependencias automaticas
DEP = $(OBJ:.o=.d) $(LIB_OBJ:.o=.d)
###################################
# Deteccion del sistema operativo
###################################
ifeq ($(OS), Windows_NT)
# ------ Windows -------
SHARED_EXT = dll
LIB_PREFIX =
IMPLIB = liboperaciones.dll.a
SHAREDFLAGS = -shared
IMPLIBFLAGS = -Wl,--out-implib,$(IMPLIB)
LDFLAGS = -L.
LDLIBS = -loperaciones
RM = del
EXE = .exe
else
# ----- Linux ---------
SHARED_EXT = so
LIB_PREFIX = lib
IMPLIB = liboperaciones.a
SHAREDFLAGS = -shared -fPIC
IMPLIBFLAGS =
LDFLAGS = -L. -Wl,-rpath,.
LDLIBS = -loperaciones
RM = rm -f
EXE =
endif
# Nombre final de la libreria
SHARED_LIB = $(LIB_PREFIX)operaciones.$(SHARED_EXT)
# Target principal
programa: $(OBJ) $(SHARED_LIB)
$(CC) $(OBJ) $(LDFLAGS) $(LDLIBS) -o programa$(EXE)
# Crear libreria dinamica. Target:Dependencias
$(SHARED_LIB): $(LIB_OBJ)
$(CC) $(SHAREDFLAGS) -o $(SHARED_LIB) $(LIB_OBJ) $(IMPLIBFLAGS)
# Version avanzada para crear libreria dinamica:
# $@ = target (operaciones.dll)
# $^ = dependencias (suma.o resta.o)
#$(SHARED_LIB): $(LIB_OBJ)
# $(CC) $(SHAREDFLAGS) -o $@ $^ $(IMPLIBFLAGS)
# Regla generica para objetos .o
%.o: %.c
$(CC) $(CFLAGS) -c $<
# Incluir dependencias si existen, evitando primer error con -
-include $(DEP)
# Limpieza
.PHONY: clean
clean:
$(RM) *.o *.d *.so *.dll *.a programa$(EXE)
Archivos base utilizados en todos los ejemplos:
Según el ejemplo trabajado, la estructura de carpetas será la siguiente:
Ejemplo básico:
📁 Proyecto
└── 📁 Proyecto_basico
├── 📄 main.c
├── 📄 suma.c
├── 📄 suma.h
└── 📄 Makefile
Resto de los ejemplos:
📁 Proyecto
└── 📁 Proyecto_operaciones
├── 📄 main.c
├── 📄 suma.c
├── 📄 suma.h
├── 📄 resta.c
├── 📄 resta.h
└── 📄 Makefile
Todos los archivos fuente se encuentran en el mismo directorio que el
Makefile. Esto permite que las reglas de compilación,
enlazado y generación de librerías funcionen correctamente.
En los ejemplos donde se utiliza el comando nm, se incluye
una variable global en suma.c únicamente con fines didácticos,
para observar su símbolo en la tabla generada.
main.c: Versión básica
#include <stdio.h>
#include "suma.h"
int main(void)
{
printf("%d\n", suma(2, 3));
return 0;
}
main.c: Solo para dependencias automáticas
#include <stdio.h>
#include "suma.h"
#include "resta.h"
int main(void)
{
printf("%d\n", suma(2, 3));
printf("y");
printf("%d\n", resta(6, 3));
return 0;
}
suma.c:
#include "suma.h"
/* Variable global definida (inicializada) solo para que se vea en el comando nm la salida D */
int contador = 0;
int suma(int a, int b)
{
contador++;
return a+b;
}
suma.h:
#ifndef SUMA_H
#define SUMA_H
int suma(int a, int b);
#endif
resta.c:
#include "resta.h"
int resta(int a, int b)
{
return a-b;
}
resta.h:
#ifndef RESTA_H
#define RESTA_H
int resta(int a, int b);
#endif