Introduzione

Nella programmazione C, un programma è spesso diviso in più file sorgente e file di intestazione per migliorare l'organizzazione, la riutilizzabilità e la velocità di compilazione. Tuttavia, questa modularità introduce una sfida: quando un file di intestazione cambia, ogni file sorgente che lo include deve essere ricompilato.

Cosa sono i Makefiles?

[LTFLT:0]make] l'utilità legge queste regole, controlla i timestamp dei file, ed esegue solo i comandi necessari per portare il progetto fino ad oggi. L'idea principale è semplice: ogni regola ha un target [FLT:]

I Makefiles sono stati parte di Unix dagli anni '70, e GNU Make è lo standard de facto su Linux e macOS. La sintassi è concisa ma può essere sottile; ottenere dipendenze è la capacità primaria di cui uno sviluppatore C ha bisogno.

Comprendere le dipendenze

In un progetto C, le dipendenze non sono limitate ai file . Ogni file sorgente include uno o più file di intestazione (ad esempio ]). Se un intestazione viene modificato, tutti i file che includono deve essere ricompilato. Allo stesso modo, i file di oggetti dipendono dai file corrispondenti , e tutti i file eseguibili finali.

Dipendenze esplicite vs implicite

Nei primi Makefiles, i programmatori hanno elencato tutti i prerequisiti manualmente. Questo approccio è fragile: dimenticare un intestazione significa costruire delle stanti, mentre elencando troppi trigger non necessari ricompilazioni. Peggio, come il progetto cresce, le liste manuali diventano insostenibile. La soluzione moderna è quella di far generare automaticamente le dipendenze del compilatore, trasformandole in prerequisiti impliciti e controllati.

Un Makefile ben costruito tratta le dipendenze come una preoccupazione di prima classe. L'obiettivo non è mai ricostruire nulla che non abbia bisogno di ricostruire, e ricostruire sempre tutto ciò che fa. Questa è l'essenza della correttalità[]] e efficienza[]] in incremental build.

Struttura di base di un Makefile

Un tipico Makefile contiene definizioni, regole e obiettivi falsi variabili, ecco un esempio minimale ma funzionale per un progetto con e :

CC = gcc
CFLAGS = -Wall -Wextra -O2

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

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

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

clean:
 rm -f main main.o utils.o

Questo Makefile ha quattro obiettivi: [] (l'eseguibile), due file di oggetti e un target falso []. Le linee di dipendenza dopo che il colon dire make[] quali file da controllare prima di decidere di ricostruire il bersaglio.

Obiettivi di Phony

Gli obiettivi come o non rappresentano i file. Per evitare ] di confonderli con i nomi dei file, essi dovrebbero essere dichiarati come fona[:

.PHONY: clean all

Senza questo, se esistesse un file chiamato , []make[[]]] lo considererebbe aggiornato e saltare la ricetta.

Gestione delle dipendenze Effettivamente

Il manuale Makefile sopra ha un difetto serio: la dipendenza di [] su è corretta, ma che cosa circa intestazioni? Se [] cambia, (che include esso) deve essere ricostruito, ma la regola dice che dipende solo da ]. La correzione è quella di lasciare che il compilatore produrre la lista di dipendenza reale.

Generazione automatica della dipendenza con GCC

GCC (e Clang) può generare informazioni di dipendenza utilizzando la famiglia [ di bandiere. La combinazione più pratica per la maggior parte dei progetti è :

  • [][[]] – scrive un file di dipendenza ([[]]) durante la compilazione, elencando solo intestazioni definite dall'utente (non intestazioni di sistema).
  • ][]] – specifica il nome del file di dipendenza.
  • ][]] – aggiunge obiettivi falsi per ogni intestazione, impedendo errori quando un intestazione viene rimosso.

Ecco come incorporare il monitoraggio automatico della dipendenza in un Makefile:

CC = gcc
CFLAGS = -Wall -Wextra -O2 -MMD -MP
SRCDIR = src
OBJDIR = obj
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: myprogram

myprogram: $(OBJS)
 $(CC) $(CFLAGS) -o $@ $^

