Skip to article frontmatterSkip to article content

Makefiles

Estructurando a proyectos

El receterio de cocina

Siendo que make se usa para literalmente hornear nuestros programas, este apunte está construido de forma que nos ayude a construir Makefiles razonables para automatizar la compilación de programas más grandes que un “Hola Mundo” ;-).

Mucha de la funcionalidad avanzada está puesta como una receta, de forma que podamos leer de que se trata cuando tengamos que ver que pasa con algún makefile ya hecho.

En particular porque el manual es colosal Manual GNU/Make

Traducido y adaptado del Tutorial de Makefile escrito por Chase Lambert en chaselambda/makefiletutorial

El ciclo de un programa en C

Lo que conocemos como la compilación de un programa C, es una de las cuatro etapas que pasa un programa para ser ejecutable. Actualmente, el compilador se encarga de todas las etapas que le sea posible. En especial cuando se trate

Primeros pasos

¿Por qué existen los Makefiles?

Los Makefiles existen por la misma razón que los scripts de Bash, la automatización. Solo que en este caso, la herramienta está dedicada a la construcción y ensamblado de cualquier cosa que requiera de múltiples pasos y configuración. Se usa generalmente para programas compilados y enlazados, pero se puede utilizar para cualquier cosa que podamos llamar desde la consola, como por ejemplo, la creación del archivo PDF de este apunte.

Hemos hablado antes de que los programas son como recetas de cocina, pero los Makefiles son literalmente las recetas para hornear nuestro programa. Contiene las instrucciones para transformar el código en un programa. Esta herramienta viene con un agregado para acelerar la cocción, el trabajo únicamente será hecho en aquellos archivos que tengan cambios exclusivamente.

Esta herramienta tiene muchos años ya (1979) y fue creada para que la construcción de programas complejos no demore mucho tiempo. En los ejemplos y trabajos de la cátedra, el proceso es instantáneo, pero pensando en proyectos grandes, como el kernel de Linux, el proceso de compilación demorar tanto que se lo utiliza como medida dela capacidad de cómputo[1] de una computadora.

¿Hay alternativas a make?

No solo existen alternativas directas, SCons, CMake, Bazel y Ninja, por nombrar los más populares, algunos entornos de desarrollo como Microsoft Visual Studio tienen sus propias herramientas de compilación. Por otro lado, para el lenguaje de programación Java están las herramientas Ant, Maven y Gradle. Y otros lenguajes como Go y Rust tienen sus propias herramientas de compilación.

Los lenguajes interpretados como Python, Ruby y Javascript no requieren un análogo a Makefiles, cuando sus archivos de programa cambian, no es necesario compilar nada, simplemente se ejecuta directamente la versión más reciente del archivo (podemos decir que la tarea de make ya está integrada en el lenguaje).

Las versiones y tipos de make

Hay una variedad de implementaciones de make, en especial si tenemos en cuenta que tiene 43 años de vida. Este apunte funcionará en cualquier versión que estés usando, ya que apuntaremos al mínimo común denominador de funcionalidad. Sin embargo, está escrita específicamente para GNU make, que es la estándar en Linux, MacOS y las herramientas que utilizaremos en Windows. Todos los ejemplos funcionan para las versiones 3 y 4 de make, que son casi equivalentes salvo algunas diferencias esotéricas que no vamos a ver.

Instalación

No es necesario instalar específicamente make, este ya viene como parte del kit de herramientas del compilador, por lo que refiéranse a la guía de instalación especifica antes de seguir con este apunte.

Pero pueden verificar que está todo en orden ejecutando.

$> make

Debieran ver...

`make`: *** No targets specified and no makefile found.  Stop.

Que indica que la herramienta está lista para ser configurada.

Ejecución de los ejemplos

Para cada ejemplo, pon el contenido en un archivo llamado Makefile, y en ese directorio ejecuta el comando make. Comencemos con el “Hola Mundo” de los Makefiles:

hola:
	echo "hola mundo"

Aquí está la salida de la ejecución del ejemplo anterior:

$> make
echo "hola mundo"
hola mundo

Este es un ejemplo simple, pero como ves, es simplemente una forma de llamar a otros programas, en donde veremos como salida, el comando ejecutado y luego la salida del mismo.

Sintaxis del Makefile

Un Makefile consiste en un conjunto de reglas. Una regla generalmente tiene el siguiente aspecto:

