Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Make et Makefile

Pourquoi a-t-on besoin d’un outil pour compiler automatiquement ?

Parce que les commandes pour compiler sont rébarbatives à taper et à retenir. Il vaut mieux un **outil pour gérer la compilation pour nous**. Aussi, parce qu'on a pas envie de perdre du temps de calcul à recompiler TOUS les fichiers à chaque petite modification.

Make est un outil créé en 1976 par Stuart Feldman.

Pourquoi apprendre Make ?

  • On a besoin de comprendre le mécanisme de compilation : ne pas recompiler les fichiers déjà compilés. Ce principe est utilisé partout : C, C++, Rust, Python, etc.
  • Comprendre Make permet de comprendre d’autres technologies comme CMake etc.
  • Même si l’outil Make précisément n’est pas important, c’est mieux de concrétiser. Et c’est un outil vieux, qui a fait ses preuves, dont le créateur a été récompensé pour Make par un ACM’s Software System Award en 2023.

Compiler avec Make (version naïve)

  • Créer un fichier Makefile.
  • Y écrire :
    all:
        gcc myotherCfile.c main.c -o main -Wall

Dans le terminal, on tape make pour construire le projet. Le nom all s’appelle une cible.

Attention, la ligne d’après contient un tab (et pas 4 espaces !) suivi de la commande à exécuter pour construire le projet.

Compiler intelligemment avec Makefile

Avec un Makefile, on peut avoir plusieurs cible.

all: main

main: main.o myotherCfile.o
    gcc -o main main.o myotherCfile.o

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

myotherCfile.o: myotherCfile.c
    gcc -c -o myotherCfile.o myotherCfile.c

clean:
    rm *.o main

Construire votre fichier Makefile pour votre projet.

Compilation et liaison

La cible main a besoin d’avoir déjà effectué le travail pour les cibles main.o et myotherCfile.o, et consiste à effectuer gcc -o main main.o myotherCfile.o.

  • Où est-ce qu’a lieu la liaison dans le Makefile ci-dessus ? Dans la cible main.

  • Qu’est ce que fait gcc -c -o main.o main.c ?

Elle compile le fichier main.c en main.o en laissant des trous pour les fonctions déclarées mais non définies.

Variables dans un MakeFile

On peut définir des constantes dans un MakeFile. Par exemple, on définit la constante CC qui donne le nom du compilateur. Pour avoir le contenu de la constante on écrit $(CC). Ecrire CC ça écrit juste CC ; nous on veut le contenu.

CC=gcc

all: main

main: main.o myotherCfile.o
    $(CC) -o main main.o myotherCfile.o

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

myotherCfile.o: myotherCfile.c
    $(CC) -c -o myotherCfile.o myotherCfile.c

clean:
    rm *.o main

Pattern

Voici trois règles qui ont le même pattern :

myotherCfile.o: myotherCfile.c
    $(CC) -c -o myotherCfile.o myotherCfile.c

bloup.o: bloup.c
    $(CC) -c -o bloup.o bloup.c

miaou.o: miaou.c
    $(CC) -c -o miaou.o miaou.c

Au lieu de cela, on peut écrire :

%.o: %.c
    $(CC) -c -o $@ $^
  • Le % signifie nimportequelnomdefichier.
  • $@ = le nom de la règle nimportequelnomdefichier.o
  • $^ = la prémisse, ici nimportequelnomdefichier.c
nom de la règle:prémisse
$@$^

Lister les fichiers

La commande principale pourrait être :

main: main.o myotherCfile.o bloup.o miaou.o
    $(CC) -o main main.o myotherCfile.o bloup.o miaou.o

Pour réaliser cela, on a besoin de lister les .o. Or, on ne les connait pas encore. Mais on sait qu’il y a en un par fichier source .c.

Obtenir la liste des .c

On peut lister les fichiers sources avec la commande wildcard :

SOURCES=$(wildcard *.c)

La fonction wildcard prend un argument qui est une expression régulière de fichiers et elle produit la liste des fichiers qui correspondent à l’expression régulière. Le terme wildcard signifie, dans un jeu de cartes, une carte qui remplace n’importe quel autre (typiquement le joker). En informatique, cela désigne ici tous les fichiers qui matchent avec *.c. Dans l’exemple, la constante SOURCES vaut main.c myotherCfile.c bloup.c miaou.c.

Obtenir la liste des .o

