Construcción de proyectos C multiarchivo y multidirectorio usando Makefile

  1. Introducción

Los lectores han solicitado información sobre cómo utilizar Makefile para construir proyectos con múltiples archivos y directorios jerárquicos, así que aquí va una solución detallada.

Para comprender mejor el funcionamiento básico de Makefile, se recomienda revisar este artículo previamente:

«Tutorial introductorio a Makefile»

A continuación se presenta un ejemplo práctico basado en un pequeño proyecto ya desarrollado por el autor.

El diseño completo del proyecto y su código pueden consultarse en el siguiente enlace:

«Proyecto de introducción a C: Sistema de gestión de números telefónicos (ideal para principiantes)»

I. Estructura de archivos

Comenzamos con la implementación.

Las funciones principales del proyecto se colocan en archivos .c nombrados según la función correspondiente, organizados dentro de directorios con el mismo nombre.

Por ejemplo, la función allfree() reside en allfree/allfree.c

La estructura final del directorio es la siguiente:

 peng@ubuntu:/mnt/hgfs/code/phone$ tree .
.
├── allfree
│   ├── allfree.c
│   └── Makefile
├── create
│   ├── create.c
│   └── Makefile
├── delete
│   ├── delete.c
│   └── Makefile
├── display
│   ├── display.c
│   └── Makefile
├── include
│   ├── Makefile
│   └── phone.h
├── init
│   ├── init.c
│   └── Makefile
├── login
│   ├── login.c
│   └── Makefile
├── main
│   ├── main.c
│   └── Makefile
├── Makefile
├── menu
│   ├── Makefile
│   └── menu.c
├── scripts
│   └── Makefile
└── search
    ├── Makefile
    └── search.c

11 directories, 22 files


Veamos ahora los resultados de compilación:

