6 puntos por GN⁺ 2025-06-21 | 1 comentarios | Compartir por WhatsApp
  • Makefile es una herramienta que simplifica la automatización de builds en C/C++ y la gestión de dependencias
  • Usa un método de detección de archivos modificados mediante marcas de tiempo, por lo que solo ejecuta la compilación cuando hace falta
  • Explica la estructura clave como reglas (rule), comandos (command) y dependencias (prerequisite) con ejemplos
  • También cubre de forma práctica funciones avanzadas como variables automáticas, reglas de patrón y expansión de variables
  • Presenta la importancia de la escalabilidad y el mantenimiento mediante una plantilla práctica de Makefile para proyectos de tamaño medio

Introducción a la guía tutorial de Makefile

  • Makefile es una herramienta clave para automatizar el build de proyectos y gestionar dependencias
  • Aunque puede parecer compleja al principio por sus múltiples reglas implícitas y símbolos, esta guía organiza los puntos principales con ejemplos breves y ejecutables directamente
  • Cada sección permite comprender el contenido a partir de ejemplos prácticos

Primeros pasos

Para qué existe Makefile

  • Makefile se usa en programas grandes para recompilar solo las partes modificadas
  • Además de C/C++, existen herramientas de build específicas para muchos lenguajes, pero Make se usa en escenarios generales de build
  • La lógica central consiste en detectar los archivos modificados y ejecutar solo las tareas necesarias

Sistemas de build alternativos a Make

  • En C/C++: hay varias opciones como SCons, CMake, Bazel y Ninja
  • En Java: Ant, Maven, Gradle, etc.
  • Go, Rust y TypeScript también ofrecen sus propias herramientas de build
  • En lenguajes interpretados como Python, Ruby o JavaScript, no se requiere compilación, así que la necesidad de una gestión aparte como Makefile es menor

Versiones y tipos de Make

  • Existen varias implementaciones de Make, pero esta guía está optimizada para GNU Make (usado principalmente en Linux y MacOS)
  • Los ejemplos son compatibles tanto con GNU Make 3 como con la versión 4

Cómo ejecutar los ejemplos

  • Después de instalar make en la terminal, guarda cada ejemplo en un archivo Makefile y ejecuta el comando make
  • Las líneas de comando dentro del Makefile deben ir indentadas obligatoriamente con tabulaciones

Sintaxis básica de Makefile

Estructura de una regla (Rule)

  • objetivo: dependencia(s)

    • comando
    • comando
  • Objetivo: nombre del archivo resultante del build (normalmente uno)

  • Comando: el script de shell que se ejecuta realmente (comienza con tabulación)

  • Dependencia: lista de archivos que deben estar listos antes de construir el objetivo


La esencia de Make

Ejemplo Hello World

hello:  
	echo "Hello, World"  
	echo "This line will print if the file hello does not exist."  
  • El objetivo hello no tiene dependencias y ejecuta 2 comandos
  • Al ejecutar make hello, si el archivo hello no existe, se ejecutan los comandos. Si ya existe, no se ejecutan
  • En general, se escribe haciendo coincidir el objetivo con el nombre del archivo

Ejemplo básico de compilación de un archivo C

  1. Crear el archivo blah.c (con el contenido int main() { return 0; })
  2. Escribir el siguiente Makefile
blah:  
	cc blah.c -o blah  
  • Al ejecutar make, si el objetivo blah no existe, se compila y se crea el archivo blah
  • Aunque blah.c cambie después, no se recompila automáticamente → hace falta agregar una dependencia

Cómo agregar dependencias

blah: blah.c  
	cc blah.c -o blah  
  • Ahora, si blah.c cambió recientemente, el objetivo blah se vuelve a construir
  • Se usan las marcas de tiempo de los archivos como criterio para detectar cambios
  • Si las marcas de tiempo se manipulan manualmente, puede comportarse distinto a lo esperado

Más ejemplos

