Uso de Make Utility y Makefiles en Linux [Guide]

Esta es una guía completa para principiantes sobre el uso del comando make en Linux.

Aprenderás:

  • El propósito del comando make
  • Instalación del comando make
  • Creación y uso del archivo MAKE para un proyecto C de muestra

¿Qué es la utilidad make?

La utilidad make es una de las utilidades más prácticas para un programador. Su propósito principal es compilar un proyecto de software mediano a grande. La utilidad make es tan útil y versátil que incluso el El kernel de Linux lo usa !

Para comprender la utilidad de la utilidad make, primero se debe comprender por qué se necesitaba en primer lugar.

A medida que su software se vuelve más extenso, comienza a depender cada vez más de dependencias externas (es decir, bibliotecas). Su código comienza a dividirse en varios archivos con Dios sabe qué hay en cada archivo. Compilar cada archivo y vincularlos entre sí de manera sensata para producir los binarios necesarios se vuelve complicado.

“¡Pero puedo crear un script Bash para eso!”

¡Por qué sí, puedes! ¡Más poder para ti! Pero a medida que su proyecto crece, debe lidiar con reconstrucciones incrementales. ¿Cómo lo manejará de manera genérica, de modo que la lógica se mantenga verdadera incluso cuando aumente la cantidad de archivos?

Todo esto es manejado por la utilidad make. Así que no reinventemos la rueda y veamos cómo instalar y hacer un buen uso de la utilidad make.

Instalación de la utilidad make

La utilidad make ya está disponible en los repositorios propios de casi todas las distribuciones de Linux.

Para instalar make en Debian , Ubuntu y sus derivados, use el apt administrador de paquetes así:

                      
                        sudo apt install make
                      
                    

Para instalar make en Fedora y distribuciones de Linux basadas en RHEL, use el dnf administrador de paquetes así:

                      
                        sudo dnf install make
                      
                    

Para instalar make en Arch Linux y sus derivados, utilice el pacman administrador de paquetes así:

                      
                        sudo pacman -Sy make
                      
                    

Ahora que la utilidad make está instalada, puede proceder a comprenderla con ejemplos.

Creación de un archivo MAKE básico

La utilidad make compila su código según las instrucciones especificadas en el archivo MAKE en el directorio de nivel superior del repositorio de código de su proyecto.

A continuación se muestra la estructura de directorios de mi proyecto:

                      
                        $ tree make-tutorial

make-tutorial
└── src
    ├── calculator.c
    ├── greeter.c
    ├── main.c
    └── userheader.h

1 directory, 4 files
                      
                    

A continuación se muestran los contenidos de la main.c archivo fuente:

                      
                        #include <stdio.h>

#include "userheader.h"

int main()
{
    greeter_func();

    printf("nAdding 5 and 10 together gives us '%d'.n", add(5, 10));
    printf("Subtracting 10 from 32 results in '%d'.n", sub(10, 32));
    printf("If 43 is  multiplied with 2, we get '%d'.n", mul(43, 2));
    printf("The result of dividing any even number like 78 with 2 is a whole number like '%f'.n", div(78, 2));

    return 0;
}
                      
                    

A continuación se encuentran los contenidos de la greeter.c archivo fuente:

                      
                        #include <stdio.h>

#include "userheader.h"

void greeter_func()
{
    printf("Hello, user! I hope you are ready for today's basic Mathematics class!n");
}
                      
                    

A continuación se muestran los contenidos de la calculator.c archivo fuente:

                      
                        #include <stdio.h>

#include "userheader.h"

int add(int a, int b)
{
    return (a + b);
}

int sub(int a, int b)
{
    if (a > b)
        return (a - b);
    else if (a < b)
        return (b - a);
    else return 0;
}

int mul(int a, int b)
{
    return (a * b);
}

double div(int a, int b)
{

    if (a > b)
        return ((double)a / (double)b);
    else if (a < b)
        return ((double)b / (double)a);
    else
        return 0;
}
                      
                    

Finalmente, a continuación se muestra el contenido de la userheader.h archivo de cabecera:

                      
                        #ifndef USERHEADER_DOT_H
#define USERHEADER_DOT_H

void greeter_func();

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
double div(int a, int b);

#endif /* USERHEADER_DOT_H */
                      
                    

Conceptos básicos de un archivo MAKE

Antes de crear un archivo MAKE básico, echemos un vistazo a la sintaxis de un archivo MAKE. El componente básico de un Makefile consta de una o varias “reglas” y “variables”.

Reglas en un archivo MAKE

