<!--
  canonical-url: https://www.avonture.be/fr/blog/makefile_tips
  published:     2024-07-16
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Makefile - Tutoriel et astuces

> Astuces essentielles pour automatiser avec un Makefile. Apprenez à vérifier l'existence d'un fichier, lancer des targets dépendantes, utiliser des conditions, ignorer les erreurs et intégrer Docker dans vos scripts.

---
![Makefile - Tutoriel et astuces](/img/v2/makefile.webp)

**TL;DR**

Voici une grande référence personnelle d'astuces Makefile accumulées au fil du temps : vérifier l'existence d'un fichier ou d'un dossier, chaîner des targets dépendantes, masquer l'affichage des commandes, écrire des conditions, ignorer les erreurs, forcer Bash au lieu de `/bin/sh`, substituer des variables, les pièges tabulations-vs-espaces, les paramètres nommés, étendre une target avec `::`, lire des valeurs depuis un fichier `.env`, basculer en mode verbeux, des targets utilitaires pour git, une vérification de fraîcheur du dossier `vendor` pour les projets Composer, et une target `make help` auto-documentée.

Quand j'apprends, je prends généralement des notes. Je trouve que c'est l'une des meilleures façons de retenir ce que j'ai vu et de pouvoir y revenir à tout moment.

Voici une note que j'ai prise et révisée plusieurs fois quand j'ai pris le temps de créer mes premiers fichiers `.make`.

*Deux articles compagnons : [Linux Makefile - When to use a makefile](/blog/makefile-using-make) pour le *pourquoi*, et [Linux Makefile - Adding a help screen](/blog/makefile-help) pour la target `make` auto-documentée qui rend toutes ces astuces découvrables par vos collègues.*

**`make` n'est pas encore installé ? Cliquez pour déplier.**

Lancez simplement les commandes suivantes pour installer l'exécutable `Make` sur votre machine hôte :

```bash
$ sudo apt-get update && sudo apt-get -y install make
```

## Comment vérifier si un fichier existe ou non {#how-to-check-if-a-file-exists-or-not}

Vous pouvez utiliser l'instruction `test -s` comme ci-dessous :

**makefile**

```
@test -s .config/phpunit.xml || echo "phpunit.xml didn't exist"

    @test -s .config/phpunit.xml && echo "phpunit.xml exists"
```

Contrairement à l'instruction `ifeq`, `test` doit être indenté.

On peut utiliser la notation `{ ... }` si on doit lancer plus d'une commande ; par exemple :

**makefile**

```
@test -s .config/phpunit.xml || { echo "phpunit.xml file is missing, please take actions! Exiting..."; exit 1; }
```

On peut aussi faire ça avant, par exemple, d'inclure un fichier externe :

**makefile**

```
ifneq ("$(wildcard .env)","")
	include .env
endif
```

Bien sûr, on peut aussi faire simple, c'est-à-dire juste utiliser le `-` avant la commande pour ignorer les erreurs ; ainsi, ci-dessous, si le fichier n'existe pas, aucune erreur ne sera levée et le script continuera.

**makefile**

```
-include .env
```

## Comment vérifier si un dossier existe ou non {#how-to-check-if-a-folder-exists-or-not}

Vous pouvez utiliser l'instruction `ifeq` avec une commande shell Linux comme ci-dessous :

**makefile**

```
ifeq ($(shell test -e vendor/a/b/c || echo -n no),no)
    @echo "The folder didn't exist"
endif

ifeq ($(shell test -e vendor/a/b/c && echo -n yes),yes)
    @echo "The folder exists"
endif
```

Contrairement à l'instruction `test`, `ifeq` doit être utilisé sans indentation. Directement à la colonne zéro.

## Lancer des targets dépendantes {#running-dependent-targets}

En lançant `make php-cs-fixer`, on va d'abord lancer `vendor` puis `update-them`, et enfin `php-cs-fixer` ; autrement dit, on peut définir une liste de targets dépendantes.

**makefile**

```
php-cs-fixer: vendor update-them
	@echo "And finally run php-cs-fixer"
    vendor/bin/php-cs-fixer

vendor:
	@echo "First get vendors"

update-them:
	@echo "Then update vendors"
```

