Comme expliqué dans mon précédent billet, chaque projet basé sur CMake doit contenir un script nommé CMakeLists.txt. Ce script définit les cibles, mais il peut aussi faire beaucoup d’autres choses, comme trouver des bibliothèques tierces ou générer des fichiers d’en-tête C++. Les scripts CMake ont beaucoup de flexibilité.

Chaque fois que vous intégrez une bibliothèque externe, et souvent lorsque vous ajoutez le support d’une autre plateforme, vous devrez éditer le script. J’ai passé un long moment à éditer des scripts CMake sans vraiment comprendre le langage, car la documentation est assez éparse, mais finalement, les choses ont cliqué. Le but de ce post est de vous amener au même point aussi rapidement que possible.

Ce post ne couvrira pas toutes les commandes intégrées de CMake, car il y en a des centaines, mais c’est un guide assez complet de la syntaxe et du modèle de programmation du langage.

Hello World

Si vous créez un fichier nommé hello.txt avec le contenu suivant :

message("Hello world!") # A message to print

…vous pouvez l’exécuter depuis la ligne de commande en utilisant cmake -P hello.txt. (L’option -P exécute le script donné, mais ne génère pas de pipeline de construction). Comme prévu, il imprime « Hello world ! ».

$ cmake -P hello.txtHello world!

Toutes les variables sont des chaînes

Dans CMake, chaque variable est une chaîne. Vous pouvez substituer une variable à l’intérieur d’un littéral de chaîne en l’entourant de ${}. Ceci est appelé une référence de variable. Modifiez hello.txt comme suit:

message("Hello ${NAME}!") # Substitute a variable into the message

Maintenant, si nous définissons NAME sur la ligne de commande cmake en utilisant l’option -D, le script l’utilisera:

$ cmake -DNAME=Newman -P hello.txtHello Newman!

Quand une variable est indéfinie, elle prend par défaut la forme d’une chaîne vide:

$ cmake -P hello.txtHello !

Pour définir une variable à l’intérieur d’un script, utilisez la commande set. Le premier argument est le nom de la variable à affecter, et le second est sa valeur:

set(THING "funk")message("We want the ${THING}!")

Les guillemets autour des arguments sont facultatifs, tant qu’il n’y a pas d’espaces ou de références à des variables dans l’argument. Par exemple, j’aurais pu écrire set("THING" funk) dans la première ligne ci-dessus – cela aurait été équivalent. Pour la plupart des commandes CMake (sauf if et while, décrites ci-dessous), le choix de citer ou non de tels arguments est simplement une question de style. Lorsque l’argument est le nom d’une variable, j’ai tendance à ne pas utiliser de guillemets.

Vous pouvez simuler une structure de données en utilisant des préfixes

CMake n’a pas de classes, mais vous pouvez simuler une structure de données en définissant un groupe de variables avec des noms qui commencent par le même préfixe. Vous pouvez ensuite rechercher des variables dans ce groupe en utilisant des références de variables imbriquées ${}. Par exemple, le script suivant imprimera « John Smith vit au 123, rue Fake »:

set(JOHN_NAME "John Smith")set(JOHN_ADDRESS "123 Fake St")set(PERSON "JOHN")message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")

Vous pouvez même utiliser des références de variables dans le nom de la variable à définir. Par exemple, si la valeur de PERSON est toujours « JOHN », ce qui suit définira la variable JOHN_NAME à « John Goodman »:

set(${PERSON}_NAME "John Goodman")

Chaque déclaration est une commande

Dans CMake, chaque déclaration est une commande qui prend une liste d’arguments de type chaîne et n’a pas de valeur de retour. Les arguments sont séparés par des espaces (non cités). Comme nous l’avons déjà vu, la commande set définit une variable à la portée du fichier.

Comme autre exemple, CMake a une commande math qui effectue une arithmétique. Le premier argument doit être EXPR, le deuxième argument est le nom de la variable à assigner, et le troisième argument est l’expression à évaluer – toutes des chaînes de caractères. Notez que sur la troisième ligne ci-dessous, CMake substitue la valeur de la chaîne de MY_SUM dans l’argument englobant avant de passer l’argument à math.

math(EXPR MY_SUM "1 + 1") # Evaluate 1 + 1; store result in MY_SUMmessage("The sum is ${MY_SUM}.")math(EXPR DOUBLE_SUM "${MY_SUM} * 2") # Multiply by 2; store result in DOUBLE_SUMmessage("Double that is ${DOUBLE_SUM}.")