Primero echemos un vistazo a las reglas en el archivo MAKE. Una regla de makefile tiene la siguiente sintaxis:

                      
                        target : prerequisites
    recipe
    ...
                      
                    
  • A target es el nombre del archivo que será generado por make. Por lo general, estos son archivos de objetos que luego se usan para vincular todo.
  • A prerequisite es un archivo que es necesario para que se genere el destino. Aquí es donde normalmente especifica su .c , .o y .h archivos
  • Finalmente, un recipe se necesitan uno o varios pasos para generar el target .

Macros/Variables en makefile

En C y C++, una característica básica del lenguaje son las variables. Nos permiten almacenar valores que podríamos querer usar en muchos lugares. Esto nos ayuda a usar el mismo nombre de variable cuando sea necesario. Un beneficio adicional es que solo necesitamos hacer un cambio si necesitamos cambiar el valor.

De manera similar, un archivo MAKE puede contener variables. A veces se denominan macros. La sintaxis para declarar una variable en un Makefile es la siguiente:

                      
                        variable = value
                      
                    

Una variable y los valores que contiene están separados por un igual ( = ) señal. Los valores múltiples están separados por espacios entre sí.

En general, las variables se utilizan para almacenar varios elementos necesarios para la compilación. Digamos que desea habilitar la detección de desbordamiento de búfer en tiempo de ejecución y habilitar ASLR completo para el ejecutable; esto se puede lograr almacenando todas las banderas del compilador en una variable, como CFLAGS .

A continuación se muestra una demostración haciendo esto:

                      
                        CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
                      
                    

Creamos una variable llamada CFLAGS (marcas del compilador) y agregamos todas nuestras marcas del compilador aquí.

Para usar nuestra variable, podemos encerrarla entre paréntesis comenzando con un signo de dólar, así:

                      
                        gcc $(CFLAGS) -c main.c
                      
                    

La línea anterior en nuestro archivo MAKE agregará todas nuestras banderas de compilador especificadas y compilará el main.c archivo como requerimos.

Variables automáticas

La utilidad make tiene algunas variables automáticas para ayudar a facilitar aún más la repetición. Estas variables se usan comúnmente en la receta de una regla.

Algunas de las variables automáticas son las siguientes:

Variables automáticas Sentido
[email protected] Nombre de la regla de destino. Usualmente se usa para especificar el nombre del archivo de salida.
$< Nombre del primer requisito previo.
ps Nombres de todos los requisitos previos que son más nuevos que el objetivo. es decir, archivos que se han modificado después de la compilación de código más reciente
$^ Nombres de todos los requisitos previos con espacios entre ellos.

Puede encontrar la lista completa de las variables automáticas en Documentación oficial de GNU Make .

Variables implícitas

Al igual que las variables automáticas cubiertas anteriormente, make también tiene algunas variables que tienen un uso determinado. Como antes usé el CFLAGS macro/variable para almacenar banderas del compilador, hay otras variables que tienen un uso supuesto.

Esto no se puede considerar como “palabras clave reservadas”, sino más bien como el “consenso general” de nombrar variables.

Estas variables convencionales son las siguientes:

Variables implícitas Descripción
VPATH Hacer el equivalente de la utilidad de Bash PATH variable. Las rutas están separadas por el signo de dos puntos ( : ). Esto está vacío por defecto.
COMO Este es el ensamblador. El valor predeterminado es el as ensamblador.
CC El programa para compilar archivos C. El valor predeterminado es cc . (Normalmente, cc puntos a gcc .)
CXX El programa para compilar archivos C++. El valor predeterminado es el g++ compilador.
CPP El programa que ejecuta el preprocesador C. El valor predeterminado se establece en $(CC) -E .
lex El programa que convierte las gramáticas léxicas en código fuente. El valor predeterminado es lex . ( deberías cambiar esto a flex .)
HILAS El programa que delinea tu código fuente. El valor predeterminado es lint .
RM El comando para eliminar un archivo. El valor predeterminado es rm -f . ( ¡Por favor, preste mucha atención a esto! )
BANDERAS ASF Esto contiene todas las banderas para el ensamblador.
FLANDERAS Esto contiene todas las banderas para el compilador de C ( cc ).
CXXBANDERAS Esto contiene todas las banderas para el compilador de C++ ( g++ ).
CPPFLAGS Contiene todas las banderas para el preprocesador C.
.FALSO Especifique objetivos que no se parezcan al nombre de un archivo. Un example es el objetivo “limpiar”; donde limpio es un valor de .PHONY

Comentarios en un archivo MAKE