```bash
$ make php-cs-fixer
First get vendors
Then update vendors
And finally run php-cs-fixer
```

### Arrêter le job si une target échoue {#stop-the-job-if-a-target-fails}

Si l'une d'elles échoue, le script s'arrête. Dans l'exemple ci-dessous, `php-cs-fixer` n'affichera jamais `And finally run php-cs-fixer`.

**makefile**

```
php-cs-fixer: vendor update-them
	@echo "And finally run php-cs-fixer"
    vendor/bin/php-cs-fixer

vendor:
	@echo "First get vendors"

update-them:
	@echo "Then update vendors"
	exit 2 && echo "Oups, an error has occurred"
```

Lancer `make php-cs-fixer` affichera ceci :

```bash
First get vendors
Then update vendors
And finally run php-cs-fixer
```

## Utiliser la commande shell find pour récupérer toutes les dépendances {#using-shell-find-option-to-get-all-dependencies}

Imaginez que vous avez beaucoup de fichiers `.md` dans le répertoire. Si l'un change, on les concatène à nouveau. Si rien ne change, rien à faire.

Ça se fait avec une commande shell pour la dépendance :

**makefile**

```
concat: $(shell find *.md -type f)
	$(shell concat.sh)
```

(source <https://tech.davis-hansson.com/p/make/#specifying-inputs>)

## Ne pas afficher la commande {#dont-echo-the-command}

Lancer `make helloWorld` comme ci-dessous affichera deux lignes dans la console.

**makefile**

```
helloWorld:
	echo "Hello world"
```

Et la sortie dans la console :

```bash
$ echo "Hello world"
Hello world
```

Pour éviter la première, c'est-à-dire l'affichage de l'instruction lancée, préfixez-la simplement d'une arobase (`@`).

**makefile**

```
helloWorld:
	@echo "Hello world"
```

Maintenant, seule la sortie est affichée ; plus l'instruction elle-même.

## Utiliser des conditions {#using-conditional-statements}

On peut écrire une condition comme ceci mais attention à l'indentation :

**makefile**

```
init:
ifeq ($(shell expr $(SET_PHP_VERSION) \>= 8), 1)
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Upgrade dependencies to PHP 8"
else
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Downgrade dependencies to PHP 7"
endif
```

`make init SET_PHP_VERSION=7.4` affichera le message de downgrade tandis que `make init SET_PHP_VERSION=8.1` affichera celui d'upgrade.

Un autre exemple :

**makefile**

```
ifneq ("$(DB_TYPE)","")
	echo "The DB_TYPE variable is not empty
endif

ifeq ("$(DB_TYPE)","")
	echo "The DB_TYPE variable is empty
endif
```

## Ignorer une erreur {#ignore-error}

Ne pas s'arrêter en cas d'erreur : ajoutez un `-` avant la ligne, comme ceci : `-php --lint index.php`

**makefile**

```
phplint:
	-php --lint index.php
	echo "Yes, this script will continue..."
```

En cas d'erreur de linting dans `index.php`, on n'arrête pas l'exécution du script et on passe à la commande suivante.

Note : make peut éventuellement afficher un message comme `make: [makefile:114: up] Error 1 (ignored)` pour informer l'utilisateur qu'une erreur s'est produite mais a été ignorée. Si vous n'en voulez pas du tout (sortie silencieuse), voici comment faire :

**makefile**

```
[...]
	@-docker network create -d bridge my_network >/dev/null 2>&1 || true
[...]
```

Cette commande va créer un nouveau réseau Docker appelé `my_network` et, en cas d'erreur (le réseau existe déjà, par exemple), on s'en fiche et on ne veut voir aucune sortie.

## Assurez-vous d'utiliser Bash {#make-sure-youre-using-bash}

Par défaut, `make` utilise `/bin/sh`. On peut changer ça en ajoutant cette affectation en haut du `makefile` :

**makefile**

```
SHELL:=bash
```

(source : <https://tech.davis-hansson.com/p/make/#always-use-a-recent-bash>)

## Définir la target par défaut {#set-the-default-target}

Par défaut, la première target définie dans le fichier sera celle par défaut, c'est-à-dire celle lancée quand l'utilisateur tape simplement `make` en ligne de commande.

**makefile**

```
default: hello

hello:
	@echo "Hello World"
```

## Substitution {#substitution}

Prenons `make convert INFILE=readme.md`

**makefile**

```
outfile=$(INFILE:.md=.pdf)

COLOR_BLUE:=34

convert:
	@printf "\e[1;${COLOR_BLUE}m%s\e[0m\n\n" "INPUT FILE IS ${INFILE}; OUTPUT WILL BE ${outfile}"
```

La syntaxe `outfile=$(INFILE:.md=.pdf)` remplace `.md` par `.pdf` ; dans cet exemple, on peut donc déduire le fichier de sortie à partir du fichier d'entrée.

## Tabulation, pas espace {#tab-not-space}

L'indentation à utiliser dans un `makefile` est la tabulation ; pas les espaces. Utiliser des espaces casse le fichier.

**makefile**

```
helloWorld:
	echo "Hello World"
```

Il est toutefois possible d'adapter ce comportement avec `.RECIPEPREFIX` :

**makefile**

```
.RECIPEPREFIX = >

changeExt:
### This is a comment
> @printf "%s\n" "It works"
```

(source : <https://tech.davis-hansson.com/p/make/#dont-use-tabs>)

## Utiliser des paramètres {#using-parameters}

Lancer une target avec un paramètre se fait via des paramètres nommés, comme ceci :

```bash
$ make hello firstname="Christophe"
```

Ça crée une variable appelée `firstname`, que l'on peut ensuite utiliser :

**makefile**

```
hello:
	@echo "Hi ${firstname}!"
```

### Vérifier que les paramètres sont bien définis {#make-sure-parameters-are-set}

Imaginons qu'on veuille lancer `make runsql SQL='SELECT * FROM users LIMIT 10'` : l'argument `SQL` doit être défini, sinon on a un problème.

**makefile**

```
COLOR_RED:=31
COLOR_YELLOW:=33

runsql:
	@[ "${SQL}" ] && printf "\e[1;${COLOR_YELLOW}m\n%s\e[0m\n\n" "Running ${SQL}" || ( printf "\e[1;${COLOR_RED}m\n%s\e[0m\n\n" "ERROR - Please set the SQL to execute; consult the help if needed"; exit 1 )
```

## Travailler avec Docker {#working-with-docker}

Dans un makefile, on peut s'arrêter tôt si on a besoin qu'un container Docker donné soit démarré.

L'instruction `if` ci-dessous vérifie que le container `sonarqube` tourne ; si ce n'est pas le cas, parce qu'il n'a pas encore été créé ou qu'il est arrêté par exemple, une instruction d'erreur sera exécutée et le script sera arrêté.

**makefile**

```
sonar-scan:
ifeq ($(shell docker ps -a -q -f name=sonarqube -f status=running),)
    ### The sonarqube container didn't exist yet or not running (exit mode f.i.)
	@printf $(_RED) "ERROR - Please first run \"make sonar-server\""
	exit 1
endif
```

Le `if` ci-dessous vérifie si le container existe et, si non, le crée.
Le `else` sait donc que le container existe, mais va s'assurer qu'il tourne.

**makefile**

```
ifeq ($(shell docker ps -a -q -f name=sonarqube),)
    ### The sonarqube container didn't exist yet, create it.
	docker run -d --name sonarqube sonarqube:latest
else
    ### Make sure the container is running
	-@docker container start sonarqube > /dev/null 2>&1
endif
```

## Configurer Visual Studio Code {#configure-visual-studio-code}

### Ajouter l'extension makefile {#add-the-makefile-extension}

<https://marketplace.visualstudio.com/items?itemName=ms-vscode.makefile-tools>

### Ajouter le fichier .editorconfig {#add-the-editorconfig-file}

Assurez-vous que votre fichier `Makefile` est correctement formaté ; ajoutez un fichier appelé `.editorconfig` dans votre répertoire racine.

**.editorconfig**

```
[*]
end_of_line = lf
insert_final_newline = true

[Makefile]
indent_style = tab
indent_size = 4
```

## Quelques astuces {#some-tips}

### Comment étendre une target {#how-to-extend-a-target}

Vous avez une target existante, disons `hello` dans notre exemple, et vous souhaitez l'étendre en ajoutant des actions supplémentaires.

`hello` peut être définie dans le même makefile ou dans un fichier inclus, mais illustrons ça avec un exemple simple : on souhaite ajouter la sortie *Nice to meet you*.

**makefile**

```
hello:
	@echo "Hello world"

hello:
	@echo "Nice to meet you"
```

Si on lance ce fichier, voici la sortie.

```bash
$ make hello
makefile:198: warning: overriding recipe for target 'hello'
makefile:195: warning: ignoring old recipe for target 'hello'
Nice to meet you
```

La solution : utiliser `::` (on parle de *règle explicite*) et non un simple `:` après la recette ; voyez l'exemple suivant :

**makefile**

```
hello::
	@echo "Hello world"

hello::
	@echo "Nice to meet you"

hello::
	@echo "Do you have any plans for this weekend?"
```

Si on lance ce fichier, voici la sortie.

```bash
$ make hello
Hello world
Nice to meet you
Do you have any plans for this weekend?
```

Les recettes sont simplement étendues, la deuxième est ajoutée à la première et ainsi de suite ; l'ordre est donc important.

### Récupérer le répertoire courant dans une variable {#getting-the-current-directory-into-a-variable}

**makefile**

```
PWD:=$(shell pwd)

current_dir:
	@echo "The current directory is ${PWD}"
```

### Récupérer une liste de fichiers et initialiser une variable {#get-a-list-of-files-and-initialize-a-variable}

Prenons un cas réel : scanner un dossier appelé `.docker` et récupérer la liste des fichiers `compose*.yaml` qui s'y trouvent.

L'objectif est d'initialiser une variable d'environnement appelée `COMPOSE_FILE` (voir <https://docs.docker.com/compose/environment-variables/envvars/#compose_file>) pour qu'en lançant `docker compose` on puisse utiliser tous les fichiers d'un coup (c'est-à-dire sans ajouter `--file file1.yml --file file2.yml` et ainsi de suite).

Récupérer la liste des `compose*.yaml` se fait donc comme ceci :

**makefile**

```
DOCKER_YAML_FILES := $(shell find ./.docker -name 'compose*.yaml')
DOCKER_YAML_FILES := $(shell echo "$(DOCKER_YAML_FILES)" | tr ' ' ':')
```

La première ligne retournera par exemple `compose.yaml compose.override.yaml compose.mysql.yaml`.

La deuxième instruction remplacera l'espace par un deux-points (`:`).

On obtiendra donc `compose.yaml:compose.override.yaml:compose.mysql.yaml`.

Maintenant, pour lancer `docker compose config` par exemple, il suffit de faire ceci : déclarer d'abord la variable d'environnement `COMPOSE_FILE` puis lancer l'action souhaitée.

**makefile**

```
.PHONY: config
config:
 COMPOSE_FILE=${DOCKER_YAML_FILES} docker compose config
```

### Récupérer des informations depuis le fichier .env {#getting-information-from-the-env-file}

Récupérer une valeur depuis un fichier `.env` est facile : incluez-le puis utilisez les variables :

**.env**

```
DOCKER_IMAGE=cavo789/my_image
```

**makefile**

```
include .env

helloDocker:
	@echo "The name of the image is ${DOCKER_IMAGE}"
```

Cette astuce de l'`include` fonctionne avec n'importe quel fichier définissant une variable et sa valeur.

On peut parfaitement avoir un fichier appelé `Make.config`, pas `.env`.

#### Définir une variable à partir des variables d'environnement du .env {#define-variable-based-on-env-environment-variables}

Imaginons que vous avez une variable appelée `APP_ENV` dans votre fichier `.env`.

Cette variable peut valoir `local`, `test` ou ce que vous voulez. Elle vaudra `production` quand l'application tourne en environnement de production.

À partir de cette variable, on peut donc définir une variable comme ceci :

**makefile**

```
ifeq ($(APP_ENV), production)
	CMD:=
else
	CMD:=docker compose exec my_docker_image
endif
```

Ça veut dire : si on ne tourne pas en production, chaque commande sera lancée dans notre container Docker. Si on tourne en production, la commande sera exécutée directement.

Voici un exemple :

**makefile**

```
composer-update:
	${CMD} composer update --no-interaction
```

#### Utiliser une valeur par défaut si la variable n'est pas définie {#use-a-default-value-if-the-variable-is-not-defined}

Si la variable d'environnement système `PHP_VERSION` n'est pas définie, mettre sa valeur par défaut à `8.1`

**makefile**

```
PHP_VERSION := $(or $(PHP_VERSION),8.1)
```

Un autre exemple : imaginez que vous avez un fichier `.env` avec la variable `DOCKER_APP_HOME`. Mais si la variable n'est pas définie, la syntaxe ci-dessous permet de fixer une valeur par défaut.

**makefile**

```
echo $(or ${DOCKER_APP_HOME},/var/www/html)
```

Ça permet des choses comme ci-dessous : cibler une version personnalisée d'une image Docker selon la version de PHP sélectionnée.

**makefile**

```
DOCKER_PHPQA:=jakzal/phpqa:php${PHP_VERSION}-alpine
```

##### Surcharger une variable {#override-a-variable}

Même si la variable est déjà définie, vous pouvez la surcharger en la passant en ligne de commande :

```bash
$ make yamllint PHP_VERSION=8.1
```

Ça lancera la target `yamllint` avec `PHP_VERSION` à `8.1` même si la variable est déjà définie et vaut, par exemple, `7.4`.

### Comment vérifier si une variable commence par une valeur donnée ? {#how-to-check-if-a-variable-starts-with-a-given-value}

Le cas d'usage : on a une variable appelée `PHP_VERSION` et on doit détecter si on a affaire à du code PHP 7 ou à du PHP 8 ou supérieur.

**makefile**

```
IS_PHP_7 := $(shell [[ $(PHP_VERSION) =~ 7[0-9.]+$$ ]] && echo "yes")

doThing:
ifdef IS_PHP_7
	@echo "Hey, it's an old version of PHP, why not migrate to PHP 8?"
else
	@echo "Nice! You're using PHP 8 or greater"
endif
```

Comme Makefile ne supporte pas les expressions régulières, on s'appuie sur le `shell` pour exécuter la regex et retourner une chaîne non vide si l'expression correspond. Dans ce cas, la variable `IS_PHP_7` est définie et la construction `ifdef` sera vérifiée.

### Mode verbeux {#verbose-mode}

L'idée : ne pas afficher de message informatif lors de l'exécution de certaines targets.

Le code ci-dessous vérifie la présence de l'argument / valeur `--quiet` dans la variable standard `$ARGS`.

Exemple de code pour montrer comment activer/désactiver le mode verbeux dans un makefile.

En utilisant l'argument "--quiet" dans ARGS.

* Mode verbeux : lancez `make testme` en ligne de commande
* Mode silencieux : lancez `make testme ARGS="--quiet"` en ligne de commande

**makefile**

```
QUIET=$(if $(findstring --quiet,${ARGS}),true,false)

testme:
ifeq ($(QUIET),false)
	@printf '\e[1;30m%s\n\e[m' "QUIET MODE NOT ENABLED - We'll show any informative text."
endif
	@printf '\e[1;32m%s\n\n\e[m' "This is an important message"
```

Le code ci-dessus définit une variable globale `QUIET` qui vaudra `true` ou `false` selon la présence du mot-clé `--quiet` dans `ARGS`.

Ensuite, utilisez la structure conditionnelle `ifeq` pour afficher (ou masquer) le message informatif.

En lançant `make testme ARGS="--quiet"`, seul *This is an important message* sera affiché.

### Travailler avec git {#working-with-git}

#### Récupérer quelques informations importantes {#retrieve-some-important-information}

Récupérer quelques variables importantes depuis le shell :

**makefile**

```
GIT_CURRENT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)

GIT_ROOT_DIR:=$(shell git rev-parse --show-toplevel)

GIT_URL_REPO:=$(shell git config --get remote.origin.url | sed -r 's:git@([^/]+)\:(.*\.git):https\://\1/\2:g' | grep -Po '.*(?=\.)')
```

#### Quelques targets git {#some-git-targets}

Une fois les variables initialisées, on peut faire des choses comme ceci :

**makefile**

```
git_open_repo:
	@sensible-browser ${GIT_URL_REPO}

git_open_pipeline:
	@sensible-browser ${GIT_URL_REPO}/pipelines

git_open_wiki:
	@sensible-browser ${GIT_URL_REPO}/-/wikis/home
```

#### Git - Travail en cours {#git---work-in-progress}

Lancez `make git_wip` pour pousser rapidement vos changements vers le repository distant et sauter les hooks locaux :

**makefile**

```
git_wip:
	git add .
	git commit -m "wip --no-verify
	git push
```

### Travailler avec un projet PHP et les vendors {#working-with-php-project-and-vendors}

#### Mettre à jour le dossier vendor seulement quand c'est nécessaire {#updating-the-vendor-folder-only-when-needed}

Avoir une target comme ci-dessous (appelée `vendor`) permet de répondre à la question *Faut-il mettre à jour vendor ou non ?*. Ça se fait en lançant d'abord la target `composer.lock`. L'idée est ensuite de comparer la date/heure de ce fichier et celle de `composer.json`. S'il y a une différence, l'outil `Make` lancera `composer update` et générera une nouvelle version de `composer.lock` et donc une nouvelle version de `vendor` aussi.

Si aucun changement n'a été fait dans le fichier `composer.json`, il n'y a rien à faire puisque `vendor` est considéré à jour.

Plutôt simple.

**makefile**

```
composer.lock: composer.json
	${CMD} composer update --no-interaction

vendor: composer.lock
	${CMD} composer install
```

Pour jouer le scénario, lancez simplement `make vendor`.

#### PHP - Contrôles qualité {#php---quality-checks}

La portion ci-dessous peut simplement être copiée/collée dans votre propre `Makefile` pour ajouter des fonctionnalités de contrôle qualité basées sur l'image Docker <https://github.com/jakzal/phpqa>.

**makefile**

```
COLOR_BLUE:=34
COLOR_CYAN:=36
COLOR_YELLOW:=33

#### region - Implement `Quality Assurance` features; relying on the jakzal/phpqa Docker image)
#### Define the Docker image to use for PHPQA (https://github.com/jakzal/phpqa)
#! PHP_VERSION should be defined in the `.env` file
PHP_VERSION:=$(or $(PHP_VERSION),8.1)

DOCKER_PHPQA:=jakzal/phpqa:php${PHP_VERSION}-alpine

#### The full "docker run" command to be able to run a command in the jakzal/phpqa container
DOCKER_PHPQA_ARGS:=-it --rm -v ${PWD}:/project -w /project -v ${PWD}/.output/tmp-phpqa:/tmp

#### Get the current user/group ID and define the docker "-u" argument
DOCKER_PHPQA_UID_GID:=-u $(shell id -u):$(shell id -g)

#### Finally, the final command for running a command in the jakzal/phpqa container
DOCKER_PHPQA_COMMAND:=docker run ${DOCKER_PHPQA_ARGS} ${DOCKER_PHPQA_UID_GID} ${DOCKER_PHPQA}

##### Run PHAN and report problems
phan:
	clear
    #### This is how to define a local variable
	$(eval COMMAND := $(shell echo "phan --progress-bar --config-file .config/phan.php"))
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Report PHAN errors in your PHP files (no fix) using $(DOCKER_PHPQA)"
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Running \"${COMMAND}\""
	@printf "\e[1;${COLOR_BLUE}m%s\e[0m\n\n" "Note: make sure your /vendor folder is populated..."
	@docker run -it ${DOCKER_PHPQA_ARGS} ${DOCKER_PHPQA} phan --version
	@echo ""
	${DOCKER_PHPQA_COMMAND} ${COMMAND}

##### Run PHP-CS-FIXER on the codebase IN A DRY-RUN MODE
php-cs-fixer-dry-run:
	clear
    #### This is how to define a local variable
	$(eval COMMAND := $(shell echo "php-cs-fixer fix --verbose --config=/project/.config/.php-cs-fixer.php --using-cache=no --diff"))
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Report code formatting issues in your PHP files (no fix) using $(DOCKER_PHPQA)"
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Running \"${COMMAND} --dry-run\""
	${DOCKER_PHPQA_COMMAND} ${COMMAND} --dry-run

##### Run PHP-CS-FIXER on the codebase and fix problems
php-cs-fixer-fix:
	clear
    #### This is how to define a local variable
	$(eval COMMAND := $(shell echo "php-cs-fixer fix --verbose --config=/project/.config/.php-cs-fixer.php --using-cache=no --diff"))
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Automatically fix formatting issues in your PHP files using $(DOCKER_PHPQA)"
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Running \"${COMMAND}\""
	${DOCKER_PHPQA_COMMAND} ${COMMAND}

##### PHP - Check the syntax of PHP files and report syntax errors if any
phplint:
	clear
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Check syntax validity of php files using $(DOCKER_PHPQA)"
    #### https://github.com/overtrue/phplint
	${DOCKER_PHPQA_COMMAND} phplint --no-cache --exclude=vendor

##### YAML - Check the syntax of yaml files in the directory root folder and report syntax errors if any
yamllint:
	clear
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Check syntax validity of yaml files using $(DOCKER_PHPQA)"
    #### https://github.com/j13k/yaml-lint
    #### (it seems yaml-lint can't search in all the tree structure and *.yml won't get .gitlab-ci.yml; strange)
	${DOCKER_PHPQA_COMMAND} yaml-lint .*.yml *.yml .install/CI/*.yml
#### endregion
```

## Quelques fonctions {#some-functions}

### Nettoyer des dossiers {#clean-folders}

**makefile**

```
COLOR_YELLOW:=33

clean:
	@printf "\e[1;${COLOR_YELLOW}m%s\e[0m\n\n" "Remove node_modules and vendor folders"
	rm --recursive --force node_modules/ vendor/
	@printf "\e[1;${COLOR_YELLOW}m\n%s\e[0m\n\n" "Empty .cache, .output and storage/logs"
	rm --recursive --force .cache/* .output/* storage/logs/*
```

On peut aussi d'abord tester l'existence du dossier :

**makefile**

```
clean:
	test ! -e vendor || rm -rf vendor
```

### Ouvrir un navigateur web {#open-a-web-browser}

**makefile**

```
open-browser:
	@sensible-browser http://www.google.fr
	@echo "The site has been opened in your browser"
```

C'est plutôt pratique quand vous travaillez avec des repositories git :

**makefile**

```
GIT_URL_REPO:=$(shell git config --get remote.origin.url | sed -r 's:git@([^/]+)\:(.*\.git):https\://\1/\2:g' | grep -Po '.*(?=\.)')

git_open_repo:
	@sensible-browser ${GIT_URL_REPO}
```

### Makefile auto-documenté {#self-documenting-makefile}

Vous pouvez utiliser une astuce pour créer une target comme `help`. L'idée est de scanner le `makefile` courant et d'extraire la liste de tous les verbes et leur description.

![Makefile auto-documenté](./images/help.webp)

Il y a plusieurs façons d'y arriver. J'ai déjà écrit un article de blog à ce sujet : [Linux Makefile - Adding a help screen](/blog/makefile-help).

La complexité vient quand vous ajoutez des fichiers via `include` (comme le fichier `.env`) et que la structure peut être un peu différente de celle de votre `makefile`.

La solution donnée ici fonctionne pour moi.

## Tutoriels {#tutorials}

* [Makefile cheatsheet](https://devhints.io/makefile)
* <https://makefiletutorial.com/>
* [Hello, and welcome to makefile basics](https://gist.github.com/isaacs/62a2d1825d04437c6f08)
* [GNU make](https://www.gnu.org/software/make/manual/html_node/index.html)