peng@ubuntu:/mnt/hgfs/code/phone$ make
make[1]: Entering directory '/mnt/hgfs/code/phone/allfree'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/allfree'
make[1]: Entering directory '/mnt/hgfs/code/phone/create'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/create'
make[1]: Entering directory '/mnt/hgfs/code/phone/delete'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/delete'
make[1]: Entering directory '/mnt/hgfs/code/phone/display'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/display'
make[1]: Entering directory '/mnt/hgfs/code/phone/init'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/init'
make[1]: Entering directory '/mnt/hgfs/code/phone/login'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/login'
make[1]: Entering directory '/mnt/hgfs/code/phone/menu'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/menu'
make[1]: Entering directory '/mnt/hgfs/code/phone/search'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/search'
make[1]: Entering directory '/mnt/hgfs/code/phone/main'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/main'
gcc -Wall -O3 -o phone allfree/*.o create/*.o delete/*.o display/*.o init/*.o login/*.o menu/*.o search/*.o main/*.o -lpthread
phone make done! 


El resultado de ejecución es el siguiente:

II. Conceptos básicos frecuentes de Makefile

[0] Explicación de símbolos '@' '$' '$$' '-' '-n '

  1. '@'Normalmente, Makefile muestra en pantalla los comandos antes de ejecutarlos. Si se antepone '@' al comando, éste no será mostrado por make. Ejemplo:
@echo  --compiling module----;  // Muestra  --compiling module----
echo  --compiling module----;  // Sin @, muestra echo  --compiling module----   


  1. ' - '

Al eliminar o crear archivos, si se encuentra un archivo inexistente o ya creado, se puede ignorar el error mediante el uso del carácter - delante del comando.

-rm dir;
-mkdir aaadir;


  1. ' $ 'El signo $ se utiliza principalmente para expandir variables definidas en el Makefile.
  2. ' $$ ' El símbolo $$ sirve para expandir variables de shell definidas en el Makefile.

[1] wildcard

Descripción: Lista todos los archivos en el directorio actual que coincidan con el patrón "PATTERN", separados por espacios. El patrón usa comodines reconocidos por el shell, incluyendo "?" (un carácter) y "*" (múltiples caracteres). Ejemplo:

$(wildcard *.c) 


Devuelve una lista de todos los archivos fuente .c en el directorio actual.

[2] patsubst

Descripción: Reemplaza las palabras que terminan en .c por aquellas que terminan en .o en la cadena "x.c.c bar.c". Ejemplo:

$(patsubst %.c,%.o,x.c.c bar.c)


Resultado de la función:

 x.c.o bar.o


[3] notdir

Descripción: Elimina la ruta del nombre de archivo. Ejemplo:

SRC = ( notdir ./src/a.c ) 


Elimina la ruta del archivo a.c, usando (notdir ./src/a.c) para obtener solo el nombre del archivo, es decir, a.c.

[4] Ruta de inclusión de archivos de cabecera

Utilizando -I+ruta_de_inclusión, se puede especificar la ubicación de los archivos de cabecera para el compilador. Ejemplo:

INCLUDES = -I./inc


$(CC) -c $(INCLUDES) $(SRC)


[5] addsuffix

Nombre de la función: Agregar sufijo — addsuffix. Sintaxis:

$(addsuffix SUFFIX,NAMES…) 


Funcionalidad: Añade el sufijo "SUFFIX" a cada nombre de archivo en "NAMES…". Devuelve: Una lista de nombres de archivo con el sufijo agregado, separados por espacios. Ejemplo:

$(addsuffix .c,foo bar) 


Resultado:

foo.c bar.c


[6] Incluir otro archivo: include

En Makefile, se puede usar la palabra clave include para incluir otros archivos Makefile, similar a #include en C. El contenido del archivo incluido se inserta en la posición donde se hace la inclusión. Ejemplo:

include file.dep


Incluye el contenido del archivo file.dep en el Makefile actual.

Antes de include pueden haber espacios, pero nunca debe comenzar con tabulador.

[7] foreach

La función foreach se diferencia de otras funciones, ya que permite realizar bucles. Sintaxis:

$(foreach <var>,<list>,<text> )


La función toma cada palabra de la lista , la asigna a la variable , y ejecuta la expresión . Cada ejecución de devuelve una cadena, y al finalizar el bucle, todas las cadenas se concatenan con espacios. Ejemplo:

names := a b c d
files := $(foreach n,$(names),$(n).o)


El resultado de $(files) será:

a.o b.o c.o d.o


Nota: La varible es local al contexto de foreach y deja de existir desppués de su ejecución.

[8] call

La función "call" permite crear funciones personalizadas con parámetros. Permite invocar funciones definidas por el usuario. Se puede definir una variable compleja y usar "call" para expandirla con diferentes parámetros.

Sintaxis:

$(call variable,param1,param2,...)


Funcionalidad: Durante la ejecución, los parámetros son asignados a variables temporales como $(1), $(2), etc. La variable "variable" se expande en el contxeto de la función, reemplazando $(1), $(2), etc., por los valores de los parámetros. Retorno: El valor calculado tras sustituir los parámetros en la expresión definida por "variable".

Ejemplo:

reverse = $(2)$(1)
foo = $(call reverse,a,b)
all:
	@echo "foo=$(foo)"


Resultado de ejecución:

foo=ba


Donde a sustituye $(1) y b sustituye $(2).

III. Detalles de compilación

Al ejecutar el comando make desde el directorio raíz, los pasos son:

  1. include scripts/Makefile : Inserta el contenido del archivo en esta posición,
  2. Se ejecuta el objetivo predeterminado all, que depende de $(Target)``$(Target) está definido en scripts/Makefile como phone.
  3. $(Target) depende de mm.
  4. El objetivo mm ejecuta:
@ $(foreach n,$(Modules),$(call modules_make,$(n)))


Donde Modules contiene los nombres de todos los directorios. foreach recorre cada palabra en $(Modules), asignándola a n, y ejecuta:

call modules_make,$(n)


  1. modules_make se sustituye por $(MAKE) -C $(1),

$(MAKE) tiene el nombre predeterminado make -C: Cambia al directorio hijo para ejecutar make $(1) : Es el nombre del directorio actual de cada iteración

Así, el paso 4 entra en cada subdirectorio y ejecuta su Makefile.

  1. Al entrar en un subdirectorio, se ejecuta el Makefile El objetivo predeterminado es all, que depende de Objs
Objs := $(patsubst %.c,%.o,$(Source))


patsubst reemplaza los archivos .c por .o en Source Y

Source := $(wildcard ./*.c)


wildcard lista todos los archivos .c del directorio actual

Por lo tanto, el paso 6 compila todos los archivos .c del subdirectorio en archivos .o correspondientes.

$(CC) $(CFLAGS) -o $(Target) $(AllObjs) $(Libs)


Estas variables están definidas en scripts/Makefile $(CC) : Se sustituye por gcc $(CFLAGS) : Se sustituye por -Wall -O3 -o $(Target) : Genera el programa ejecutable phone $(AllObjs) :

AllObjs := $(addsuffix /*.o,$(Modules))


addsuffix agrega /*.o a cada directorio en Modules, obteniendo todos los archivos .o compilados $(Libs) : Se sustituye por -lpthread

Analiza los pasos anteriores para entender cómo funciona make clean.

El programa completo puede descargarse escribiendo en el chatbot del canal: «gestor de números telefónicos»

«Gestor de números telefónicos - versión Makefile.rar»

Etiquetas: Makefile C compilación multidirectorio programación

Publicado el 10-11 02:24