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 eltarget
.
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 paramain.o
,$?
se expandirá amain.c
SImain.c
ha sido modificado despuésmain.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 paramain.o
,[email protected]
se expande amain.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'.
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.