6 puntos por GN⁺ 2025-06-21 | 1 comentarios | Compartir por WhatsApp
  • Makefile se usa principalmente para automatizar la compilación de C/C++ en proyectos grandes, volviendo a ejecutar solo los pasos de build necesarios según los cambios en los archivos
  • Make compara los timestamps del sistema de archivos entre objetivos y dependencias, y ejecuta comandos solo cuando el objetivo no existe o una dependencia es más reciente
  • La regla básica consiste en targets: prerequisites y comandos indentados con tabulador; usar espacios puede hacer que make falle
  • El flujo de build se puede ampliar con variables, comodines, variables automáticas, reglas de patrón, condicionales, funciones, make recursivo, include, .PHONY y .DELETE_ON_ERROR
  • El ejemplo del Cookbook compila fuentes C/C++ de src/ en artefactos dentro de build/ y configura la gestión automática de dependencias con -MMD -MP y -include

Cuándo se necesita un Makefile

  • Makefile se usa para determinar qué archivos deben recompilarse en programas grandes
  • El caso central del tutorial es la compilación de archivos C/C++
  • Make también puede usarse fuera de la compilación para tareas que requieren ejecutar una serie de comandos según si cambiaron archivos
  • Como otras herramientas de build para C/C++, se presentan SCons, CMake, Bazel, Ninja
  • En Java existen Ant, Maven y Gradle, y lenguajes como Go, Rust y TypeScript cuentan con sus propias herramientas de build
  • Los lenguajes interpretados como Python, Ruby o JavaScript puro no necesitan recompilarse cuando cambian los archivos, por lo que una herramienta como Makefile no es indispensable

Modelo básico de ejecución de Make

  • Los ejemplos funcionan creando un archivo Makefile en una terminal donde make esté instalado y ejecutando make en ese directorio
  • La regla básica sigue esta forma
    • targets: prerequisites
    • command comenzando con un tabulador
  • El objetivo (target) suele ser un nombre de archivo, y el comando es el paso para crear ese objetivo
  • Las dependencias (prerequisites) son archivos que deben existir antes de ejecutar el comando, y también se les llama dependencies
  • Las líneas de comando en un Makefile deben estar indentadas obligatoriamente con un carácter tabulador; si se usan espacios, make falla

Comparación de tiempos entre objetivos y dependencias

  • make hello ejecuta el comando asociado al objetivo hello, pero si el archivo hello ya existe, no ejecuta el comando
  • El objetivo y el nombre del archivo están conectados directamente, y normalmente el comando de un objetivo crea un archivo con el mismo nombre
  • En la regla blah: blah.c, el comando de compilación solo se ejecuta si blah no existe o si blah.c es más reciente que blah
  • Make no rastrea directamente el historial de cambios, sino que usa los timestamps del sistema de archivos como heurística
  • Si modificas un archivo y luego cambias su hora de modificación a una fecha anterior, Make puede concluir erróneamente que el archivo no cambió

Objetivo por defecto y clean

  • Si no se especifica un objetivo al ejecutar make, se ejecuta como predeterminado el primer objetivo del Makefile
  • El objetivo all se usa para construir varios objetivos a la vez, y si se coloca como primera regla, basta con ejecutar make para compilar todo
  • clean suele usarse como objetivo para borrar artefactos de build, pero no es una palabra reservada especial de Make
  • Si clean no es el primer objetivo ni dependencia de otro objetivo, solo se ejecuta si se invoca explícitamente, como con make clean
  • Si realmente existe un archivo llamado clean, puede que el objetivo clean no se ejecute; más adelante esto se resuelve con .PHONY

Variables y manejo de cadenas

  • Las variables de Make solo pueden contener cadenas de texto, y el tutorial recomienda usar normalmente :=
  • Las referencias a variables usan la forma $(name) o ${name}
  • Las comillas simples y dobles no tienen un significado especial dentro de Make; solo son caracteres dentro de la variable
  • Hay dos formas principales de definir variables
    • =: variable recursiva, que expande buscando el valor cuando se usa
    • :=: variable de expansión simple, que expande en el momento de la definición solo los valores ya definidos
  • ?= asigna un valor solo si la variable todavía no está definida
  • += agrega un valor a la variable
  • Las variables no definidas se tratan como cadenas vacías
  • Los espacios al final de línea no se eliminan; para crear una variable que contenga un solo espacio se puede usar una técnica como $(nullstring)