Pour obtenir la liste des .o correspondantes, on fait une substitution :

main.c myotherCfile.c bloup.c miaou.c

🡇

main.o myotherCfile.o bloup.o miaou.o

Pour cela on écrit :

OBJECTS=$(SOURCES:.c=.o)

Et maintenant, la règle principale qui était :

main: main.o myotherCfile.o bloup.o miaou.o
    $(CC) -o main main.o myotherCfile.o bloup.o miaou.o

devient

main: $(OBJECTS)
    $(CC) -o main $^

Organisation en dossiers

Le must est d’organiser son projet avec différents dossiers :

  • un dossier header qui contient les fichiers header
  • un dossier src qui contient les fichiers .c
  • un (futur) dossier build qui contient les fichier .o créés à la compilation

Ainsi voici un exemple de fichier Makefile qui gère ça :

CC=gcc
SOURCES=$(wildcard src/*.c)
OBJECTS=$(patsubst src/%.c,build/%.o,$(SOURCES))

main.exe: $(OBJECTS)
	$(CC) $^ -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 -o main.exe

build/%.o: src/%.c build
	$(CC) -Iheader -c -o $@ $<

build:
	mkdir build

.PHONY:clean run

## supprimer tous les fichiers générés
clean:
	-rm -r build
    -rm ./main.exe

## lancer le programme
run: main.exe
	./main.exe

La fonction patsubst permet de réaliser une substitution d’un pattern. Ici :

src/main.c src/myotherCfile.c src/bloup.c src/miaou.c

🡇

build/main.o build/myotherCfile.o build/bloup.o build/miaou.o

Le flag -Iheader indique qu’il faut cherche dans le dossier header pour les #include (-I). Le symbole $< correspond à la première partie d’une prémisse. Ici : src/%.c build on prend src/%.c (par exemple src/main.c).

La directive .PHONY permet de dire qu’une règle ne correspond pas à un fichier. La directive clean par exemple devrait s’exécuter même si un fichier clean existe ! Ca n’a rien à voir !

Le tiret - avant rm permet d’ignorer l’erreur si le répertoire build n’existe pas, ou si l’exécutable n’existe pas.

Aller plus loin

On peut faire des boucles et autres en Makefile… bref…

Exercices

  • Que fait la commande ci-dessous ?
help:
	@grep -E '^##' Makefile | sed 's/## //'

Il s’agit d’une commande qui affiche l’aide écrite dans le fichier Makefile. Plus précisément, elle affiche le contenu des lignes commançant par ##. Voici comment cela fonctionne :

  • le @ devant @grep fait que la commande elle-même n’est pas affichée ;
  • @grep -E '^##' Makefile imprime toutes les lignes du fichier Makefile qui commence par ##
    • -E signifie que l’on utilise des expressions régulières étendues (plus lisible, même si ici ça sert à rien)
    • ^## veut dire que l’on cherche ## au début d’une ligne (le ^ veut dire début de ligne)
  • Puis on fait un pipe à sed 's/## //' qui remplace ## par une chaîne vide. sed veut dire stream editor et permet de réaliser des transformations de texte comme ici une substitution. La syntaxe sed 's/cequejecherche/avecquoijeremplace/'. Le résultat est imprimé sur la sortie.
  • Que fait la commande ci-dessous ?
PYTHON := python3
VENV := venv

venv:
	test -d $(VENV) || $(PYTHON) -m venv $(VENV)
	@echo "Virtual environment created."

Cette commande crée un environement s’il n’y a pas un déjà.

  • test -d $(VENV) teste si un répertoire nommé $(VENV) existe
    • si oui (i.e. la commande test a renvoyé 0), le ou (||) renvoie vrai et on fait rien
    • si non (i.e. la commande test a renvoyé 1), on exécute la partie droite qui crée un environnement
  • La seconde ligne affiche "Virtual environment created." mais le @ fait que la commande elle-même echo "Virtual environment created." n’est pas affiché par make
  • Que fait la commande ci-dessous ?
VENV := venv
ACTIVATE := . $(VENV)/bin/activate

run:
	$(ACTIVATE) && python src/main.py

Cette commande active l’environnement Python, puis exécute le fichier src/main.py. Le bloup1 && bloup2 signifie :

  • effectuer bloup1. Si cela échoue, on arrête tout
  • Si bloup1 avait réussi, on exécute bloup2