Aller au contenu principal

Passer des arguments à une target GNU/Make

Si comme moi, vous appréciez la simplicité et l’automatisme dans votre travail de développeur de tous les jours, vous allez adorer avoir des fichiers Makefile dans vos projets. Et si vous aimez les targets make, vous allez avoir besoin de passer des arguments.


Les targets build, remove et cache-clear peuvent utiliser les arguments en utilisant $(COMMAND_ARGS)

De quoi parle-t-on ?

J’utilise docker au quotidien, composer et npm, mais aussi symfony. Qu’est ce qu’ils ont en commun ? Leur utilisation en ligne de commande pardi !

Et à chaque fois, avec des options bien précises :

  • Démarrer mon projet
docker-compose -f docker-compose-dev.yml up -d 
  • Installer un nouveau vendor
composer require behat/mink --dev 
  • Clear le cache de symfony
php bin/console cache:clear --env=dev

Mais si on rajoute à ça le fait que mon app symfony est dans un container, c’est plutôt : 

docker exec -it mon_container_php bash -c 'php bin/console cache:clear --env=dev'

Et ça juste pour clear le cache !

Du coup, pour faciliter tout ça, j’ai pris l’habitude d’utiliser GNU/Make. Pour simplifier, disons make.

J’ai donc mon petit Makefile, dans lequel j’ajoute mes petites targets qui vont bien (une pour clear le cache, une pour composer install …).

Ça peut être utilisé pour compiler des projets, mais pas que ! Le système de targets qui peuvent appeler d’autres targets est très pratique. La syntaxe est assez proche du shell, cependant il y a des spécificités à saisir (attention à ne pas assumer que puisque ça marche dans votre terminal, alors ça doit fonctionner dans la target)

Enfin tout ça c’est très bien pour le cache clear, mais mon composer require, lui, il a besoin de savoir que c’est quoi qu’on installe non ? Et viennent ainsi les arguments.

Le mode easy

Pas besoin de code très fancy en fait.

  • Votre target dans le fichier Makefile
composer-require: composer require $(EXTRA_ARGS) --dev 
  • Qu'on appelle comme ça
EXTRA_ARG=behat/mink make composer-require

Trop simple non ? C’est ni plus ni moins l’utilisation des variables.

Le mode compliqué (mais plus sympa à l’usage)

Alors oui, je suis faignant, comme tous les développeurs. Et je veux pas taper à chaque fois le nom de ma variable pour passer un arg. Et en plus faut que je me souvienne du nom qu’elle doit avoir !

Moi je veux faire make composer-require behat/mink  !

Et c’est là qu’on se plonge dans la doc et qu’on sort ça :

Note : ce code n’est pas de moi, vous pouvez trouver la réponse originale sur stackoverflow. Le but ici est de d’en détailler le fonctionnement.

Faut bien se dire qu’à chaque appel de make, le fichier Makefile et tous ceux inclus sont parsés et tout ce qui n’est pas dans une target est “interprété” : déclaration de variable, conditions … Donc à chaque fois que je fais un make composer-require behat/mink, ce bout de code est exécuté.

Explications

Le but de la manœuvre est de détecter si la target appelée peut avoir des arguments en ligne de commande, et si c’est le cas, lui passer ce qui est donné après en tant que tel. Sur la ligne de commande plus haut, ce serait donner behat/mink comme argument à la target composer-require .

À la ligne 1, on déclare chaque target pour lesquelles on va vouloir “interpréter ce qu’il y a après la commande comme un argument”. Imaginons que build , remove, cache-clear et composer-require sont 4 targets de notre Makefile.

A la ligne 2, on cherche à savoir si la target demandée composer-require fait partie des targets concernées. Pour ça on utilise MAKECMDGOALS . Cette variable est automatiquement définie par make et contient la liste des “goals” : c’est la liste de mots clés (chaines de caractères séparées par des espaces) qui suivent le mot “make” dans la ligne de commande.

Imaginons la target build dans votre Makefile comme ceci :

build: echo $(MAKECMDGOALS)

Et que dans votre terminal vous faites ça :

make build start

Alors MAKECMDGOALS va être affichée et vous allez voir :

build start

Et vous allez probablement avoir une erreur si start n'existe pas comme target :)

Retour à la ligne 2 : on dit à make de chercher (fonction findstring) pour le premier "goal" spécifié (fonction firstword) en ligne de commande (les “goals” étant dans MAKECMDGOALS) s'il n'est pas dans la liste des targets supportées (SUPPORTED_COMMANDS)