Il y a une commande CMake pour à peu près tout ce que vous aurez besoin de faire. La commande string vous permet d’effectuer une manipulation avancée des chaînes de caractères, y compris le remplacement d’expressions régulières. La commande file peut lire ou écrire des fichiers, ou manipuler les chemins du système de fichiers.

Commandes de contrôle de flux

Même les instructions de contrôle de flux sont des commandes. Les commandes if/endif exécutent les commandes jointes de manière conditionnelle. L’espace blanc n’a pas d’importance, mais il est courant d’indenter les commandes incluses pour plus de lisibilité. Ce qui suit vérifie si la variable intégrée de CMake WIN32 est définie:

if(WIN32) message("You're running CMake on Windows.")endif()

CMake a également des commandes while/endwhile qui, comme vous pouvez vous y attendre, répètent les commandes jointes tant que la condition est vraie. Voici une boucle qui imprime tous les nombres de Fibonacci jusqu’à un million :

set(A "1")set(B "1")while(A LESS "1000000") message("${A}") # Print A math(EXPR T "${A} + ${B}") # Add the numeric values of A and B; store result in T set(A "${B}") # Assign the value of B to A set(B "${T}") # Assign the value of T to Bendwhile()

Les conditions if et while deCMake ne sont pas écrites de la même manière que dans les autres langages. Par exemple, pour effectuer une comparaison numérique, vous devez spécifier LESS comme argument de chaîne, comme indiqué ci-dessus. La documentation explique comment écrire une condition valide.

if et while sont différentes des autres commandes CMake en ce sens que si le nom d’une variable est spécifié sans guillemets, la commande utilisera la valeur de la variable. Dans le code ci-dessus, j’ai profité de ce comportement en écrivant while(A LESS "1000000") au lieu de while("${A}" LESS "1000000") – les deux formes sont équivalentes. D’autres commandes CMake ne font pas cela.

Les listes sont juste des chaînes de caractères délimitées par des points-virgules

CMake a une règle de substitution spéciale pour les arguments non cités. Si l’argument entier est une référence de variable sans guillemets, et que la valeur de la variable contient des points-virgules, CMake divisera la valeur au niveau des points-virgules et passera plusieurs arguments à la commande englobante. Par exemple, ce qui suit passe trois arguments à math:

set(ARGS "EXPR;T;1 + 1")math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")

En revanche, les arguments cités ne sont jamais divisés en plusieurs arguments, même après substitution. CMake passe toujours une chaîne citée comme un seul argument, laissant les points-virgules intacts:

set(ARGS "EXPR;T;1 + 1")message("${ARGS}") # Prints: EXPR;T;1 + 1

Si plus de deux arguments sont passés à la commande set, ils sont joints par des points-virgules, puis affectés à la variable spécifiée. Cela crée effectivement une liste à partir des arguments:

set(MY_LIST These are separate arguments)message("${MY_LIST}") # Prints: These;are;separate;arguments

Vous pouvez manipuler de telles listes en utilisant la commande list:

set(MY_LIST These are separate arguments)list(REMOVE_ITEM MY_LIST "separate") # Removes "separate" from the listmessage("${MY_LIST}") # Prints: These;are;arguments

La commande foreach/endforeach accepte plusieurs arguments. Elle itère sur tous les arguments sauf le premier, en affectant chacun d’eux à la variable nommée:

foreach(ARG These are separate arguments) message("${ARG}") # Prints each word on a separate lineendforeach()

Vous pouvez itérer sur une liste en passant une référence de variable non citée à foreach. Comme avec toute autre commande, CMake divisera la valeur de la variable et passera plusieurs arguments à la commande:

foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments message("${ARG}") # Prints each item on a separate lineendforeach()

Les fonctions s’exécutent dans leur propre portée ; les macros ne le font pas

Dans CMake, vous pouvez utiliser une paire de commandes function/endfunction pour définir une fonction. En voici une qui double la valeur numérique de son argument, puis imprime le résultat:

function(doubleIt VALUE) math(EXPR RESULT "${VALUE} * 2") message("${RESULT}")endfunction()doubleIt("4") # Prints: 8

Les fonctions s’exécutent dans leur propre portée. Aucune des variables définies dans une fonction ne pollue la portée de l’appelant. Si vous voulez retourner une valeur, vous pouvez passer le nom d’une variable à votre fonction, puis appeler la commande set avec l’argument spécial PARENT_SCOPE:

