Booster notre workflow de développement avec un Makefile

Présentation

Le Makefile est un outil qui existe depuis des lustres, quel intérêt d'écrire un article en 2024 ? En fait cet outil initialement conçu pour compiler du code a fait son apparition depuis assez peu dans la boîte à outil du développeur web. Voyons cela !

Quel bazar !

Dans notre quotidien de développeur nous sommes souvent amenés à intervenir sur différents projets, utiliser des technologies différentes et un code plus ou moins ancien.

Nos tâches ne se résument pas uniquement à écrire du code, mais il existe une quantité, toujours grandissante, de tâches associées à notre développement et que l'on exécute à des fréquence variables.

Par exemple :

  • Installer les dépendances de notre projet - npm install, yarn installcomposer install, mvn dependecy:whatever, ...
  • Lancer les tests unitaires - npx jest --some-idontremember-option, ./vendor/bin/phpunit, mvn test -PtheProfileIForgotTheName, ...
  • Construire le livrable - ng build, gulp build-front, mvn package, ...
  • ...

Nous pourrions citer des dizaines de commandes plus ou moins barbares qu'il faut répertorier dans une documentation car disons-le, personne n'a envie d'apprendre ce genre de choses par cœur !

Les scripts viennent à notre secours

Une première étape de simplification, et non des moindres, est d'utiliser la section scripts du fichier de configuration proposée par certains outils comme NPM ou Composer. Cette approche permet de regrouper les commandes complexes en un seul endroit et d'y ajouter une certaine abstraction.

Un exemple NPM avec le fichier package.json :

{
"name": "myproject",
"version": "1.0.0",
"private": true,
...
"scripts": {
  "start": "ng serve",
  "prebuild": "node scripts/generate_version.js",
  "build": "ng build",
  "test": "npx jest"
},
"dependencies": {
},
...
}

Ou encore avec Composer et le fichier composer.json :

{
"type": "myproject",
"prefer-stable": true,
"require": {
...
},
...
"scripts": {
  "test": [
    "php vendor/bin/phpunit --testdox"
  ],
  "test-integration": [
    "php vendor/bin/phpunit --testdox --group integration"
  ],
  "check-style": [
    "vendor/bin/php-cs-fixer check -v"
  ],
  "fix-style": [
    "vendor/bin/php-cs-fixer fix -v"
  ],
  "phpstan": [
    "vendor/bin/phpstan analyse"
  ],
  "start": [
    "Composer\\Config::disableProcessTimeout",
    "php -S localhost:8000 -t public"
  ],
  ...
},
...
}

À ce stade, le développeur pourra lancer des comandes plus amicales telles que :

  • npm run start, npm run test, npm run build, ...
  • composer test, composer fix-style, composer start, ... 

Nous avons fait des beaux progrès dans la simplification !

Toutefois, cette approche reste plus ou moins liée à une technologie ou une typologie de projet : "dans les projets front tu utilises npm run machin, dans les projets PHP en revanche, tu utilises composer bidule" . Enfin lorsque le mélange des genres intervient, par exemple utiliser un outil NPM tel que Redoc pour générer la documentation OpenAPI de mon projet PHP, tout tombe à l'eau ! On va devoir utiliser une commande `npm` sur un projet backend.

Et voici le Makefile

