make est un outil qui permet de déterminer quelles parties d'un programme nécessitent une recompilation et de lancer les commandes nécessaires pour les recompiler. C'est un outil qui est très utilisé dans le cas de programmes C, mais que l'on peut utiliser pour des cas beaucoup plus proches du développement web. Il a aussi l'avantage d'être largement disponible sur les systèmes ce qui permet de ne pas avoir à installer une dépendance supplémentaire juste pour utiliser make.

Makefile

Afin d'utiliser make il faut commencer par créer un fichier Makefile qui va permettre de décrire la relation entre les fichiers du programme et les commandes à éxécuter pour les obtenir. La syntaxe d'un Makefile est très simple :

cible: prerequis1 prerequis2
    command1
    command2

Attention ! les commandes doivent être précédée d'une tabulation (pas de 2 ou 4 espaces, une vraie tabulation). Dans le cas d'un projet C on peut ainsi indiquer comment compiler les fichiers à partir de leur dépendances

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c

Ainsi pour générer le fichier main.o, make sait qu'il doit regarder le fichier main.c et defs.h. Si le fichier main.o est plus récent que les 2 prérequis alors aucune opération ne sera effectuée, sinon il éxécutera la commande cc -c main.c.

Mais je fais du web pas du C !

Dans le cas d'un gros projet C on peut comprendre que make permet d'économiser du temps en évitant de devoir tout recompiler en permanence. Mais du coup, comment make peut être utilé dans le cadre d'un projet web ?
Il peut par exemple servir à décrire la phase d'installation des différentes dépendances. Par exemple dans le cadre d'un projet PHP :

.PHONY: install update

composer.lock: composer.json
    composer update

vendor: composer.lock
    composer install

install: vendor

On indique ici plusieurs choses :

  • Si le fichier composer.json est plus récent que le fichier composer.lock on met à jour les dépendances via la commande composer update.
  • Si le fichier composer.lock est plus récent que le dossier vendor alors on installe les dépendances.
  • On crée une "fausse" cible (déclarée dans le .PHONY) qui permet de lancer l'installation via un simple make install

L'utilisation de la cible ".PHONY" permet d'utiliser make comme un simple système d'alias :

.PHONY: test server cache-clear install help
.DEFAULT_GOAL= help

composer.lock: composer.json
    composer update

vendor: composer.lock
    composer install

install: vendor

help: 
    @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-10s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'

test: install ## Lance les tests unitaire
    php ./vendor/bin/phpunit --stop-on-failure

cache-clear: ## Nettoie le cache
    rm -rf ./tmp

server: install ## Lance le serveur interne de PHP
    ENV=dev php -S localhost:8000 -t public/ -d display_errors=1

On remarque ici que notre cible install est utilisée en prérequis de la cible test. Ainsi, si on lance la commande make test sans avoir préinstallé les dépendances, make le détectera et commancera par éxécuter les cibles nécessaires.

Les variables

On peut pousser les choses un peu plus loin en utilisant des variables. Il est par exemple possible de spécifier l'éxécutable à utiliser dans une variable.

test: install ## Lance les tests unitaire
    $(PHP) ./vendor/bin/phpunit --stop-on-failure

On peut ainsi facilement redéfinir la variable en amont et lancer les tests unitaires avec différentes versions :

PHP=php
CURRENT_DIR=$(shell pwd)
ifdef VERSION
    PHP=docker run -it --rm --name phpcli -v $(CURRENT_DIR):/usr/src/myapp -w /usr/src/myapp php:$(VERSION)-cli php
endif

test: install ## Lance les tests unitaire
    $(PHP) ./vendor/bin/phpunit --stop-on-failure

On choisit ici de lancer nos tests gràce à docker, ce qui nous permet de facilement utiliser plusieurs versions d'un même outil :

make test
make test VERSION=7.0
make test VERSION=7.1
make test VERSION=7.2-rc

Pour en apprendre plus sur l'utilisation des variables n'hésitez pas à faire un tour sur le manuel.

Pattern Rule

Il est aussi possible de définir des motifs de règles gràce au symbole % :

images/optimized/%.jpg: images/raw/%.jpg
    mkdir -p images/optimized
    guetzli --quality 85 --verbose $< $@

Dès qu'une autre règle aura besoin d'une image située dans le dossier optimized, make sera en mesure de comprendre comment la construire. Combiné avec l'utilisation de variables et de fonctions il est possible de créer des tâches plus complexes :

RAW_IMAGES=$(subst images/raw/,images/optimized/,$(wildcard images/raw/*.jpg))

images/optimized/%.jpg: images/raw/%.jpg
    mkdir -p images/optimized
    guetzli --quality 85 --verbose $< $@

images: $(SRC)

Si on tape make images le système va automatiquement optimiser les nouvelles images et les placer dans le dossier optimized. Il est aussi possible, gràce au drapeau -j de paralléliser les tâches.

make -j8 images

Cette parallélisation peut aussi être utilisée pour démarrer plusieurs tâches en même temps. Par exemple pour lancer le serveur web interne de PHP et browser-sync pour actualiser automatiquement la page.

PHP=php
PORT?=8000
HOST?=127.0.0.1
COM_COLOR   = \033[0;34m
OBJ_COLOR   = \033[0;36m
OK_COLOR    = \033[0;32m
ERROR_COLOR = \033[0;31m
WARN_COLOR  = \033[0;33m
NO_COLOR    = \033[m

server: install ## Lance le serveur interne de PHP
    echo -e "Lancement du serveur sur $(OK_COLOR)http://$(HOST):$(PORT)$(NO_COLOR)"
    ENV=dev $(PHP) -S $(HOST):$(PORT) -t public/ -d display_errors=1

browsersync:
    browser-sync start --port 3000 --proxy localhost:$(PORT) --files 'src/**/*.php' --files 'src/**/*.twig'

watch: server browsersync 

La commande make -j2 watch permettra donc de lancer les cibles server et browsersync en parallèle.