Comodines y variables automáticas

  • En Make, tanto * como % se consideran comodines, pero tienen significados distintos
  • * busca nombres de archivo coincidentes en el sistema de archivos, y es más seguro envolverlo en la función wildcard, como en $(wildcard *.o), que usarlo directamente en una definición de variable
  • Si * no coincide con ningún archivo, puede quedarse tal cual si no está dentro de la función wildcard
  • % se usa en reglas y en ciertas funciones, y la parte coincidente se llama stem
  • Las variables automáticas más usadas son
    • $@: nombre del objetivo
    • $?: dependencias más recientes que el objetivo
    • $^: todas las dependencias
    • $<: primera dependencia

Reglas implícitas y reglas de patrón

  • Make tiene reglas implícitas para compilar C, por lo que el build puede avanzar incluso sin escribir explícitamente el comando de compilación
  • Algunas reglas implícitas representativas tienen esta forma
    • n.o se genera desde n.c con $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
    • n.o se genera desde n.cc o n.cpp con $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
    • n se genera desde n.o con un comando del linker
  • Las variables importantes en las reglas implícitas son CC, CXX, CFLAGS, CXXFLAGS, CPPFLAGS y LDFLAGS
  • Las reglas de patrón estático tienen la forma targets...: target-pattern: prereq-patterns ..., y sustituyen en el patrón de dependencia el stem que coincida en el objetivo
  • Las reglas de patrón incluyen % en el objetivo, como %.o: %.c, y pueden usarse como reglas implícitas creadas por ti
  • Las reglas con doble dos puntos :: permiten definir varias reglas para un mismo objetivo, pero se usan rara vez

Ejecución de comandos y manejo de errores

  • Si se antepone @ a un comando, ese comando no se imprime en pantalla
  • Si se ejecuta make -s, el comportamiento es como si cada línea tuviera @ al inicio
  • Cada línea de comando se ejecuta como si fuera en un shell nuevo, así que un cd .. en una línea no afecta a la siguiente
  • Para conservar el mismo estado del shell, hay que escribirlo en una sola línea, como cd ..; echo \pwd`` o continuarlo con backslash
  • El shell por defecto es /bin/sh, pero puede cambiarse con algo como SHELL=/bin/bash
  • Para usar el $ de variables del shell dentro de un Makefile, se debe escribir $$
  • El manejo de errores cambia según cómo se ejecute
    • make -k: sigue ejecutando otras tareas posibles aunque haya errores
    • - antes del comando: ignora el error de ese comando
    • make -i: aplica ignorar errores a todos los comandos
  • Si interrumpes Make con ctrl+c, Make elimina el objetivo más reciente que acaba de crear

Make recursivo y variables de entorno

  • Cuando se vuelve a invocar make desde dentro del Makefile, debe usarse la variable especial $(MAKE) en lugar de make
  • $(MAKE) transmite las flags de make y maneja adecuadamente sus efectos
  • Cuando Make inicia, convierte automáticamente las variables de entorno definidas al momento de ejecución en variables de Make
  • La directiva export establece variables en el entorno de shell de todas las recipes
  • Si quieres usar una variable también en un make hijo, debes hacer export de esa variable
  • Con .EXPORT_ALL_VARIABLES se pueden exportar todas las variables
  • Si se especifican varios objetivos a la vez, como make clean run test, se ejecutan en el orden clean, run, test