objetivo: prerequisitos
	comando
	comando
	comando

Nos podemos imaginar que el objetivo es cualquier paso intermedio y final en una receta de cocina, los comandos como las operaciones individuales en la receta y los prerrequisitos como todo aquello que sea posible hacer por separado, del que dependa alguno de los pasos. Por ejemplo, cocinar unas galletitas para hornear.

bandeja_galletitas_crudas: bandeja_en_mantecada masa_preparada
	hacer_galletitas bandeja masa --en=bandeja_galletitas_crudas
bandeja_galletitas: bandeja_galletitas_crudas
	hornear 20min 250C bandeja_galletitas_crudas

De este ejemplo podemos extraer un detalle interesante, ¿en qué se diferencia una receta de este tipo de una en la que todos los pasos son una secuencia completa?

Esta herramienta simplifica la forma de dividir las tareas, dedicándose a la coordinación en la construcción de las piezas y su ensamblado final.

Pero al igual que la compilación de un programa, si es de un solo archivo, o un solo paso; mucho sentido no tiene armar un Makefile si podemos llamar directamente la herramienta que hace el trabajo, como venimos utilizando gcc hasta ahora, concretamente.

Un ejemplo simple

El siguiente Makefile tiene tres reglas separadas. Cuando ejecute make blah en la terminal, construirá un programa llamado blah en una serie de pasos:

blah: blah.o
	cc blah.o -o blah # Tercero
blah.o: blah.c
	cc -c blah.c -o blah.o # Segundo
blah.c:
	echo "int main() { return 0; }" > blah.c # Primero

Objetivo ‘implícito’

El siguiente makefile tiene un único objetivo, llamado some_file. El objetivo por defecto es el primer objetivo, por lo que en este caso some_file se ejecutará.

some_file:
	echo "Esta línea siempre imprime"

Como make no “ve” el archivo some_file, el objetivo no es alcanzado y make seguirá ejecutando sus comandos.

¿Qué pasa cuando lo ejecutamos dos veces?

En el siguiente ejemplo, al ejecutar make some_file por segunda vez, veremos algo como make: 'some_file' is up to date.. Esto pasa porque some_file es creado dentro del objetivo, y como se llega al objetivo, no es necesario volver a modificarlo.

Esta es una de las características más importantes de la herramienta, si la “dependencia” no cambió, entonces, no es necesario volver a completar el paso.

Dependencias

Aquí, el objetivo some_file “depende” de other_file. Cuando ejecutamos make, el objetivo por defecto (some_file, ya que es el primero) será llamado. Primero mirará su lista de dependencias, y si alguna de ellas es más antigua, primero ejecutará los objetivos de esas dependencias, y luego se ejecutará a sí mismo. La segunda vez que se ejecute, ningún objetivo se ejecutará porque ambos objetivos existen.

some_file: other_file
	echo "Esto va después, porque depende de other_file"
	touch some_file

other_file:
	echo "Esto va primero"
	touch other_file

La noción de dependencia es importante, en el siguiente Makefile, ambos objetivos se ejecutarán, ya que la dependencia de some_file con respecto de other_file nunca es satisfecha, ya que el archivo no es creado.

some_file: other_file
	touch some_file

other_file:
	echo "Tipo nada"

Empezando de cero con clean

El objetivo clean es uno muy común y se utiliza para limpiar las salidas de los objetivos, esto es usado para “empezar de cero”. Este objetivo no lleva un nombre especial o reservado, y lo podríamos llamar “limpiar”, pero es mejor apegarnos al nombre en inglés para mantener consistencia.

some_file:
	touch some_file

