Principios de funcionamiento de Makefile

Makefile es una herramienta fundamental en el desarrollo de software para automatizar el proceso de compilación y gestión de dependencias. Su funcionamiento se basa en un conjunto de reglas que especifican cómo construir objetivos a partir de sus dependencias.

Principio de Construcción de Objetivos

Cuando se invoca el comando make, este busca un archivo llamado Makefile (o makefile) en el directorio actual. Dentro de este archivo, make examina las reglas definidas. El principio básico es el siguiente:

  1. Para construir un objetivo, make primero verifica si sus dependencias existen.
  2. Si una dependencia no existe, make busca una regla que especifique cómo construir esa dependencia. Este proceso se repite recursivamente hasta que todas las dependencias neceasrias estén presentes.
  3. Una vez que todas las dependencias están listas, make ejecuta los comandos asociados a la regla para crear el objetivo.

Ejemplo de Principio de Construcción

Consideremos el siguiente Makefile:

All: hola
hola: hola.o
	gcc hola.o -o hola

hola.o: hola.c
	gcc -c hola.c -o hola.o

Al ejecutar make:

$ make
gcc -c hola.c -o hola.o
gcc hola.o -o hola

En este caso, para construir All, make necesita hola. Para construir hola, neecsita hola.o. Para construir hola.o, necesita hola.c. Como hola.c es un archivo fuente y no una dependencia que requiera una regla, make procede a compilar hola.o y luego a enlazarlo para crear el ejecutable hola.

Estructura de una Regla

Una regla básica en un Makefile tiene la siguiente estructura:

Objetivo: Dependencia1 Dependencia2 ...
	[Tabulador] Comando1
	[Tabulador] Comando2
	...

  • Objetivo (Target): Es el archivo que se desea crear o la acción que se desea realizar.
  • Dependencias (Prerequisites): Son los archivos o los objetivos de otras reglas que deben existir o estar actualizados antes de que el objetivo actual pueda ser construido.
  • Comandos (Recipe): Son las instrucciones del shell que se ejecutan para construir el objetivo. Es crucial que los comandos estén precedidos por un carácter de tabulación (\t), no por espacios.

Ejemplo con un solo archivo fuente

Supongamos la siguiente estructura de directorios:

.
├── hola.c
└── makefile

El contenido del makefile para compilar hola.c sería:

hola: hola.c
	gcc hola.c -o hola

Al ejecutar make:

$ make
gcc hola.c -o hola
$ ls
hola  hola.c  makefile

Ejemplo con múltiples archivos fuente

Si tenemos varios archivos fuente, queremos evitar recompilaciones innecesarias. La estrategia es compilar cada archivo fuente a su respectivo archivo objeto (.o) y luego enlazar todos los archivos objeto para crear el ejecutable final.

Estructura de directorios:

.
├── sumar.c
├── hola.c
├── makefile
└── restar.c

Opción 1: Especificando todas las dependencias directamente (menos eficiente):

# Opción 1
hola: hola.c sumar.c restar.c
	gcc hola.c sumar.c restar.c -o hola

Opción 2: Utilizando archivos objeto (más eficiente):

# Opción 2
hola: hola.o sumar.o restar.o
	gcc hola.o sumar.o restar.o -o hola

hola.o: hola.c
	gcc -c hola.c -o hola.o
sumar.o: sumar.c
	gcc -c sumar.c -o sumar.o
restar.o: restar.c
	gcc -c restar.c -o restar.o

Al compilar por primera vez:

$ make
gcc -c hola.c -o hola.o
gcc -c sumar.c -o sumar.o
gcc -c restar.c -o restar.o
gcc hola.o sumar.o restar.o -o hola

Si solo modificamos sumar.c y volvemos a ejecutar make:

$ make
gcc -c sumar.c -o sumar.o
gcc hola.o sumar.o restar.o -o hola

Solo se recompila el archivo modificado y luego se realiza el enlazado.

Funciones Integradas de make

make proporciona funciones para manipular cadenas de texto, lo que facilita la gestión de listas de archivos.

$(wildcard patrón)

Esta función busca archivos que coincidan con el patrón especificado y devuelve una lista de los nombres de archivo encontrados. Por ejemplo:

fuentes = $(wildcard *.c)

Si en el directorio actual hay hola.c, sumar.c y restar.c, la variable fuentes contendrá la cadena hola.c sumar.c restar.c.

$(patsubst patrón,reemplazo,texto)

Esta función sustituye las ocurrencias del patrón por el reemplazo dentro del texto. Es muy útil para generar nombres de archivos objeto a partir de nombres de archivos fuente.

objetos = $(patsubst %.c,%.o,$(fuentes))

Si fuentes es hola.c sumar.c restar.c, entonces objetos será hola.o sumar.o restar.o.

Reescribiendo el Makefile con estas funciones:

fuentes = $(wildcard *.c)
objetos = $(patsubst %.c,%.o,$(fuentes))

All: hola

hola: $(objetos)
	gcc $(objetos) -o hola

# Reglas para compilar cada archivo .o a partir de su .c
# Estas reglas se pueden generalizar con reglas de patrón (ver más adelante)
hola.o: hola.c
	gcc -c hola.c -o hola.o
sumar.o: sumar.c
	gcc -c sumar.c -o sumar.o
restar.o: restar.c
	gcc -c sumar.c -o sumar.o

clean:
	-rm -f $(objetos) hola # El '-' antes del comando hace que make no falle si el comando falla (ej. si los archivos no existen)

Para ejecutar la limpieza:

make clean -n  # Muestra los comandos que se ejecutarían sin ejecutarlos
make clean     # Ejecuta la limpieza

Variables Automáticas

make proporciona variables especiales que se refieren a partes de la regla actual.