$(OBJDIR)/%.o: $(SRCDIR)/%.c
 @mkdir -p $(OBJDIR)
 $(CC) $(CFLAGS) -c $< -o $@

-include $(DEPS)

.PHONY: all clean
clean:
 rm -rf $(OBJDIR) myprogram

Spiegazione:

  • raccoglie tutti i file nella directory sorgente.
  • li trasforma in percorsi di file oggetto.
  • elenca i file di dipendenza (ad esempio ]).
  • La regola del modello compila ogni file [ e, a causa di , genera un file come effetto collaterale.
  • La linea legge i file generati [, trasformandoli in veri prerequisiti Makefile. Il trattino ([]) sopprime gli errori quando i file non esistono ancora (ad esempio, sulla prima costruzione).

Ora, se ] viene modificata, la successiva invocazione di [] ricompierà automaticamente [ perché il file per ] include come prerequisito.

Tecniche avanzate

Gestione delle dipendenze generate

Quando un intestazione viene eliminato, il file può ancora riferirlo, causando ]make[]] a fallire con un errore di destinazione mancante. Il ]] flag affronta questo aggiungendo regole fonose vuote per ogni intestazione di dipendenza.

Compresi i file di dipendenza dopo le modifiche di origine

Una sottigliezza: se un file sorgente aggiunge o rimuove un [, il file corrispondente [] deve essere rigenerato. Poiché il file è di per sé un prerequisito del file oggetto, make]]] noterà il timestamp modificato e il file di ricompilazione, che rigenera automaticamente [FLT] [FLT] [FLT]

Utilizzo di Ordina-Solo Prerequisiti

A volte è necessario che una directory esista prima di costruire, ma non si desidera che il suo timestamp si crei una ricostruzione. Questo è il ruolo di [ solo prerequisiti di ordine[[]] (separato da ]]). Nella regola del modello sopra, abbiamo usato all'interno della ricetta; un'alternativa è:

$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
 $(CC) $(CFLAGS) -c $< -o $@

$(OBJDIR):
 mkdir -p $@

Ciò garantisce che la directory venga creata prima di qualsiasi compilazione, ma una modifica alla directory stessa (ad esempio, un nuovo file all'interno) non innescherà la ricompilazione.

Migliori Pratiche per gestire le dipendenze

  1. Utilizza la generazione automatica della dipendenza dal primo giorno. Anche per un progetto a un singolo file, è una buona abitudine.
  2. Tenere i file di dipendenza separati dai file sorgente. Mettili in una directory [ o []. Questo rende la pulizia più facile ed evita di ingoiare l'albero sorgente.
  3. Includi i file generati [ dopo le regole che li creano.] La direttiva ] va bene, ma posizionarla dopo la regola del modello assicura che []] ] prima sa come costruire file oggetti prima di cercare di leggere i file .
  4. ] Utilizzare obiettivi falsi per la pulizia. [], , , e ] sono comuni.
  5. Leverage variabili per bandiere di compilatore, directory di origine e liste di file. Questo rende il Makefile riutilizzabile attraverso progetti e più facile da personalizzare.
  6. Test your Makefile with a deliberate header change. Modificare un header, eseguire [, e confermare solo i file oggetto interessato vengono ricompilati. Se una ricostruzione completa accade, qualcosa non va con il monitoraggio della dipendenza.
  7. Tenere il Makefile semplice ma non più semplice del necessario.[ L'over-engineering con funzioni avanzate come [] può rendere il debugging doloroso. Inizia con il modello mostrato in questo articolo; si scala bene a decine di file.

Risorse esterne

Per approfondire la vostra comprensione, consultate questi riferimenti autorevoli:

Conclusioni

Gestire le dipendenze del programma C con Makefiles non è un lusso ma una necessità per qualsiasi progetto che supera un singolo file. Combinando regole di pattern, generazione automatica di dipendenza con , e l'inclusione accurata dei file generati , è possibile creare un sistema di compilazione che è sia veloce e corretto. Le tecniche mostrate qui eliminare il monitoraggio manuale, ridurre i tempi di costruzione e prevenire i bug causati solo da stanti.