L'idée est donc de trouver un outil transverse et indépendant qui pourra apporter une couche d'abstraction supplémentaire : Make (make) qui est disponible, souvent par défaut, sur Linux et MacOS (il doit exister une version pour Windows, mais quel développeur sérieux utilise Windows #troll ?).

Cet outil est très connu depuis des décénies des développeurs C++, mais en tant que développeur web, il ne fait partie de notre panoplie d'outil que depuis relativement peu de temps. Cet outil très puissant, orienté vers la compilation de code source, va être  principalement utilisé dans notre contexte pour lancer des commandes (cf le premier paragraphe).

NPM et Composer s'appuient sur un fichier .json, Make s'appuie sur le fichier Makefile. Ce fichier va contenir vos commandes, voire les dépendances entre vos commandes, et peut être mis en place très simplement puis enrichi au fur et à mesure du temps.

Un Makefile est avant tout composé de cibles (target) qui décrivent des recettes (recipes) que l'on peut assimiler à des commandes :

cible1:
@echo "commande 1"

cible2:
@echo "commande 2"

cible3: cible1 cible2
@echo "commande 3"

À l'usage, nous obtenons :

$ make cible1
commande 1

$ make cible2
commande 2

$ make cible3
commande1
commande2
commande3

Comme vous l'avez peut être deviné, la directive cible: [dépendances] permet d'exécuter les dépendances avant les commandes de la cible proprement dite.

Attention à bien utiliser des caractères Tab pour indenter les commandes dans les cibles, ce paramétrage peut dépendre de votre éditeur de texte. Si vous utilisez un fichier .editorconfig, cette directive peut vous aider :

[Makefile]
indent_style = tab

Un exemple un peu plus conséquent pour un projet NodeJS/Express :

.DEFAULT_GOAL:=help

help:
@echo -e "The great Makefile for my project, more info to come"

env:
. ${HOME}/.nvm/nvm.sh && nvm use

install:
npm install

lint:
npx eslint .

lint-html-report:
npx eslint -f html -o ./lint-report/index.html .

test:
npx jest

test-api: test
npx dredd

doc-api:
npx @redocly/cli build-docs api-description.yml -o public/index.html

start:
npx nodemon src/app.js

Dans cet exemple, nous pouvons noter la directive .DEFAULT_GOAL qui permet de définir la cible appelée par défaut lorsque l'on lance uniquement make sans autre argument, par défaut nous afficherons donc un message d'aide (pas très utile pour le moment).

Vous avez aussi remarqué que lorsque l'on lance les tests "API", on lance au préalable les tests unitaires (test-api: test).

Cet exemple de Makefile n'apporte pas grand chose à votre workflow de développement par rapport aux scripts gérés dans le fichier package.json, je pense toutefois que votre créativité vous permet d'entrevoir des usages très pratiques au quotidien.

Makefile level 2

Voici dans ce paragraphe quelques astuces et pistes pour commencer à donner des super-pouvoirs à votre Makefile.

Les variables

Le Makefile supporte des variables, ceci va vous permettre de le gérer plus finement et  de le rendre plus portable :

  • Définir des "constantes" pour éviter de répéter de longues lignes de commandes ;
  • Définir des éléments "configurables" selon le projet ;
  • Affecter des valeurs dynamiques et adapter votre workflow en conséquence.

Les variables sont affectées avec le signe '=' MA_VAR=something et leur valeur est accessible avec le caractère '$' echo $(MA_VAR).

# Define handy constants
SC=php $(PWD)/app/bin/console

# Define project specific tools
NODE_PACKAGE_MANAGER=yarn

# Get some project data
COUNT_JPG_FILES = $(shell ls -1  ./img/*.jpg | wc -l)

...

purge-cache:
$(SC) cache:clear

install:
$(NODE_PACKAGE_MANAGER) install

process-jpg:
  @echo "Processing jpg images"
  @if [ $(COUNT_JPG_FILES) -gt 1 ]; then \
      echo "processing images"; \
      ...
  else \
      echo "No image found for processing"; \
  fi

Inclusion de fichiers

Il est possible d'inclure des fichiers dans votre Makefile, ce point nous semble particulièrement intéressant pour deux aspects :

  • Gérer des variables liée à votre environnement (les fameux fichiers .env) ;
  • Importer des fonctions ou commandes d'autres fichiers, par exemple pour vous créer une petite bibliotèque de fonctions réutilisables.
# Load current environment
include $(PWD)/.env # contains APP_ENV var
include $(PWD)/.env.local
include $(PWD)/.env.$(APP_ENV).local # contains variables for dev/test environment

# Include common functions
LIB_PATH=~/git-repos/makefile-helper
include $(LIB_PATH)/docker-helper

...

start:
$(DOCKER_RUN) my-container

Auto-documenter son Makefile

Si vous adhérez à cette approche du Makefile, vous allez vraissemblablement avoir de plus en plus de tâches gérées par ce biais. Le risque est d'avoir arrété de documenter votre projet avec les commandes "natives" (cf le premier paragraphe) mais que vous deviez documenter les diverses commandes make , ou pire, avoir recours au cat Makefile.

Avec un peu de convention et un morceau de script magique nous allons pouvoir documenter automatiquement notre Makefile au travers de commentaires (lignes qui commencent par '#') :

  • La séquence # - définit un titre (ou une section) de premier niveau, ## -- un titre de deuxième niveau ;
  • Une cible sans commande, avec une majuscule, définit un libellé de section ;
  • La séquence ## définit un commentaire de cible (target).

Voici un petit exemple de Makefile auto-documentable :

# -
Main:

## Display this help dialog

help:
@echo -e "The great Makefile for my project, more info to come"

## Start the application
start:
$(DOCKER_RUN) my-container

...

# -
Frontend:

## --
Code-quality:

## Lint code
lint:
npx eslint .

## Launch unit tests
test:
npx jest

## --
Image-processing:

## Compress jpg files
process-jpg:
    @echo "Processing jpg images"

...

Et le script magique :

SHELL=/bin/bash

.DEFAULT_GOAL:=help

# Color vars
RED=\033[1;31m
YELLOW = \033[1;93m
CYAN=\033[1;36m
GREEN = \033[0;32m
WHITE = \033[0m

# Commands

# -
Main:

## Display this help dialog
help:
@echo -e "$(YELLOW)My project$(WHITE)"
@echo -e "This Makefile helps you using your local development environment."
@echo -e "Usage: make <action>"
@awk '/^[a-zA-Z\-_0-9]+:/ { \
separator = match(lastLine, /^# -/); \
if (separator) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
printf "\n$(YELLOW)== %s ==$(WHITE)\n", helpCommand; \
} \
separator = match(lastLine, /^## --/); \
if (separator) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
printf "$(CYAN)= %s =$(WHITE)\n", helpCommand; \
} \
helpMessage = match(lastLine, /^## (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \
if(helpMessage!="--") {printf "$(GREEN)%-40s$(WHITE) %s\n", helpCommand, helpMessage;} \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)

...

Vous devriez maintenant avoir un message d'aide qui ressemble à cela :

Aide d'un Makefile

Bonus : amusez vous avec un générateur de banière ASCII pour agrémenter votre première ligne (http://patorjk.com/software/taag/#p=display&f=Graffiti&t=Type%20Something%20, attention certaines polices sont plus compliquées à intégrer du fait de caractères spéciaux).

Et après ?

Il reste beaucoup de chose à apprendre et à creuser autour de l'utilisation des Makefiles, mais à ce stade, cela a déjà bien amélioré notre confort de travail au quotidien. Allez jeter un œil au site https://makefiletutorial.com/, c'est une mine d'informations.

N'hésitez pas à nous faire part de vos commentaires via le formulaire de contact, nous serions heureux d'avoir votre retour sur ce billet.

 

Happy coding !