$@

Representa el nombre del objetivo de la regla.

$<

Representa el nombre de la primera dependencia de la regla.

$^

Representa los nombres de todas las dependencias de la regla, eliminando duplicados.

Aplicando estas variables a nuestro ejemplo:

fuentes = $(wildcard *.c)
objetos = $(patsubst %.c,%.o,$(fuentes))

All: hola

hola: $(objetos)
	gcc $^ -o $@ # $@ es 'hola', $^ es 'hola.o sumar.o restar.o'

hola.o: hola.c
	gcc -c $< -o $@ # $@ es 'hola.o', $< es 'hola.c'
sumar.o: sumar.c
	gcc -c $< -o $@ # $@ es 'sumar.o', $< es 'sumar.c'
restar.o: restar.c
	gcc -c $< -o $@ # $@ es 'restar.o', $< es 'restar.c'

clean:
	-rm -f $(objetos) hola

Reglas de Patrón (Pattern Rules)

Las reglas de patrón permiten definir una única regla para un conjunto de objetivos similares. Utilizan el carácter % para representar una coincidencia.

Podemos simplificar las reglas de compilación de archivos objeto utilizando una regla de patrón:

fuentes = $(wildcard *.c)
objetos = $(patsubst %.c,%.o,$(fuentes))

All: hola

hola: $(objetos)
	gcc $^ -o $@

# Regla de patrón: Para cualquier archivo .o, su dependencia es el archivo .c con el mismo nombre base.
%.o: %.c
	gcc -c $< -o $@

clean:
	-rm -f $(objetos) hola

Con esta regla de patrón, no es necesario escribir una regla separada para cada archivo .o. Si añadimos un nuevo archivo multiplicar.c, solo necesitamos añadirlo a la lista fuentes o asegurarnos de que $(wildcard *.c) lo detecte, y la regla de patrón se encargará de generar multiplicar.o automáticamente.

Directorios Anidados (Nested Execution of Make)

Para proyectos más grandes, es común organizar el código fuente en subdirectorios. make puede manejar esto mediante la invocación de make desde un subdirectorio o configurando las rutas de compilación.

Ejemplo con un subdirectorio

Estructura de directorios:

.
├── sumar.c
├── dir1
│   ├── makefile
│   └── multiplicar.c
├── hola.c
├── makefile
└── restar.c

Makefile en la raíz (control principal):

src = $(wildcard *.c) # hola.c sumar.c restar.c
obj = $(patsubst %.c,%.o,$(src))

All: hola

hola: $(obj)
	gcc $^ -o $@

%.o: %.c
	gcc -c $< -o $@

# Objetivo para compilar el subdirectorio
subsystem:
	cd dir1 && $(MAKE) # $(MAKE) es una variable especial que invoca make con los mismos flags

clean:
	-rm -f $(obj) hola

makefile dentro de dir1:

src = $(wildcard *.c) # multiplicar.c
obj = $(patsubst %.c,%.o,$(src))

All: $(obj)
	@echo "Compilación en dir1 completada."

%.o: %.c
	gcc -c $< -o $@

clean:
	-rm -f $(obj)

Al ejecutar make subsystem en la raíz, se ejecutará make dentro de dir1.

Organización con directorios de fuente y objeto separados

Una práctica común es mantener los archivos fuente en un directorio (ej. src) y los archivos objeto en otro (ej. obj).

Estructura:

.
├── ejecutable_final
├── inc
│   └── mi_math.h
├── makefile
├── obj
│   ├── dir1
│   ├── sumar.o
│   ├── hola.o
│   └── restar.o
└── src
    ├── sumar.c
    ├── dir1
    │   ├── makefile
    │   └── multiplicar.c
    ├── hola.c
    └── restar.c

Makefile en la raíz:

# Buscar fuentes en src/ y src/dir1/
fuentes = $(wildcard ./src/*.c)
fuentes += $(wildcard ./src/dir1/*.c)

# Generar nombres de objetos en obj/, manteniendo la estructura de directorios si es necesario
# Ejemplo: ./src/dir1/multiplicar.c -> ./obj/dir1/multiplicar.o
objetos = $(patsubst ./src/%.c,./obj/%.o,$(fuentes))

All: ejecutable_final

ejecutable_final: $(objetos)
	gcc $^ -o $@

# Regla de patrón para compilar .c en src/ a .o en obj/
# El símbolo '%' en la regla de patrón NO incluye la ruta.
# Por lo tanto, la regla debe ser explícita para la estructura de directorios.
./obj/%.o: ./src/%.c
	@mkdir -p $(dir $@) # Crea el directorio de destino si no existe
	gcc -c $< -o $@

clean:
	-rm -rf $(objetos)
	-rm -f ejecutable_final

exec: # Comando de ejemplo para mostrar variables
	@echo "Objetos: $(objetos)"
	@echo "Fuentes: $(fuentes)"

Nota importante: El símbolo % en las reglas de patrón de make normalmente solo coincide con la parte del nombre del archivo, no con la ruta completa. Sin embargo, dentro de las funciones de make (como patsubst), el % puede coincidir con cualquier carácter, incluyendo partes de la ruta.

Salida esperada de la estructura de directorios después de make:

.
├── ejecutable_final
├── inc
│   └── mi_math.h
├── makefile
├── obj
│   ├── dir1
│   │   └── multiplicar.o
│   ├── sumar.o
│   ├── hola.o
│   └── restar.o
└── src
    ├── sumar.c
    ├── dir1
    │   ├── makefile
    │   └── multiplicar.c
    ├── hola.c
    └── restar.c

Etiquetas: make Makefile compilación automatización gestión-de-dependencias

Publicado el 7-6 06:30