Ejemplo de objetivos y dependencias encadenados

blah: blah.o  
	cc blah.o -o blah   
  
blah.o: blah.c  
	cc -c blah.c -o blah.o   
  
blah.c:  
	echo "int main() { return 0; }" > blah.c   
  • Las dependencias se siguen como una estructura en árbol y se automatiza la generación en cada etapa

Ejemplo de objetivo que siempre se ejecuta

some_file: other_file  
	echo "This will always run, and runs second"  
	touch some_file  
  
other_file:  
	echo "This will always run, and runs first"  
  • Como other_file no se crea como archivo real, el comando de some_file se ejecuta cada vez

Make clean

  • El objetivo clean se usa con frecuencia para borrar los artefactos del build
  • No es una palabra reservada especial en Make; hay que definirlo manualmente como comando
  • Si existe un archivo llamado clean, puede causar confusión, por lo que se recomienda usar .PHONY

Ejemplo:

some_file:   
	touch some_file  
  
clean:  
	rm -f some_file  

Manejo de variables

  • Las variables siempre son cadenas de texto.
  • Normalmente se recomienda :=, aunque existen varias formas de asignación como =, ?= y +=
  • Ejemplo de uso:
files := file1 file2  
some_file: $(files)  
	echo "Look at this variable: " $(files)  
	touch some_file  
  
file1:  
	touch file1  
file2:  
	touch file2  
  
clean:  
	rm -f file1 file2 some_file  
  • Las variables se referencian como $(variable) o ${variable}
  • Las comillas en un Makefile no tienen significado para Make en sí (aunque sí pueden ser necesarias en comandos de shell)

Gestión de objetivos

Objetivo all

  • Para ejecutar varios objetivos a la vez, se le da ese rol al primer objetivo (el predeterminado)
all: one two three  
  
one:  
	touch one  
two:  
	touch two  
three:  
	touch three  
  
clean:  
	rm -f one two three  

Múltiples objetivos y variables automáticas

  • Es posible ejecutar comandos individuales para varios objetivos. $@ contiene el nombre del objetivo actual
all: f1.o f2.o  
  
f1.o f2.o:  
	echo $@  

Variables automáticas y comodines

Comodín *

  • * busca directamente nombres en el sistema de archivos
  • Se recomienda usarlo siempre envuelto en la función wildcard
print: $(wildcard *.c)  
	ls -la  $?  
  • No uses * directamente en la definición de variables
thing_wrong := *.o  
thing_right := $(wildcard *.o)  

Comodín %

  • Se usa sobre todo en reglas de patrón, donde permite extraer y expandir el patrón especificado

Reglas avanzadas

Reglas implícitas (Implicit)

  • Make incluye varias reglas predeterminadas ocultas relacionadas con builds de C/C++
  • Variables representativas: CC, CXX, CFLAGS, CPPFLAGS, LDFLAGS, etc.
  • Ejemplo en C:
CC = gcc   
CFLAGS = -g   
  
blah: blah.o  
  
blah.c:  
	echo "int main() { return 0; }" > blah.c  
  
clean:  
	rm -f blah*  

Static Pattern Rules

  • Permiten escribir de forma concisa varias reglas que siguen el mismo patrón
objects = foo.o bar.o all.o  
all: $(objects)  
	$(CC) $^ -o all  
  
$(objects): %.o: %.c  
	$(CC) -c $^ -o $@  
  
all.c:  
	echo "int main() { return 0; }" > all.c  
  
%.c:  
	touch $@  
  
clean:  
	rm -f *.c *.o all  

Static Pattern Rules + función filter

  • Con filter, se pueden seleccionar solo los elementos que coincidan con un patrón de extensión específico
obj_files = foo.result bar.o lose.o  
src_files = foo.raw bar.c lose.c  
  
all: $(obj_files)  
.PHONY: all  
  
$(filter %.o,$(obj_files)): %.o: %.c  
	echo "target: $@ prereq: $

