Capire Make

August 1, 2026 · visitatori

make compare in quasi tutti i progetti embedded e di sistema che ho toccato — ricette Yocto, build del kernel, U-Boot, piccoli repository di firmware. È anche uno di quegli strumenti che le persone usano per anni per imitazione, copiando un Makefile dall'ultimo progetto senza mai arrivare a fissare davvero il modello che c'è sotto. Ecco quel modello, esposto direttamente.

Cos'è Make?

Make è uno strumento di automazione del build: legge un file (un Makefile) che descrive un insieme di target, i prerequisiti da cui dipende ciascun target, e la ricetta (comandi di shell) necessaria per produrre quel target a partire dai suoi prerequisiti. Data questa descrizione, make determina quali target sono obsoleti ed esegue solo le ricette necessarie per aggiornarli — nient'altro.

main: main.o utils.o
	gcc -o main main.o utils.o

main.o: main.c
	gcc -c main.c

Esegui make, e costruirà main.o solo se main.c è più recente di main.o (o se main.o non esiste ancora), poi collegherà main solo se main.o o utils.o sono cambiati. Nulla viene ricostruito inutilmente.

A chi si rivolge?

Make è stato scritto da Stuart Feldman ai Bell Labs nell'aprile del 1976. Secondo il suo stesso racconto, la causa scatenante fu un collega — Steve Johnson, autore di yacc — che perse un'intera mattinata a fare debug di un programma che, in realtà, era corretto; il binario semplicemente non era stato ricollegato dopo una modifica al sorgente. Feldman ricevette per questo l'ACM Software System Award nel 2003. Lo strumento nato da quel pomeriggio — tracciare le dipendenze una sola volta e lasciare che sia la macchina a capire cosa va rifatto — è diventata una delle idee più copiate negli strumenti software. GNU Make, la versione che quasi tutti usano oggi, è una reimplementazione libera del Progetto GNU (1) (5) (6).

In pratica, è rivolto a chiunque assembli un artefatto non banale — un binario compilato, un documento, un insieme di file generati — a partire da pezzi più piccoli, dove ricostruire tutto da zero a ogni modifica sarebbe uno spreco o troppo lento. Questo copre progetti C/C++, build di kernel e bootloader, documenti LaTeX, e parecchia automazione ad hoc che non ha nulla a che fare con la compilazione di codice.

Quando conviene usarlo?

Make si guadagna il suo posto quando tre condizioni sono vere contemporaneamente:

Per una manciata di file senza interdipendenze, uno script di shell è spesso più semplice e più onesto. Make comincia a ripagare quando "questo aveva davvero bisogno di essere ricostruito?" diventa una domanda che vale la pena porsi automaticamente invece che a mano.

Dove si colloca nella toolchain?

Make di solito si colloca un livello sopra il compilatore e un livello sotto ciò che esegue un essere umano. In un tipico progetto C: file sorgente → (compilatore, guidato da Make) → file oggetto → (linker, guidato da Make) → binario finale. Make di per sé non compila né collega nulla — invoca gli strumenti che lo fanno, come comandi di shell in ciascuna ricetta.

Tende anche a collocarsi sotto strumenti di configurazione del build di livello più alto. CMake, Autotools e BitBake di Yocto, ad esempio, non sostituiscono tanto Make quanto generano Makefile (o pilotano un equivalente) come loro effettivo backend di esecuzione. Quindi anche i progetti che non mostrano mai direttamente un Makefile spesso ne stanno eseguendo uno da qualche parte nella pipeline.

Perché funziona nel modo in cui funziona?

La scelta di design centrale — confrontare i tempi di modifica dei file per decidere cosa è obsoleto — è ciò che rende Make veloce su alberi di grandi dimensioni: controllare un timestamp è economico, quindi ricostruire un progetto enorme dopo una modifica di una riga può saltare quasi tutto e restare comunque corretto, a condizione che il grafo delle dipendenze fornito sia accurato.