SUPPORTS_MAKE_ARGS contient alors le premier goal si la target est supportée (a été trouvée dans la liste), ou une chaîne vide. On ne fait rien si c’est une chaîne vide. Ce test est fait à la ligne 3.

Toute la magie opère à la ligne 4. On veut tous les goals qui arrivent après le premier pour les considérer comme arguments du premier. C’est fait grâce à la fonction wordlist :

$(wordlist setext)

Returns the list of words in text starting with word s and ending with word e (inclusive). The legitimate values of s start from 1; e may start from 0.

  • If s is bigger than the number of words in text, the value is empty.

  • If e is bigger than the number of words in text, words up to the end of text are returned.

  • If s is greater than e, nothing is returned.

— GNU make : Text functions

s est facile à deviner, on veut tous les mots à partir du second (le premier est notre target initiale). On obtient e grâce à la fonction words : elle donne le nombre de mots dans la chaîne spécifiée (ici MAKECMDGOALS). Le troisième argument text, c’est notre liste de goals ( MAKECMDGOALS).

Maintenant, COMMAND_ARGS est une variable qui contient tous nos arguments. On peut l’utiliser dans toutes nos targets, comme une simple variable :

composer-require: composer require --prefer-source --prefer-stable $(COMMAND_ARGS)

Simplement utiliser les goals supplémentaires comme arguments n’empêchera pas make de vouloir les utiliser comme target aussi. Pour éviter ça, on converti ces arguments en tant que target qui “ne font rien” à la ligne 5 :

The eval function is very special: it allows you to define new makefile constructs that are not constant; which are the result of evaluating other variables and functions. The argument to the eval function is expanded, then the results of that expansion are parsed as makefile syntax. The expanded results can define new make variables, targets, implicit or explicit rules, etc.

— GNU make : The eval function

L’utilisation d’eval ici est un peu tricky : on dit à make d’interpréter le contenu de COMMAND_ARGS en tant que target aussi, qui font ;@: (ce qui veut dire “rien”)

Inconvénients

Cette technique n’a pas que des avantages:

  • Vous ne pouvez pas chaîner des targets make si une d’entre elle a besoin d’argument, autrement les suivantes seront transformées en “do-nothing” comme vu précédemment.

    • build et start sont deux targets de votre Makefile, vous voulez faire dans votre terminal ceci : make build start

    • Si build accepte les arguments, start sera transformé en argument de "build" (et peut donner un résultat assez inattendu)

    • Pour considérer start comme une autre target, vous devez faire : make build && make start

  • Faites attentions si vos arguments contiennent des ou des . Vous pouvez obtenir encore une fois des résultats inattendus si vos targets utilisent COMMAND_ARGS à l’intérieur d’autres guillemets.

  • Si vous voulez passer une option en tant qu’argument d’une target (ex: --dev), make considérera ça comme une option pour lui même. Pour éviter ça, entourez l’option de guillemets doubles et ajoutez un espace au début.

    • Notez l'espace ici : make composer-require " --dev behat/mink"

    • Malheureusement, même comme ça, la transformation “do-nothing” par make ne fonctionnera pas non plus, et vous allez avoir un message du genre “make: *** No rule to make target ‘- - youroption’. Stop.” Ignorez la.

  • Vous ne pouvez pas avoir de = dans vos options.

Any target in the makefile may be specified as a goal (unless it starts with - or contains an =, in which case it will be parsed as a switch or variable definition, respectively).

— GNU make : Goals

In this case, I don’t know how to avoid this. If you have any clue, come explain it to me

— Me

  • Si vous devez absolument avoir un = , vous pouvez toujours exceptionnellement utiliser le mode easy vu plus haut
EXTRA_VARS="target=host_demo" make deploy

Bonus

Si vous êtes encore là à lire ces lignes, déjà merci, et vous méritez bien un petit bonus !

Vous ne pouvez pas avoir de = mais peut être que vous voulez utiliser des :  ? Pour ça il vous faut échapper ce caractère (ajouter un \ ). Vous pouvez le faire entre la ligne 4 et 5 en ajoutant ceci :

COMMAND_ARGS := $(subst :,\:,$(COMMAND_ARGS))

Vous pourriez, je sais pas moi, faire ceci par exemple :

make console debug:autowiring

Bon développement !