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:

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:

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:

Desventajas:

Opciones de archiver:

nm es una herramienta que lista los símbolos del ejecutable:

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:

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á:

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