Questa ultima condizione è l'origine di quasi tutti i reclami "Make è rotto" in cui ci si imbatte, e vale la pena soffermarcisi: Make è corretto solo quanto lo è il grafo di cui è stato informato. Se un file .c fa #include di un header che non è elencato come prerequisito del corrispondente file .o, modificare quell'header non innescherà una ricostruzione — non perché Make sia confuso, ma perché nessuno gli ha mai detto che quella dipendenza esisteva. Per questo i Makefile del mondo reale sono quasi sempre abbinati a un passaggio di generazione delle dipendenze da parte del compilatore (-MMD -MP con GCC/Clang) piuttosto che a elenchi di header scritti a mano — il compilatore conosce già il vero grafo degli include, quindi è l'unica fonte affidabile per ottenerlo (4).

Come funziona realmente?

Target, prerequisiti, ricette. L'unità di base è:

target: prerequisite1 prerequisite2
	recipe line 1
	recipe line 2

Le righe di ricetta devono essere indentate con un tab, non con spazi — una celebre stranezza storica che ancora oggi fa inciampare le persone.

Le variabili automatiche riducono la ripetizione:

%.o: %.c
	gcc -c $< -o $@

$@ è il target, $< è il primo prerequisito — questa singola regola con pattern può costruire ogni .o dal corrispondente .c senza una riga per ogni file.

I target .PHONY contrassegnano nomi che non corrispondono a file reali — clean, all, test — così che Make non si confonda se per caso esiste un file con quel nome, ed esegua sempre la ricetta invece di controllare i timestamp: (2)

.PHONY: clean
clean:
	rm -f *.o main

Le variabili evitano di ripetere i flag ovunque e facilitano il riutilizzo tra progetti diversi:

CC = gcc
CFLAGS = -Wall -O2

main.o: main.c
	$(CC) $(CFLAGS) -c main.c

La generazione automatica delle dipendenze colma il divario descritto sopra:

CFLAGS = -Wall -O2 -MMD -MP
-include $(wildcard *.d)

%.o: %.c
	$(CC) $(CFLAGS) -c $<

-MMD fa sì che il compilatore emetta un file .d per ogni oggetto, elencando ogni header effettivamente incluso durante quella compilazione. -include reintegra quei file nel Makefile in modo che il grafo delle dipendenze resti accurato man mano che gli header cambiano — senza che nessuno debba mantenerlo a mano (3).

Riflessione finale

Gran parte di ciò che sembra una stranezza di Make — i confronti tra timestamp, l'insistenza sui tab, il modo in cui una dipendenza mancante fallisce silenziosamente invece che rumorosamente — deriva da un'unica decisione di design presa nel 1976: mantenere il modello semplice e il controllo economico, e fidarsi che il Makefile descriva la realtà in modo accurato. Una volta adottata questa prospettiva, la maggior parte dei momenti in cui "Make si comporta in modo strano" si rivela essere "il grafo che gli ho fornito era incompleto" — cosa molto più facile da correggere.

Letture Correlate

Riferimenti

  1. Feldman, S. I. (1979). Make — A Program for Maintaining Computer Programs. Software: Practice and Experience, 9(4), 255–265. — l'articolo originale di Feldman che presenta Make, inclusa la motivazione originale.
  2. GNU Make Manual — il riferimento completo e autorevole per la sintassi e la semantica di GNU Make.
  3. GNU Make Manual: Automatic Prerequisites — il pattern -MMD/-include nelle parole degli stessi maintainer.
  4. GCC: Preprocessor Options (-M family) — cosa fanno realmente -MMD e -MP.
  5. Make (software) — Wikipedia — storia generale e discendenza delle implementazioni di Make (BSD Make, GNU Make, NMAKE, ecc.).
  6. Stuart Feldman — Wikipedia — approfondimento sul lavoro di Feldman ai Bell Labs e sull'ACM Software System Award 2003.

Commenti