Condicionales y funciones

  • Los condicionales se componen de ifeq, ifneq, ifdef, ifndef, else y endif
  • ifdef no expande la referencia de la variable, sino que solo comprueba si está definida
  • Se puede usar $(MAKEFLAGS) junto con findstring para verificar si se pasó una flag como make -i
  • Las funciones de Make se usan principalmente para procesamiento de texto, y se invocan con la forma $(fn, arguments) o ${fn, arguments}
  • Las funciones principales son
    • subst: sustitución de cadenas
    • patsubst: sustitución de palabras que coincidan con un patrón
    • foreach: transformación de una lista de palabras separadas por espacios
    • if: si el primer argumento no está vacío, usa el segundo; si no, el tercero
    • call: invoca una variable como si fuera una función y usa parámetros como $(1), $(2)
    • shell: invoca al shell, pero convierte saltos de línea en espacios
    • filter: selecciona de una lista los elementos que coinciden con cierto patrón
    • filter-out: selecciona los elementos que no coinciden con cierto patrón

include, vpath, .PHONY, .DELETE_ON_ERROR

  • include filenames... hace que se lean uno o más Makefiles adicionales
  • include es útil para leer Makefiles generados por opciones como -M de gcc
  • vpath <pattern> <directories> especifica directorios donde buscar ciertos archivos de dependencia
  • La variable VPATH permite definir un comportamiento de búsqueda similar a nivel global
  • El backslash \ se usa para dividir comandos largos en varias líneas
  • Si agregas un objetivo a .PHONY, Make no lo confundirá con un archivo del mismo nombre
  • .DELETE_ON_ERROR elimina el objetivo de una regla si un comando de esa regla devuelve un estado de salida distinto de 0
  • .DELETE_ON_ERROR no se aplica solo al objetivo inmediato anterior como PHONY, sino a todos los objetivos

Ejemplo del Makefile Cookbook

  • El ejemplo del Cookbook ofrece una plantilla de Makefile para proyectos de tamaño medio
  • Si colocas archivos C/C++ en la carpeta src/, el Makefile determina automáticamente los objetivos de compilación y sus dependencias
  • La configuración principal de variables es la siguiente
    • TARGET_EXEC := final_program
    • BUILD_DIR := ./build
    • SRC_DIRS := ./src
  • SRCS usa find y $(shell ...) para localizar archivos *.cpp, *.c y *.s
  • OBJS antepone BUILD_DIR a cada archivo fuente y agrega .o para crear la ruta de los artefactos de build
  • DEPS cambia .o por .d para crear la lista de Makefiles de dependencias
  • INC_DIRS encuentra todos los directorios bajo src/, e INC_FLAGS agrega el prefijo -I a cada directorio
  • CPPFLAGS := $(INC_FLAGS) -MMD -MP configura GCC para que genere archivos de dependencias .d
  • Los archivos fuente de C y C++ se compilan con reglas de patrón separadas para producir archivos .o dentro de build/, y los directorios necesarios se crean con mkdir -p $(dir $@)
  • clean se declara como .PHONY y elimina el directorio de build con rm -r $(BUILD_DIR)
  • -include $(DEPS) incluye los archivos .d, pero suprime el error si todavía no existen, como en el primer build