function(doubleIt VARNAME VALUE) math(EXPR RESULT "${VALUE} * 2") set(${VARNAME} "${RESULT}" PARENT_SCOPE) # Set the named variable in caller's scopeendfunction()doubleIt(RESULT "4") # Tell the function to set the variable named RESULTmessage("${RESULT}") # Prints: 8

De même, une paire de commandes macro/endmacro définit une macro. Contrairement aux fonctions, les macros s’exécutent dans la même portée que leur appelant. Par conséquent, toutes les variables définies à l’intérieur d’une macro sont définies dans la portée de l’appelant. Nous pouvons remplacer la fonction précédente par la suivante :

macro(doubleIt VARNAME VALUE) math(EXPR ${VARNAME} "${VALUE} * 2") # Set the named variable in caller's scopeendmacro()doubleIt(RESULT "4") # Tell the macro to set the variable named RESULTmessage("${RESULT}") # Prints: 8

Les fonctions et les macros acceptent toutes deux un nombre arbitraire d’arguments. Les arguments non nommés sont exposés à la fonction sous forme de liste, via une variable spéciale nommée ARGN. Voici une fonction qui double chaque argument qu’elle reçoit, en imprimant chacun sur une ligne séparée:

function(doubleEach) foreach(ARG ${ARGN}) # Iterate over each argument math(EXPR N "${ARG} * 2") # Double ARG's numeric value; store result in N message("${N}") # Print N endforeach()endfunction()doubleEach(5 6 7 8) # Prints 10, 12, 14, 16 on separate lines

Inclusion d’autres scripts

Les variablesCMake sont définies à la portée du fichier. La commande include exécute un autre script CMake dans la même portée que le script appelant. C’est un peu comme la directive #include en C/C++. Elle est généralement utilisée pour définir un ensemble commun de fonctions ou de macros dans le script appelant. Elle utilise la variable CMAKE_MODULE_PATH comme chemin de recherche.

La commande find_package recherche les scripts de la forme Find*.cmake et les exécute également dans la même portée. De tels scripts sont souvent utilisés pour aider à trouver des bibliothèques externes. Par exemple, s’il existe un fichier nommé FindSDL2.cmake dans le chemin de recherche, find_package(SDL2) est équivalent à include(FindSDL2.cmake). (Notez qu’il existe plusieurs façons d’utiliser la commande find_package – ce n’est que l’une d’entre elles.)

La commande add_subdirectory deCMake, d’autre part, crée une nouvelle portée, puis exécute le script nommé CMakeLists.txt à partir du répertoire spécifié dans cette nouvelle portée. Vous l’utilisez généralement pour ajouter un autre sous-projet basé sur CMake, tel qu’une bibliothèque ou un exécutable, au projet appelant. Les cibles définies par le sous-projet sont ajoutées au pipeline de construction, sauf indication contraire. Aucune des variables définies dans le script du sous-projet ne pollue la portée du parent, à moins que l’option PARENT_SCOPE de la commande set ne soit utilisée.

À titre d’exemple, voici certains des scripts impliqués lorsque vous exécutez CMake sur le projet Turf:

Getting and Setting Properties

Un script CMake définit des cibles à l’aide des commandes add_executable, add_library ou add_custom_target. Une fois qu’une cible est créée, elle possède des propriétés que vous pouvez manipuler en utilisant les commandes get_property et set_property. Contrairement aux variables, les cibles sont visibles dans chaque scope, même si elles ont été définies dans un sous-répertoire. Toutes les propriétés des cibles sont des chaînes de caractères.

add_executable(MyApp "main.cpp") # Create a target named MyApp# Get the target's SOURCES property and assign it to MYAPP_SOURCESget_property(MYAPP_SOURCES TARGET MyApp PROPERTY SOURCES)message("${MYAPP_SOURCES}") # Prints: main.cpp

Les autres propriétés des cibles comprennent LINK_LIBRARIES, INCLUDE_DIRECTORIES et COMPILE_DEFINITIONS. Ces propriétés sont modifiées, indirectement, par les commandes target_link_libraries, target_include_directories et target_compile_definitions. À la fin du script, CMake utilise ces propriétés de cible pour générer le pipeline de construction.

Il y a aussi des propriétés pour d’autres entités de CMake. Il y a un ensemble de propriétés de répertoire à chaque portée de fichier. Il y a un ensemble de propriétés globales qui est accessible à partir de tous les scripts. Et il y a un ensemble de propriétés de fichier source pour chaque fichier source C/C++.

Articles

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.