clean:
	rm -f some_file`

Variables

Las variables solo pueden contener texto. Lo más común es utilizar :=, pero = también funciona.

Hay más sobre este tema por acá [variables parte 2](#7.Variables parte 2|outline).

Acá tenés un ejemplo con variables:

files := file1 file2
some_file: $(files)
	echo "Mirá a esta variable!: " $(files)
	touch some_file

file1:
	touch file1
file2:
	touch file2

clean:
	rm -f file1 file2 some_file

Podes referenciar las variables con ${} o $()

x := coso

all:
	echo $(x)
	echo ${x}

Demás está decir que es muy recomendable usar nombres razonables para las variables

Objetivos

El objetivo all

¿Estás construyendo múltiples objetivos sin dependencias entre sí y deseas ejecutarlos todos?

Es muy recomendable agregar al principio a este objetivo. Ya que de esta forma, será el llamado si no indicamos uno específico y de esta forma podemos compilar todo sin tener que indicarlo puntualmente cada vez.


all: one two three

one:
	touch one
two:
	touch two
three:
	touch three

clean:
	rm -f one two three

Objetivos múltiples

Cuando hay varios objetivos para una regla, los comandos se ejecutarán para cada objetivo, para saber cuál se está procesando, podemos utilizar la variable automática $@ que contiene el nombre del objetivo.

all: f1.o f2.o

f1.o f2.o:
	echo "ahora con" $@
# Equivalente a:
# f1.o:
#	echo f1.o
# f2.o:
#	echo f2.o

Es particularmente útil cuando tenemos que hacer lo mismo en varios archivos diferentes, pero no es la única forma.

Variables automáticas y comodines

Comodín * (wildcard)

Tanto * como % se llaman comodines en make, pero significan cosas totalmente diferentes. * busca en su sistema de archivos nombres de archivos que coincidan. Es muy recomendable que siempre esté envuelto en la función wildcard, porque de lo contrario podés caer en una trampa común que se describe un poco más abajo.

# Imprime información de cada archivo .c
print: $(wildcard *.c)
	ls -la  $?

El comodín * puede ser utilizado en el objetivo, prerrequisitos o en la función wildcard.

Pero, no es posible usarlo directamente en la creación de variables, y cuando no hay coincidencias, es dejado como está, a no ser que sea utilizado dentro de la función wildcard.

thing_wrong := *.o # No lo hagas. '*' no se expandirá
thing_right := $(wildcard *.o)

all: one two three four

# Falla, porque $(thing_wrong) es la cadena "*.o"
one: $(thing_wrong)

# Se queda como *.o si no hay archivos que coincidan con este patrón :(
two: *.o

# Funciona como cabría esperar. En este caso, no hace nada.
three: $(thing_right)

# Igual que la regla three
four: $(wildcard *.o)

Trampa con el comodín

El comodín %

El otro comodín, es realmente útil, pero es algo confuso debido a la variedad de situaciones en las que se puede utilizar.

Consulte estas secciones para ver ejemplos de su uso:

Variables automáticas

Hay más variabless automáticas, pero estas son las más comunes:

hey: one two
	# Da como resultado "hey", ya que este es el primer objetivo
	echo $@

	# Muestra a todos los prerrequisitos más nuevos que el objetivo
	echo $?

	# Muestra a todos los prerrequisitos
	echo $^

	touch hey

one:
	touch one

two:
	touch two

clean:
	rm -f hey one two

Fancy Rules

Reglas implícitas

A make le encanta la compilación de C. Y cada vez que expresa su amor, las cosas se vuelven confusas, y quizás lo más enrevesado de la herramienta son las reglas mágicas/automáticas.

make llama a estas reglas “implícitas”. Personalmente no estoy de acuerdo con esta decisión de diseño, y no recomiendo usarlas, pero se usan seguido y por lo tanto, es importante conocerlas. Aquí hay una lista de reglas implícitas:

Las variables importantes que utilizan las reglas implícitas son:

Veamos cómo podemos ahora construir un programa en C sin tener que decirle explícitamente a make cómo hacer la compilación:

CC = gcc    # Utilizamos gcc como compilador
CFLAGS = -g # Activar la información de depuración

# #1: blah se construye a través de la regla implícita del enlazador de C
# #2: blah.o se construye a través de la regla implícita de compilación de C,
#     porque blah.c existe

blah: blah.o

blah.c:
	echo "int main() { return 0; }" > blah.c

clean:
	rm -f blah*

Patrones de reglas estáticas

Las reglas de patrones estáticos son otra forma de escribir menos en un Makefile, pero yo diría que son más útiles y un poco menos “mágicas”. Esta es su sintaxis:

objetivos...: patron-objetivo: patrones-prereq ...
	comandos

La esencia es que el objetivo dado coincide con el patrón-objetivo (a través de un comodín %). Lo que se ha encontrado se denomina “tronco”, este se sustituye por patrones-prereq, para generar los prerrequisitos del objetivo.

Un caso de uso típico es compilar archivos .c en archivos .o. Esta es la forma manual:

objects = foo.o bar.o all.o
all: $(objects)

# Estos archivos se compilan mediante reglas implícitas
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
	echo "int main() { return 0; }" > all.c

%.c:
	touch $@

clean:
	rm -f *.c *.o all

Esta es la forma más eficiente, utilizando una regla de patrón estático:

objects = foo.o bar.o all.o
all: $(objects)

# Estos archivos son compilados por las reglas implicitas
# Sintaxis - targets ...: target-patterns: prereq-patterns ...
# En el caso del primer objetivo, foo.o, el patrón-objetivo coincide
#  con foo.o y establece que la "raíz" sea "foo".
# A continuación, sustituye el "%" de patrones-prereq por esa raíz

$(objects): %.o: %.c

all.c:
	echo "int main() { return 0; }" > all.c

%.c:
	touch $@

clean:
	rm -f *.c *.o all

Reglas y filtro de patrones estáticos

Si bien las funciones son introducidas más adelante, adelantaré lo que se puede hacer con ellas. La función filter se puede utilizar en las reglas de patrones estáticos para hacer coincidir los archivos correctos. En este ejemplo, hice las extensiones .raw y .result.

obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

.PHONY: all
all: $(obj_files)

$(filter %.o,$(obj_files)): %.o: %.c
	echo "target: $@ prereq: $<"
$(filter %.result,$(obj_files)): %.result: %.raw
	echo "target: $@ prereq: $<"

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files)

Reglas de los patrones

Las reglas de los patrones se utilizan a menudo, pero son bastante confusas. Puedes verlas de dos maneras:

Empecemos con un ejemplo:

# Definir una regla de patrón que compile cada archivo .c en un archivo .o
%.o : %.c
		$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

Las reglas de patrones contienen un ‘%’ en el objetivo. Este ‘%’ coincide con cualquier cadena no vacía, y los demás caracteres coinciden por sí mismos. El ‘%’ en un prerrequisito de una regla de patrón representa el mismo tronco que fue igualado por el ‘%’ en el objetivo.

Aquí hay otro ejemplo:

# Definir una regla de patrón que no tenga ningún
#   patrón en los prerrequisitos.
# Esto sólo crea archivos .c vacíos cuando es necesario.
%.c:
   touch $@

Reglas de dos puntos

Las Reglas de dos puntos se utilizan raramente, pero permiten definir múltiples reglas para el mismo objetivo. Si fueran dos puntos simples, se imprimiría una advertencia y solo se ejecutaría el segundo conjunto de comandos.

all: blah

blah::
	echo "Hola!"

blah::
	echo "Hola denuevo!"

Comandos y ejecución

Eco de comandos/Silencio

Añade una @ antes de un comando para evitar que se imprima. También puedes ejecutar make con -s para añadir una @ antes de cada línea

all:
	@echo "Esto no va a aparecer en la salida"
	echo "Pero eso si!"

Ejecución de comandos

Cada comando se ejecuta en un nuevo shell (o al menos el efecto es como tal)

all:
	cd ..
	# El cd anterior no afecta a esta línea,
		porque cada comando se ejecuta efectivamente en un nuevo shell
	echo `pwd`

	# Este comando cd afecta al siguiente porque están en la misma línea
	cd ..;echo `pwd`

	# Igual que antes, pero con la barra para dividir la linea en dos
	cd ..; \
	echo `pwd`

Shell por defecto

El shell por defecto es /bin/sh. Es posible cambiar esto modificando la variable SHELL:

SHELL=/bin/bash

cool:
	echo "Hola desde bash"

Gestión de errores con -k, -i, y -

Añadí -k cuando ejecutes make para que siga funcionando incluso ante errores. Útil si querés ver todos los errores de make a la vez. (--keep-going)

Agregá un - antes de un comando para suprimir el error

Y con -i al llamar a make para que esto pase con cada comando. (--ignore-errors)

one:
	# Este error se imprimirá pero se ignorará, make continuará ejecutándose
	-false
	touch one

Uso recursivo de make

Para llamar recursivamente a un makefile, usá el especial $(make) en lugar de make, ya que pasará las opciones de la llamada original de make por usted y no se verá afectado por ellas.

new_contents = "hello:\n\ttouch a inside_file"
all:
	mkdir -p subdir
	printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
	cd subdir && $(MAKE)

clean:
	rm -rf subdir

Usar export para un make recursivo

La directiva export toma una variable y la hace accesible a los comandos sub-make. En este ejemplo, cooly se exporta de tal manera que el makefile en el subdirectorio puede usarlo.

Nota: export tiene la misma sintaxis que en la terminal de la consola (sh, bash), pero no están relacionados (aunque son similares en su función)

new_contents = "hello:\n\\techo \$$(cooly)"

all:
	mkdir -p subdir
	echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
	@echo "---MAKEFILE CONTENTS---"
	@cd subdir && cat makefile
	@echo "---END MAKEFILE CONTENTS---"
	cd subdir && $(MAKE)

# Tenga en cuenta que las variables y las exportaciones.
#	Se establecen/afectan globalmente.
cooly = "¡El subdirectorio puede verme!"
export cooly
# Esto anularía la línea anterior: unexport cooly

clean:
	rm -rf subdir

Es necesario exportar las variables para que se ejecuten también en el shell.

one=esto sólo funcionará localmente
export two=podemos ejecutar subcomandos con esto

all:
	@echo $(one)
	@echo $$one
	@echo $(two)
	@echo $$two

Mientras que .EXPORT_ALL_VARIABLES lo hace por tí, para todas las variables.

.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"

cooly = "¡El subdirectorio puede verme!"
# Esto anularía la línea anterior: unexport cooly

all:
	mkdir -p subdir
	echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
	@echo "---MAKEFILE CONTENTS---"
	@cd subdir && cat makefile
	@echo "---END MAKEFILE CONTENTS---"
	cd subdir && $(MAKE)

clean:
	rm -rf subdir

Argumentos a make

De la muy bonita lista en el manual lista de opciones, peguenlé una mirada a --dry-run, --touch y --old-file. Además de ver en detalle que hace -i y -k.

El otro tipo de argumentos son los objetivos a ejecutar, y podemos ejecutar varios y en orden, solo tenemos que indicarlos uno a continuación de otro.

Por ejemplo:

$> make clean run test

Que limpia los archivos generados previamente (clean), compila y ejecuta el programa (run) y finalmente corre los test’s.

Variables parte 2

Sabores y modificaciones

Hay dos sabores de variables

# Variable recursiva. Esto imprimirá "tardio" a continuación
uno = uno ${later_variable}
# Variable simplemente expandida. Esto no imprimirá "tardio" abajo
dos := dos ${later_variable}

later_variable = tardio

all:
	echo $(uno)
	echo $(dos)

La expansión simple (utilizando :=) permite añadir a una variable. Las definiciones recursivas darán un error de bucle infinito.

uno = hola
uno ?= no será asignado
dos ?= será asignado

all:
	echo $(uno)
	echo $(dos)

Los espacios al final de una línea no se eliminan, pero sí los del principio. Para crear una variable con un solo espacio, utilice $(nullstring)

con_espacios = alo   # con_espacios tiene 3 espacios luego de "alo"
after = $(con_espacios)there

nullstring =
space = $(nullstring) # Hace una variable con un solo espacio.

all:
	echo "$(after)"
	echo inicio"$(space)"fin

Una variable indefinida es en realidad una cadena vacía, por lo que emplear una variable desconocida no producirá un error.

all:
	# Las variables no definidas son sólo cadenas vacías!
	echo $(nowhere)

Y podes usar += para concatenar.

foo := start
foo += more

all:
	echo $(foo)

La substitución de cadenas es una forma muy útil para modificar el contenido de las variables. Para más información, consulten las páginas del manual Text Functions y Filename Functions de GNU/make.

Argumentos de linea de comandos y anulaciones

Puedes anular las variables que provienen de la línea de comandos utilizando override. Aquí ejecutamos make con make option_one=hi

# Supera los argumentos de la línea de comandos
override option_one = did_override
# No anula los argumentos de la línea de comandos
option_two = not_override
all:
	echo $(option_one)
	echo $(option_two)

Puesto de otra manera, no importa lo que venga de afuera, el valor siempre va a ser el que este definido internamente.

Lista de comandos y definición

define” es en realidad sólo una lista de comandos. No tiene nada que ver con ser una función. Tené en cuenta aquí que es un poco diferente a tener un punto y coma entre los comandos, porque cada uno se ejecuta en un shell separado, como se espera.

one = export blah="Estaba definida!"; echo $$blah

define two
export blah=set
echo $$blah
endef

# One y two son diferentes.

all:
	@echo "Esto muestra 'Estaba definida!'"
	@$(one)
	@echo "Esto no muestra 'Estaba definida!', ya que cada comando se ejecuta en un shell separado."
	@$(two)

Variables específicas de los objetivos

Las variables pueden ser asignadas para objetivos específicos:

all: uno = copado

all:
	echo uno esta definida: $(uno)

other:
	echo uno esta definida: $(uno)

Variables específicas a patrones

Así como podemos definir variables para objetivos específicos, podemos también asignarlas a patrones.

%.c: one = buenardo

blah.c:
	echo uno esta definida: $(one)

other:
	echo uno esta definida: $(one)

La parte condicional de los Makefiles

Un if/else clásico

foo = ok

all:
ifeq ($(foo), ok)
	echo "foo vale ok"
else
	echo "nope"
endif

[]{#anchor-13}Verificar si una variable esta vacía

nullstring =
foo = $(nullstring) # fin de linea; noten que hay un espacio acá

all:
ifeq ($(strip $(foo)),)
	echo "foo está vacío después de ser limpiada"
endif
ifeq ($(nullstring),)
	echo "nullstring ni siquiera tiene espacios"
endif

Verificar si una variable está definida

ifdef no expande las referencias a variables, solo se encarga de verificar que existen.

bar =
foo = $(bar)

all:
ifdef foo
	echo "foo esta definida"
endif
ifndef bar
	echo "pero bar no lo esta"
endif

$(makeflags)

Este ejemplo muestra como verificar opciones (flags) con findstring y MAKEFLAGS. Estos son útiles para indicar alguna variación en lo que se desea construir.

Para el siguiente ejemplo, utilizá -i para ver el texto en el echo.

bar =
foo = $(bar)

all:
# Buscando por la opción "-i". MAKEFLAGS es solo una lista de caracteres
# Y en estes caso, busca por "i"
ifneq (,$(findstring i, $(MAKEFLAGS)))
	echo "la opcion i fue agregada a MAKEFLAGS"
endif

Funciones

Primeras funciones

Las funciones son principalmente para procesamiento de texto, después de todo, la idea de esta herramienta es llamar a otras. Estas se llaman con la siguiente sintaxis:

$(fn, argumentos) o ${fn, argumentos}.

Podes hacer make propio utilizando call para llamar funciones ‘de libreria’. Y make tiene una cantidad interesante de funciones integradas.

Substituciones textuales (subst)

bar := ${subst no, absolutamentoe, "Yo no soy Paturuzu"}
all:
	@echo $(bar)

Si querés reemplazar espacio o comas, usá variables:

comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
	@echo $(bar)

No incluyas espacios en los argumentos antes del primero, esto será visto como parte de la cadena.

comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
	# La salida es is ", a , b , c". Mirá los espacios que se introdujeron
	@echo $(bar)

Substituciones usando patrones (patsubst)

$(patsubst patron,reemplazo,texto) does the following:

Según el manual:

"Busca palabras separadas por espacios en blanco en el texto que coinciden con el patrón y las reemplaza con el ‘reemplazo’ indicado. Aquí, el patrón puede contener un “%”, que actúa como comodón y coincide con cualquier número de caracteres dentro de una palabra. Si el reemplazo también contiene un “%”, este se reemplaza por el texto que coincidió con el “%” en el patrón. Solo el primero “%” en el patrón y el reemplazo se trata de esta manera; cualquier “%” posterior no se modifica.

(GNU docs)

La referencia de substitución $(texto:patron=reemplazo) es un atajo para esto.

Existe otra abreviación que solo reemplaza sufijos, $(texto:sufijo=reemplazo). No es necesario el comodín “%”.

Footnotes
  1. A julio del 2022, el primer puesto lo tiene una computadora doble AMD EPYC 7773X de 64 núcleos, para hacer un total de ¡128 núcleos! Completando la tarea en 138 segundos. Una computadora más mundana, suele llevar varias horas.

  2. NdT: stem

References
  1. Feldman, S. I. (1979). Make — a program for maintaining computer programs. Software: Practice and Experience, 9(4), 255–265. 10.1002/spe.4380090402