Makefile, organisation d’un projet

Michel Billaud

10 octobre 2018

Résumé. Quand un projet contient de nombreux fichiers sources, il est intéressant de produire les fichiers intermédiaires et les exécutables dans des répertoires séparés des sources.

Dernière mise à jour : 15 novembre 2021.

1 Problématique

1.1 Un Exemple de projet

On construit un projet avec deux exécutables à produire :

Source de main_fb.c :

#include <stdio.h>

#include "foo.h"
#include "bar.h"

int main() {
  foo();
  bar();
  return 0;
}

Chaque fonction est compilée séparément. Nous avons donc les fichiers sources :

1.2 Un makefile “basique”

En utilisant les règles par défaut, les dépendances implicites, et la génération automatique des dépendances, on peut écrire un Makefile simple :

CFLAGS = -MMD -MP

EXECS = main_f main_fb
all: $(EXECS) 

main_f:  main_f.o   foo.o
main_fb: main_fb.o  foo.o bar.o

-include $(wildcard *.d)

clean:
    $(RM) *~ *.o *.d

mrproper: clean
    $(RM) $(EXECS)

dont l’exécution conduit au résultat voulu

$ make -f Makefile.simple 
cc -MMD -MP   -c -o main_f.o main_f.c
cc -MMD -MP   -c -o foo.o foo.c
cc   main_f.o foo.o   -o main_f
cc -MMD -MP   -c -o main_fb.o main_fb.c
cc -MMD -MP   -c -o bar.o bar.c
cc   main_fb.o foo.o bar.o   -o main_fb

1.3 Critique

Cette solution a l’inconvénient d’envahir le répertoire avec des fichiers de travail :

$ ls
bar.c  bar.o  foo.h   main_fb    main_fb.o  main_f.o
bar.d  foo.c  foo.o   main_fb.c  main_f.c   Makefile
bar.h  foo.d  main_f  main_fb.d  main_f.d   

C’est le problème auquel on essaie de remédier.

2 Utilisation de répertoires séparés

2.1 Objectif

L’objectif est de ne pas polluer le répertoire des sources. Pour cela on utilisera deux sous-répertoires

2.2 Construction raisonnée du Makefile

2.2.1 Les variables

Le Makefile commence par quelques définitions

BUILD_DIR = build
DIST_DIR  = dist
EXECS = main_f main_fb
OBJS_MAIN_F =  main_f.o  foo.o
OBJS_MAIN_FB = main_fb.o foo.o bar.o
CFLAGS = -MMD -MP

2.2.2 La production des exécutables

all: $(addprefix $(DIST_DIR)/,$(EXECS))
$(DIST_DIR)/main_f:  $(addprefix $(BUILD_DIR)/,$(OBJS_MAIN_F))
$(DIST_DIR)/main_fb: $(addprefix $(BUILD_DIR)/,$(OBJS_MAIN_FB))
$(DIST_DIR)/%:  
    @mkdir -p $(DIST_DIR)  
    $(LINK.c) -o $@ $^
$(BUILD_DIR)/%.o: %.c
    @mkdir -p $(BUILD_DIR)
    $(COMPILE.c) -o $@ $^

2.2.3 Dépendances et utilitaires

-include $(BUILD_DIR)/*.d
clean:
    $(RM) *~ 
    $(RM) -r (BUILD_DIR)

mrproper: clean
    $(RM) -r  $(DIST_DIR)

2.3 Exécution du Makefile

$ make
cc -MMD -MP   -c -o build/main_f.o main_f.c
cc -MMD -MP   -c -o build/foo.o foo.c
mkdir -p dist
cc -MMD -MP    -o dist/main_f build/main_f.o build/foo.o
cc -MMD -MP   -c -o build/main_fb.o main_fb.c
cc -MMD -MP   -c -o build/bar.o bar.c
mkdir -p dist
cc -MMD -MP    -o dist/main_fb build/main_fb.o build/foo.o build/bar.o
$ LANG= tree -c
.
|-- foo.c
|-- foo.h
|-- bar.h
|-- main_f.c
|-- main_fb.c
|-- bar.c
|-- Makefile
|-- build
|   |-- bar.d
|   |-- bar.o
|   |-- foo.d
|   |-- foo.o
|   |-- main_f.d
|   |-- main_f.o
|   |-- main_fb.d
|   `-- main_fb.o
`-- dist
    |-- main_f
    `-- main_fb

Fichier build/main_fb.d :

build/main_fb.o: main_fb.c foo.h bar.h

foo.h:

bar.h:

Fichier build/foo.d

build/foo.o: foo.c foo.h

foo.h:

3 Conclusion

Le Makefile ainsi construit ne contient que quelques lignes spécifiques à ce projet :

EXECS = main_f main_fb    
...
main_f  : main_f.o  foo.o
main_fb : main_fb.o foo.o bar.o
...
$(DIST_DIR)/main_f  : $(addprefix $(BUILD_DIR)/,$(OBJS_MAIN_F))
$(DIST_DIR)/main_fb : $(addprefix $(BUILD_DIR)/,$(OBJS_MAIN_FB))

Pour des projets qui ont une structure similaire, c’est-à-dire

il est facile de l’adapter.