Los comentarios en un archivo MAKE son como los de un script de shell. Comienzan con el símbolo de libra/almohadilla ( # ) y el contenido de dicha línea (después del símbolo almohadilla/almohadilla) se considera un comentario por parte de la utilidad make y se ignora.

A continuación se muestra un example demostrando esto:

                      
                        CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
# The '-D_FORTIFY_SOURCE=2' flag enables run-time buffer overflow detection
# The flags '-fpie -Wl,-pie' are for enabling complete address space layout randomization
                      
                    

Borrador inicial de un makefile

Ahora que he descrito la sintaxis básica de los elementos de un archivo MAKE y también el árbol de dependencias de mi proyecto simple, escribamos un archivo MAKE muy básico para compilar nuestro código y vincular todo.

Comencemos por configurar el CFLAGS , CC y el VPATH variables que son necesarias para nuestra compilación. (Este no es el archivo MAKE completo. Lo construiremos progresivamente).

                      
                        CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src
                      
                    

Con eso hecho, definamos nuestras reglas para construir. Voy a crear 3 reglas, para cada .c expediente. Mi binario ejecutable se llamará make_tutorial pero el tuyo puede ser lo que quieras!

                      
                        CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src


make_tutorial : main.o calculator.o greeter.o
        $(CC) $(CFLAGS) $? -o [email protected]

main.o : main.c
        $(CC) $(CFLAGS) -c $? -o [email protected]

calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o [email protected]

greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o [email protected]
                      
                    

Como puedes ver, estoy recopilando todos los .c archivos en archivos objeto ( .o ) y enlazándolos al final.

Cuando ejecutamos el make comando, comenzará con la primera regla ( make_tutorial ). Esta regla es para crear un binario ejecutable final del mismo nombre. Tiene 3 archivos de objetos de requisitos previos para cada .c archivos

Cada regla consecutiva después de la make_tutorial La regla es crear un archivo de objeto a partir del archivo de origen del mismo nombre. Puedo entender lo complejo que se siente esto. Entonces, analicemos cada una de estas variables automáticas e implícitas y comprendamos lo que significan.

  • $(CC) : Llama al compilador GNU C ( gcc ).
  • $(CFLAGS) : Una variable implícita para pasar en nuestras banderas del compilador como -Wall etc.
  • $? : nombres de todos los archivos de requisitos previos que son más nuevos que el destino. En la regla para main.o , $? se expandirá a main.c SI main.c ha sido modificado después main.o se había generado.
  • [email protected] : Este es el nombre del objetivo. Estoy usando esto para omitir escribir el nombre de la regla dos veces. En regla para main.o , [email protected] se expande a main.o .

Finalmente, las opciones -c y -o son gcc Opciones de para compilar/ensamblar archivos de origen sin vincular y especificar un nombre de archivo de salida respectivamente. Puede verificar esto ejecutando el man 1 gcc comando en su terminal.

¡Ahora intentemos ejecutar este archivo MAKE y esperemos que funcione en el primer intento!

                      
                        $ make
gcc -Wall -Wextra -c src/main.c -o main.o
gcc -Wall -Wextra -c src/calculator.c -o calculator.o
gcc -Wall -Wextra -c src/greeter.c -o greeter.o
gcc -Wall -Wextra main.o calculator.o greeter.o -o make_tutorial
                      
                    

Si observa detenidamente, cada paso de la compilación contiene todas las banderas que especificamos en el CFLAGS variable implícita. También podemos ver que los archivos de origen se obtuvieron automáticamente del directorio “src”. Esto ocurrió automáticamente porque especificamos “src” en el VPATH variable implícita.

Intentemos ejecutar el make_tutorial binario y verifique si todo funciona según lo previsto.

                      
                        $ ./make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!

Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is  multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.
                      
                    

vía GIPHY

Mejorando el archivo MAKE

“¿Qué hay que mejorar?”
Ejecutemos el ls comando puedes verlo por ti mismo 😉

                      
                        $ ls --group-directories-first -1
src
calculator.o
greeter.o
main.o
Makefile
make_tutorial
                      
                    

¿Ve los artefactos de compilación (archivos de objetos)? Sí, pueden desordenar las cosas para peor. Usemos nuestro directorio de compilación y reduzcamos este desorden.

A continuación se muestra el archivo MAKE modificado:

                      
                        CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build


make_tutorial : main.o calculator.o greeter.o
        $(CC) $(CFLAGS) $? -o [email protected]

build/main.o : main.c
        mkdir build
        $(CC) $(CFLAGS) -c $? -o [email protected]

build/calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o [email protected]

build/greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o [email protected]
                      
                    

Aquí, hice un cambio simple: agregué el build/ cadena antes de cada regla que genera un archivo de objeto. Esto colocará cada archivo de objeto dentro del directorio “construir”. También agregué “construir” al VPATH variable.

Si miras de cerca, nuestro primer objetivo de compilación es make_tutorial . Pero no será el blanco que es pedantemente el primero. El primer objetivo cuya receta se ejecuta es main.o (o mejor build/main.o ). Por lo tanto, agregué el comando “mkdir build” como una receta en el main.o objetivo.

Si no creara el directorio “compilar”, obtendría el siguiente error:

                      
                        $ make
gcc -Wall -Wextra -c src/main.c -o build/main.o
Assembler messages:
Fatal error: can't create build/main.o: No such file or directory
make: *** [Makefile:12: build/main.o] Error 1
                      
                    

Ahora que hemos modificado nuestro archivo make, eliminemos los artefactos de compilación actuales junto con el binario compilado y volvamos a ejecutar la utilidad make.

                      
                        $ rm -v *.o make_tutorial
removed 'calculator.o'
removed 'greeter.o'
removed 'main.o'
removed 'make_tutorial'

$ make
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o make_tutorial
                      
                    

Esto compilado perfectamente! Si observa detenidamente, ya habíamos especificado el directorio “compilar” en el VPATH variable, lo que hace posible que la utilidad make busque nuestros archivos de objetos dentro del directorio “build”.

Nuestros archivos fuente y de encabezado se encontraron automáticamente en el directorio “src” y los artefactos de compilación (archivos de objetos) se mantuvieron dentro y se vincularon desde el directorio “compilar”, tal como pretendíamos.

Adición de destinos .PHONY

Podemos llevar esta mejora un paso más allá. Agreguemos los objetivos “make clean” y “make run”.

A continuación se muestra nuestro makefile final:

                      
                        CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build


build/bin/make_tutorial : main.o calculator.o greeter.o
        mkdir build/bin
        $(CC) $(CFLAGS) $? -o [email protected]

build/main.o : main.c
        mkdir build
        $(CC) $(CFLAGS) -c $? -o [email protected]

build/calculator.o : calculator.c
        $(CC) $(CFLAGS) -c $? -o [email protected]

build/greeter.o : greeter.c
        $(CC) $(CFLAGS) -c $? -o [email protected]


.PHONY = clean
clean :
        rm -rvf build


.PHONY = run
run: make_tutorial
        ./build/bin/make_tutorial
                      
                    

Todo sobre los objetivos de compilación es igual, excepto por un cambio en el que especifico que quiero que el make_tutorial archivo ejecutable binario colocado dentro del build/bin/ directorio.

Entonces, puse .PHONY variable a clean para especificar que clean no es un archivo del que deba preocuparse la utilidad make. Es… falso. Bajo la clean objetivo, especifico lo que se debe eliminar para “limpiarlo todo”.

hago lo mismo para el run objetivo. Si eres un desarrollador de Rust, te gustará este patrón. Como el cargo run comando, uso el make run comando para ejecutar el binario compilado.

Para que ejecutemos el make_tutorial binario, debe existir. Así que lo agregué al requisito previo para el run objetivo.

Corramos make clean primero y luego correr make run ¡directamente!

                      
                        $ make clean
rm -rvf build
removed 'build/greeter.o'
removed 'build/main.o'
removed 'build/calculator.o'
removed 'build/bin/make_tutorial'
removed directory 'build/bin'
removed directory 'build'

$ make run
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
mkdir build/bin
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o build/bin/make_tutorial
./build/bin/make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!

Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is  multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.
                      
                    

Como puede ver aquí, no ejecutamos el make Comando para compilar nuestro proyecto primero. Al ejecutar el make run , la compilación estuvo a cargo. Entendamos cómo sucedió.

Al ejecutar el make run comando, la utilidad make primero busca en el run objetivo. Un requisito previo para la run target es nuestro archivo binario que compilamos. Entonces nuestro make_tutorial El archivo binario se compila primero.

los make_tutorial tiene sus propios requisitos previos que se colocan dentro del build/ directorio. Una vez que se compilan esos archivos de objetos, nuestro make_tutorial se compila el binario; finalmente, la utilidad Make vuelve a la run destino y el archivo binario ./build/bin/make_tutorial es ejecutado.

tanta elegancia mucho wow

Conclusión

Este artículo cubre los conceptos básicos de un archivo MAKE, un archivo del que depende la utilidad make, para simplificar la compilación de su repositorio de software. Esto se hace comenzando desde un Makefile básico y construyéndolo a medida que crecen nuestras necesidades.

Related Posts