1 comentarios

 
GN⁺ 2025-06-21
Opiniones de Hacker News
  • Alguien contó que en 1985 vio personalmente a una persona en el laboratorio de Graphics de Boston University crear un renderizador 3D para animación usando Makefile. Esa persona era programadora de Lisp, estaba trabajando en generación temprana de procedimientos y en un sistema de actores 3D, y creó un Makefile realmente elegante de unas 10 líneas. La estructura generaba automáticamente cientos de animaciones usando simples dependencias por fecha de archivos. Las formas 3D de cada cuadro se creaban en Lisp, y Make se encargaba de generar los cuadros. En 1985, a diferencia de hoy que damos por sentado el 3D y la animación, todo el mundo quedaba asombrado; recuerda que esa persona era Brian Gardner, quien después estuvo a cargo del renderizador 3D de Iron Giant y Coraline

    • Alguien expresó curiosidad sobre si se trata de la persona que aparece en 3d-consultant.com/bio.html

    • Preguntan si efectivamente se refería a la película Coraline

  • Presentan algunas banderas útiles de Make que no son tan conocidas

    • --output-sync=recurse -j10: significa que agrupa stdout/stderr y los imprime hasta que termina el trabajo de cada objetivo; de otro modo, los logs se mezclan y se vuelven difíciles de analizar
    • En sistemas ocupados o entornos multiusuario, en vez de -j se puede usar --load-average para controlar la carga del sistema durante el paralelismo (make -j10 --load-average=10)
    • La opción --shuffle, que mezcla aleatoriamente la planificación de objetivos de compilación, es útil en entornos de CI para detectar problemas de dependencias en el Makefile
    • Se menciona la idea de recopilar formalmente las distintas opciones de make e incluirlas en texto o documentación dentro del programa para facilitar su uso

    • Explica que la opción que más usa es la bandera -B, para forzar una compilación completa

    • Como ha visto con frecuencia problemas provocados por make -j en máquinas DOS, percibe ese fenómeno como un bug

    • Pregunta si los problemas de paralelización en sistemas ocupados o multiusuario no deberían ser responsabilidad del scheduler del OS

    • Aunque son banderas útiles, recomienda no usarlas fuera de proyectos privados para uno mismo porque estas opciones no son portables

  • Considera que saltarse .PHONY en un tutorial con el argumento de que no se usa es una excusa débil. Opina que lo correcto es enseñar a usar bien la herramienta

    • En su equipo usaban Make como task runner y tuvieron discusiones por agregar y mantener .PHONY en todas las recetas
    • Recomienda la guía de estilo de Makefile de Clark Grubb (clarkgrubb.com/makefile-style-guide)
    • Comparte experiencias con distintos estilos entre declarar .PHONY por receta o agruparlo todo una sola vez al inicio del archivo, y le gustaría que un linter lo hiciera cumplir
    • Después de leerlo, le parece un buen documento, pero hay varias cosas con las que no está de acuerdo
      • Aplicar -o pipefail a ciegas es problemático; puede romperse al usar grep y otros comandos en pipes, así que recomienda aplicarlo según el caso
      • Marcar objetivos que no son archivos con .PHONY es riguroso, pero casi siempre innecesario y solo vuelve más verboso el Makefile; prefiere usarlo solo cuando haga falta
      • Las recetas que generan varios archivos de salida antes usaban archivos dummy, pero desde GNU Make 4.3 ya existe soporte oficial para objetivos agrupados (ver aquí)
  • Afirman que Make es una herramienta especializada en compilar grandes codebases en C

    • A alguien le gusta usarlo como job runner por proyecto, pero piensa que Make no es adecuado como job runner y que incluso cosas como los condicionales se vuelven difíciles en esa estructura
    • También ha visto casos en los que falló al intentar envolver herramientas como Terraform
    • Otro opina que Make, más que un job runner, es una herramienta shell de propósito general que transforma scripts shell lineales en una forma declarativa basada en dependencias

    • También se plantea que ya no tiene sentido ver Make solo como una herramienta de compilación para codebases en C. Menciona que en los últimos 20 años se han desarrollado sistemas de build más robustos y claros, y que hace falta actualizar esa visión

    • Preguntan cuál sería un buen job runner. (Luego agrega una disculpa por haber confundido el significado de job runner)

  • Recomiendan just como una herramienta moderna para reemplazar las partes del Makefile que se vuelven complejas

    • just sirve bien como reemplazo de una lista de scripts shell, pero no puede sustituir la función esencial de Make de “ejecutar solo las reglas que necesitan volver a correrse”

    • Otras alternativas mencionadas son

    • Aunque esas herramientas alternativas se presentan como reemplazos de Make, piensa que en realidad son muy distintas y que incluso compararlas es difícil. El núcleo de Make está en generar artefactos y no recompilar lo que ya fue construido. En cambio, just cumple el papel de simple ejecutor de comandos

    • La ventaja de usar Make como ejecutor de comandos es la confiabilidad de ser una herramienta estándar instalada casi en todas partes. Aunque las alternativas estén mejor diseñadas, no siente la necesidad de usarlas por la carga de tener que instalarlas aparte

    • Usa Task en proyectos sencillos de hobby en C, pero todavía no sabe si también es adecuado para proyectos grandes (sitio oficial de Task)

  • Le parece interesante que recientemente CMake haya elegido ninja como predeterminado al considerar que Makefile no es adecuado para soporte de módulos de C++20 (guía de CMake)

    • En la práctica, es casi imposible definir estáticamente las dependencias entre objetivos, así que se adoptó un enfoque de análisis dinámico con herramientas como clang-scan-deps (slides técnicas)
    • En realidad, cree que esta limitación es una decisión del lado de CMake o un problema de falta de voluntarios para el generador de Makefiles. Señala que ninja tampoco puede soportar directamente módulos de C++ (issue relacionado), y que ninja incluso tiene menos funciones que Make y exige que todas las dependencias se especifiquen estáticamente

    • Opinan que la introducción misma de los módulos es compleja y confusa

  • Preguntan si alguien tiene experiencia usando tup. (documentación oficial)

    • tup es un sistema de build que detecta dependencias automáticamente a partir del acceso al sistema de archivos, por lo que puede aplicarse a cualquier compilador o herramienta
  • Se presenta como el creador y maintainer principal de la herramienta alternativa a Make llamada Task. Dice que la desarrolla desde hace más de 8 años y sigue evolucionando

    • También recomienda just como otra alternativa a Make (GitHub de just)

    • Como coincidencia curiosa, comenta que usa Task con frecuencia y que incluso esta mañana abrió un issue

  • Este tutorial tiene problemas peligrosos y sutiles

    • Al parsear opciones desde MAKEFLAGS, para manejar opciones largas o opciones cortas vacías hay que hacerlo así
      ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
    • Si se necesita compatibilidad con la vieja versión de make incluida por defecto en OS X, faltan bastantes funciones o se comportan de forma sutilmente distinta
    • Los demás problemas se omiten porque en su mayoría son typos o infracciones a buenas prácticas de estilo
    • Como referencia, load es más portable que guile, y en entornos de cross-compilation hay que especificar correctamente el compilador
    • Recomienda leer Paul’s Rules of Makefiles (aquí), el manual de GNU make (aquí) y manuales relacionados
    • También mantiene un proyecto demo simple de Makefile (demo en github)
  • Tiene la costumbre de incluir siempre un Makefile en cada repo de GitHub

    • Como es fácil olvidar los comandos, al guardarlos en un Makefile luego se pueden agregar pasos complejos sin problema, y con solo correr make se ejecuta de inmediato el comportamiento esperado para ese proyecto sin tener que recordarlo aparte