1 comentarios

 
GN⁺ 2025-06-21
Opiniones de Hacker News
  • Hay algunas flags de make poco conocidas pero útiles. Sincronización de salida hace que stdout/stderr se impriman recién cuando termina el target, lo que reduce el problema de que los logs se mezclen y se vuelvan difíciles de leer durante builds paralelos
    make --output-sync=recurse -j10
    En sistemas multiusuario ocupados, puede que solo con el número de jobs de -j no alcance, así que también se puede limitar el paralelismo según el promedio de carga: make -j10 --load-average=10
    --shuffle, que aleatoriza el orden de planificación de los targets, también es útil en CI para detectar dependencias faltantes en un Makefile: make --shuffle # or --shuffle=seed/reverse

    • Si está “poco conocido”, estaría bueno que los autores de make reunieran la lista de opciones en algún lugar y la distribuyeran junto con el programa. Si fuera en forma de archivo de texto legible por usuarios o en algún lenguaje de composición tipográfica, el acceso al conocimiento mejoraría muchísimo
    • La que más uso es -B, una opción que fuerza reconstruir todo sin condiciones
    • He visto que make -j pone máquinas en estado de DoS varias veces, así que casi lo considero un bug
    • Si es un sistema multiusuario ocupado, ¿no debería poder encargarse el scheduler del sistema operativo?
    • Aunque sea útil, no es portable. Mejor no usarlo fuera de proyectos personales de juguete que no vayas a distribuir
  • Alrededor de 1985, en el Boston University Graphics Lab, vi a alguien usar un Makefile para impulsar la generación de un renderer 3D para animación. Era alguien del mundo Lisp y trabajaba con generación procedural temprana y un sistema de actores 3D; el Makefile tenía unas 10 líneas y era muy elegante
    Con simples dependencias de fechas de archivos generaba cientos de animaciones, y cuando Lisp creaba la forma 3D de cada frame, Make generaba el frame
    Era 1985, una época en la que casi nada de lo que hoy damos por sentado en 3D y animación existía, así que sorprendió a todos. Después, según recuerdo, usó el renderer 3D de Iron Giant y también tuvo un rol clave en Coraline. Era Brian Gardner

  • Make es una de esas herramientas que me alegra mucho haber aprendido al inicio de mi carrera. Hoy no la uso tanto, pero me mostró el poder que tienen los sistemas declarativos frente a los sistemas imperativos
    Me di cuenta de que esta idea se extiende de forma natural a otras tareas. Viendo la imagen de arriba de este sitio, parece que el autor sintió algo parecido: una receta de cocina se entiende mejor si se mira de forma declarativa, como un Makefile, en vez de como una secuencia de comandos estilo script, como una receta tradicional
    Artículo relacionado: https://blog.gpkb.org/posts/cooking-with-make/
    Siempre escribo las recetas para poder leerlas como un Makefile y las llevo a la cocina; me pregunto si alguien las habrá compuesto o mostrado así. Creo que ahorraría mucho tiempo al leer recetas nuevas si no tuviera que convertir mentalmente el script a un Makefile

    • Lo bueno de este enfoque es que el usuario, en la práctica, interpreta directamente el grafo de dependencias y se convierte en el ejecutor, por lo que tiene más control. En el ejemplo de cocina, la estructura declarativa muestra mejor los puntos en los que quedan opciones abiertas sobre qué hacer después, y da libertad para reflejar restricciones externas que no están escritas formalmente en el Makefile, como el orden que facilita lavar los platos
      Claro que el costo es tener que resolver personalmente el grafo de dependencias. Si solo quieres seguir pasos secuenciales ya serializados de antemano, terminas asumiendo más trabajo
  • El artículo dice que la mayoría de las recetas no se marcan como .PHONY, y lo usa como si fuera una razón para no tratarlo en el tutorial, pero es una excusa débil. Una herramienta debería enseñar el uso correcto
    En mi equipo, como usamos make como ejecutor de tareas, me llamaron la atención por agregar y mantener .PHONY en todas las recetas
    La guía de estilo para Makefile de Clark Grubb es buena: https://clarkgrubb.com/makefile-style-guide
    Me pregunto si alguien usa esta guía de estilo. También me interesa si prefieren marcar las recetas phony en el punto de declaración o tener una lista enorme al inicio del archivo. Estaría bueno que existiera un linter que lo hiciera cumplir

    • La leí y, en general, es un buen documento. Pero no estoy de acuerdo con algunas cosas
      No me gusta usar -o pipefail por inercia. Pipefail es útil, pero rompe una de las cosas más comunes que la gente hace en pipelines: filtrar la salida con grep. Es mejor ponerlo individualmente solo en las recetas que lo necesitan
      Marcar como .PHONY los targets que no son archivos es estrictamente correcto, pero por lo general no hace falta. Si hay muchos targets, agrega verbosidad innecesaria al Makefile, así que creo que es mejor añadirlo solo cuando haga falta
      En recetas que generan varios archivos de salida, antes lo estándar eran archivos dummy/archivos flag cuando las reglas de patrón no encajaban, pero desde GNU Make 4.3 hay soporte nativo para targets agrupados. También está en Ubuntu 22.04 LTS: https://www.gnu.org/software/make/manual/html_node/Multiple-...
  • Algo interesante recientemente es que CMake determinó que los Makefiles no son adecuados para proyectos que usan módulos de C++20, y que Ninja sí lo es. https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
    Básicamente parece considerar que definir estáticamente las dependencias de los targets es demasiado difícil o imposible. Ahora se maneja de forma dinámica con herramientas como clang-scan-deps: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...

    • Corríjanme si me equivoco, pero esta limitación relacionada con Makefile parece ser enteramente una decisión de CMake, o al menos el resultado de la falta de voluntarios para agregar soporte al generador de Makefiles. Ninja en sí tampoco soporta módulos de C++: https://github.com/ninja-build/ninja/issues/2457
      De hecho, Ninja exige que todas las dependencias se definan estáticamente, así que en ese sentido tiene menos funcionalidad que Make común
    • Sinceramente, los módulos son casi un desastre
  • Make tiene su lugar como herramienta de build para grandes bases de código en C. Pero a menudo se lo trata como una especie de “ejecutor de tareas por proyecto” de propósito general, y no encaja bien ahí. Incluso los condicionales simples son difíciles
    Por ejemplo, vi varias veces intentos bienintencionados de envolver Terraform con Make, pero no terminaron bien

    • Make no es un ejecutor de tareas de propósito general. Es más bien una forma general de convertir scripts de shell lineales en dependencias declarativas. Es una herramienta general para el shell
    • Creo que eso de que sea una herramienta de build para grandes bases de código en C ya tampoco es cierto. En los últimos 20 años se crearon sistemas de build más sólidos y mejor definidos, así que es hora de actualizarse
    • ¿Existe algún buen ejecutor de tareas de propósito general? Edición: parece que entendí completamente mal lo de “ejecutor de tareas”
  • Una excelente herramienta moderna para reemplazar las partes donde los Makefiles se vuelven desordenados es just: https://github.com/casey/just

    • Reemplaza el aspecto de Make de “lista de scripts de shell cortos”, pero no reemplaza la parte realmente útil: “ejecutar solo las reglas que hay que volver a ejecutar”
    • También hay otras opciones: Task (Go) https://github.com/go-task/task, Cake (C#) https://github.com/cake-build/cake, Rake (Ruby) https://github.com/ruby/rake
      Como concepto completamente distinto, también está Makedown, que se comentó en HN hace 8 meses: https://news.ycombinator.com/item?id=41825344
    • Aunque estas herramientas se posicionan como alternativas a make, personalmente las veo como algo completamente distinto y ni siquiera comparable. make se centra en la generación de artefactos y en no recompilar lo que ya está hecho; just es un ejecutor de comandos
    • La principal ventaja de usar Make como ejecutor de comandos es que es una herramienta estándar instalada “en todas partes”. Aunque las alternativas parecen más cómodas de usar, nunca sentí que aportaran un beneficio lo bastante grande como para justificar instalar una herramienta adicional
    • Task también es otra alternativa. Eso sí, personalmente solo lo usé en proyectos hobby sencillos en C, así que no puedo decir si aguanta bien al crecer en escala: https://taskfile.dev/
  • Los Makefiles son excelentes, pero conviene no meterse demasiado de lleno. Hace unos años intenté crear un framework GNU Make puro y me di cuenta de que básicamente estaba reinventando autoconf; recién entonces entendí por qué se crearon las GNU autotools
    Makefile es una especie de Turing tarpit curiosamente parecida a Lisp. GNU Make incluso tiene funciones de metaprogramación, así que es difícil resistir la tentación de metaprogramar algún sistema sacrílego dentro de un Makefile. El hecho de que GNU Make esté instalado tan ampliamente aumenta la tentación

  • Soy una de las personas que creó y mantiene Task, una alternativa a Make. Existe desde hace más de 8 años y sigue evolucionando, así que si estás buscando algo nuevo, estaría bueno que lo pruebes. También puedes hacer preguntas con confianza
    https://taskfile.dev/
    https://github.com/go-task/task

  • Me da curiosidad si alguien ha usado tup: https://gittup.org/tup/ex_dependencies.html
    Es un sistema de build que determina automáticamente las dependencias a partir del acceso al sistema de archivos, así que puede usarse con cualquier compilador o herramienta