Un guide essentiel de Qmake

Publié: 2022-03-11

introduction

qmake est un outil système de construction fourni avec la bibliothèque Qt qui simplifie le processus de construction sur différentes plates-formes. Contrairement à CMake et Qbs , qmake faisait partie de Qt depuis le tout début et doit être considéré comme un outil "natif". Inutile de dire que l'IDE par défaut de Qt - Qt Creator - a le meilleur support de qmake prêt à l'emploi. Oui, vous pouvez également choisir les systèmes de construction CMake et Qbs pour un nouveau projet, mais ceux-ci ne sont pas très bien intégrés. Il est probable que la prise en charge de CMake dans Qt Creator s'améliorera au fil du temps, et ce sera une bonne raison de publier la deuxième édition de ce guide, destinée spécifiquement à CMake. Même si vous n'avez pas l'intention d'utiliser Qt Creator, vous pouvez toujours considérer qmake comme un deuxième système de construction au cas où vous construisiez des bibliothèques publiques ou des plugins. Pratiquement toutes les bibliothèques ou plugins tiers basés sur Qt fournissent des fichiers qmake utilisés pour s'intégrer de manière transparente dans les projets basés sur qmake. Seuls quelques-uns d'entre eux fournissent une configuration double, par exemple, qmake et CMake. Vous préférerez peut-être utiliser qmake si les conditions suivantes s'appliquent à vous :

  • Vous construisez un projet multiplateforme basé sur Qt
  • Vous utilisez Qt Creator IDE et la plupart de ses fonctionnalités
  • Vous construisez une bibliothèque/plugin autonome à utiliser par d'autres projets qmake

Ce guide décrit les fonctionnalités qmake les plus utiles et fournit des exemples concrets pour chacune d'entre elles. Les lecteurs qui découvrent Qt peuvent utiliser ce guide comme un tutoriel sur le système de construction de Qt. Les développeurs Qt peuvent traiter cela comme un livre de recettes lors du démarrage d'un nouveau projet ou peuvent appliquer de manière sélective certaines des fonctionnalités à l'un des projets existants à faible impact.

Une illustration du processus de construction de qmake

Utilisation de base de Qmake

La spécification qmake est écrite dans des fichiers .pro ("projet"). Voici un exemple du fichier .pro le plus simple possible :

 SOURCES = hello.cpp

Par défaut, cela créera un Makefile qui construira un exécutable à partir du fichier de code source unique hello.cpp .

Pour construire le binaire (exécutable dans ce cas), vous devez d'abord exécuter qmake pour produire un Makefile, puis make (ou nmake , ou mingw32-make selon votre chaîne d'outils) pour construire la cible.

En un mot, une spécification qmake n'est rien de plus qu'une liste de définitions de variables mélangées à des instructions de flux de contrôle facultatives. Chaque variable, en général, contient une liste de chaînes. Les instructions de flux de contrôle vous permettent d'inclure d'autres fichiers de spécification qmake, de contrôler des sections conditionnelles et même d'appeler des fonctions.

Comprendre la syntaxe des variables

Lors de l'apprentissage de projets qmake existants, vous serez peut-être surpris de voir comment différentes variables peuvent être référencées : \(VAR,\){VAR} ou $$(VAR)

Utilisez ce mini cheat-sheet tout en adoptant les règles :

  • VAR = value Affecter une valeur à VAR
  • VAR += value Ajouter la valeur à la liste VAR
  • VAR -= value Supprimer la valeur de la liste VAR
  • $$VAR ou $${VAR} Obtient la valeur de VAR au moment de l'exécution de qmake
  • $(VAR) Contenu d'un VAR d'environnement au moment de l'exécution du Makefile (et non de qmake)
  • $$(VAR) Contenu d'un VAR d'environnement au moment où qmake (et non Makefile) est en cours d'exécution

Modèles communs

La liste complète des variables qmake se trouve dans la spécification : http://doc.qt.io/qt-5/qmake-variable-reference.html

Passons en revue quelques modèles courants pour les projets :

 # Windows application TEMPLATE = app CONFIG += windows # Shared library (.so or .dll) TEMPLATE = lib CONFIG += shared # Static library (.a or .lib) TEMPLATE = lib CONFIG += static # Console application TEMPLATE = app CONFIG += console

Ajoutez simplement SOURCES += … et HEADERS += … pour lister tous vos fichiers de code source, et vous avez terminé.

Jusqu'à présent, nous avons examiné des modèles très basiques. Les projets plus complexes comprennent généralement plusieurs sous-projets avec des dépendances les uns sur les autres. Voyons comment gérer cela avec qmake.

Sous-projets

Le cas d'utilisation le plus courant est une application livrée avec une ou plusieurs bibliothèques et projets de test. Considérez la structure suivante :

 /project ../library ..../include ../library-tests ../application

Évidemment, on veut pouvoir tout construire d'un coup, comme ceci :

 cd project qmake && make

Pour atteindre cet objectif, nous avons besoin d'un fichier de projet qmake sous le dossier /project :

 TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library

REMARQUE : l'utilisation de CONFIG += ordered est considérée comme une mauvaise pratique - préférez utiliser .depends à la place.

Cette spécification demande à qmake de construire d'abord un sous-projet de bibliothèque car d'autres cibles en dépendent. Ensuite, il peut construire library-tests et l'application dans un ordre arbitraire car ces deux éléments sont dépendants.

La structure du répertoire du projet

Relier les bibliothèques

Dans l'exemple ci-dessus, nous avons une bibliothèque qui doit être liée à l'application. En C/C++, cela signifie que nous devons configurer quelques éléments supplémentaires :

  1. Spécifiez -I pour fournir des chemins de recherche pour les directives #include.
  2. Spécifiez -L pour fournir des chemins de recherche pour l'éditeur de liens.
  3. Spécifiez -l pour indiquer quelle bibliothèque doit être liée.

Parce que nous voulons que tous les sous-projets soient mobiles, nous ne pouvons pas utiliser de chemins absolus ou relatifs. Par exemple, nous ne ferons pas ceci : INCLUDEPATH += ../library/include et bien sûr nous ne pouvons pas référencer le binaire de la bibliothèque (fichier .a) à partir d'un dossier de construction temporaire. En suivant le principe de « séparation des préoccupations », nous pouvons rapidement réaliser que le dossier de projet d'application doit faire abstraction des détails de la bibliothèque. Au lieu de cela, il est de la responsabilité de la bibliothèque de dire où trouver les fichiers d'en-tête, etc.

Tirons parti de la directive include() de qmake pour résoudre ce problème. Dans le projet de bibliothèque, nous ajouterons une autre spécification qmake dans un nouveau fichier avec l'extension .pri (l'extension peut être n'importe quoi, mais ici i signifie include). Ainsi, la bibliothèque aurait deux spécifications : library.pro et library.pri . Le premier est utilisé pour construire la bibliothèque, le second est utilisé pour fournir tous les détails nécessaires à un projet consommateur.

Le contenu du fichier library.pri serait le suivant :

 LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary

BASEDIR spécifie le dossier du projet de bibliothèque (pour être exact, l'emplacement du fichier de spécification qmake actuel, qui est library.pri dans notre cas). Comme vous pouvez le deviner, INCLUDEPATH sera évalué à /project/library/include . DESTDIR est le répertoire dans lequel le système de génération place les artefacts de sortie, tels que (fichiers .o .a .so .dll ou .exe). Ceci est généralement configuré dans votre IDE, vous ne devez donc jamais faire d'hypothèses sur l'emplacement des fichiers de sortie.

Dans le fichier application.pro , ajoutez simplement include(../library/library.pri) et vous avez terminé.

Examinons comment le projet d'application est construit dans ce cas :

  1. Topmost project.pro est un projet de sous-répertoires. Il nous indique que le projet de bibliothèque doit d'abord être construit. Ainsi, qmake entre dans le dossier de la bibliothèque et le construit à l'aide de library.pro . A ce stade, library.a est produit et placé dans le dossier DESTDIR .
  2. Ensuite, qmake entre dans le sous-dossier de l'application et analyse le fichier application.pro . Il trouve la directive include(../library/library.pri) , qui demande à qmake de la lire et de l'interpréter immédiatement. Cela ajoute de nouvelles définitions aux variables INCLUDEPATH et LIBS , donc maintenant le compilateur et l'éditeur de liens savent où rechercher les fichiers d'inclusion, les binaires de la bibliothèque et quelle bibliothèque lier.

Nous avons sauté la construction du projet de bibliothèque-tests, mais il est identique au projet d'application. Évidemment, notre projet de test aurait également besoin de lier la bibliothèque qu'il est censé tester.

Avec cette configuration, vous pouvez facilement déplacer le projet de bibliothèque vers un autre projet qmake et l'inclure, référençant ainsi le fichier .pri . C'est exactement ainsi que les bibliothèques tierces sont distribuées par la communauté.

config.pri

Il est très courant dans un projet complexe d'avoir des paramètres de configuration partagés qui sont utilisés par de nombreux sous-projets. Pour éviter la duplication, vous pouvez à nouveau utiliser la directive include() et créer config.pri dans le dossier de niveau supérieur. Vous pouvez également avoir des « utilitaires » qmake communs partagés avec vos sous-projets, similaires à ce dont nous discutons ensuite dans ce guide.

Copie d'artefacts dans DESTDIR

Souvent, les projets ont d'« autres » fichiers qui doivent être distribués avec une bibliothèque ou une application. Nous avons juste besoin de pouvoir copier tous ces fichiers dans DESTDIR pendant le processus de construction. Considérez l'extrait suivant :

 defineTest(copyToDestDir) { files = $$1 for(FILE, files) { DDIR = $$DESTDIR FILE = $$absolute_path($$FILE) # Replace slashes in paths with backslashes for Windows win32:FILE ~= s,/,\\,g win32:DDIR ~= s,/,\\,g QMAKE_POST_LINK += $$QMAKE_COPY $$quote($$FILE) $$quote($$DDIR) $$escape_expand(\\n\\t) } export(QMAKE_POST_LINK) }

Remarque : à l'aide de ce modèle, vous pouvez définir vos propres fonctions réutilisables qui fonctionnent sur des fichiers.

Placez ce code dans /project/copyToDestDir.pri afin de pouvoir include() dans des sous-projets exigeants comme suit :

 include(../copyToDestDir.pri) MYFILES += \ parameters.conf \ testdata.db ## this is copying all files listed in MYFILES variable copyToDestDir($$MYFILES) ## this is copying a single file, a required DLL in this example copyToDestDir($${3RDPARTY}/openssl/bin/crypto.dll)

Remarque : DISTFILES a été introduit dans le même but, mais il ne fonctionne que sous Unix.

Génération de codes

Un bon exemple de génération de code en tant qu'étape prédéfinie est lorsqu'un projet C++ utilise Google protobuf. Voyons comment injecter l'exécution du protoc dans le processus de construction.

Vous pouvez facilement rechercher une solution appropriée sur Google, mais vous devez être conscient d'un cas particulier important. Imaginez que vous avez deux contrats, où A fait référence à B.

 A.proto <= B.proto

Si nous générons d'abord du code pour A.proto (pour produire A.pb.h et A.pb.cxx ) et le transmettons au compilateur, cela échouera simplement car la dépendance B.pb.h n'existe pas encore. Pour résoudre ce problème, nous devons passer toutes les étapes de génération de code proto avant de créer le code source résultant.

J'ai trouvé un excellent extrait pour cette tâche ici : https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri

C'est un script assez volumineux, mais il faut déjà savoir s'en servir :

 PROTOS = A.proto B.proto include(protobuf.pri)

En examinant protobuf.pri , vous remarquerez peut-être le modèle générique qui peut être facilement appliqué à toute compilation personnalisée ou génération de code :

 my_custom_compiler.name = my custom compiler name my_custom_compiler.input = input variable (list) my_custom_compiler.output = output file path + pattern my_custom_compiler.commands = custom compilation command my_custom_compiler.variable_out = output variable (list) QMAKE_EXTRA_COMPILERS += my_custom_compiler

Portées et conditions

Souvent, nous devons définir des déclarations spécifiquement pour une plate-forme donnée, telle que Windows ou MacOS. Qmake propose trois indicateurs de plate-forme prédéfinis : win32, macx et unix. Voici la syntaxe :

 win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }

Les étendues peuvent être imbriquées, peuvent utiliser des opérateurs ! , | et même des jokers :

 macx:debug { # include only on Mac and only for debug build HEADERS += debugging.h } win32|macx { HEADERS += windows_or_macx.h } win32-msvc* { # same as win32-msvc|win32-mscv.net }

Remarque : Unix est défini sur Mac OS ! Si vous voulez tester pour Mac OS (pas Unix générique), utilisez la condition unix:!macx .

Dans Qt Creator, les conditions de portée debug et release ne fonctionnent pas comme prévu. Pour les faire fonctionner correctement, utilisez le modèle suivant :

 CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }

Fonctions utiles

Qmake a un certain nombre de fonctions intégrées qui ajoutent plus d'automatisation.

Le premier exemple est la fonction files() . En supposant que vous ayez une étape de génération de code qui produit un nombre variable de fichiers source. Voici comment vous pouvez tous les inclure dans SOURCES :

 SOURCES += $$files(generated/*.c)

Cela trouvera tous les fichiers avec l'extension .c dans le sous-dossier generated et les ajoutera à la variable SOURCES .

Le deuxième exemple est similaire au précédent, mais maintenant la génération de code produit un fichier texte contenant les noms des fichiers de sortie (liste des fichiers) :

 SOURCES += $$cat(generated/filelist, lines)

Cela ne fera que lire le contenu du fichier et traitera chaque ligne comme une entrée pour SOURCES .

Remarque : La liste complète des fonctions intégrées peut être trouvée ici : http://doc.qt.io/qt-5/qmake-function-reference.html

Traiter les avertissements comme des erreurs

L'extrait de code suivant utilise la fonctionnalité d'étendue conditionnelle décrite précédemment :

 *g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX

La raison de cette complication est que MSVC a un indicateur différent pour activer cette option.

Génération de la version Git

L'extrait de code suivant est utile lorsque vous devez créer une définition de préprocesseur contenant la version logicielle actuelle obtenue à partir de Git :

 DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"

Cela fonctionne sur n'importe quelle plate-forme tant que la commande git est disponible. Si vous utilisez des balises Git, cela affichera la balise la plus récente, même si la branche a continué. Modifiez la commande git describe pour obtenir la sortie de votre choix.

Conclusion

Qmake est un excellent outil qui se concentre sur la construction de vos projets multiplateformes basés sur Qt. Dans ce guide, nous avons passé en revue l'utilisation de base de l'outil et les modèles les plus couramment utilisés qui maintiendront la flexibilité de la structure de votre projet et la facilité de lecture et de maintenance des spécifications de construction.

Vous voulez savoir comment améliorer l'apparence de votre application Qt ? Essayez : Comment obtenir des formes de coins arrondis en C++ à l'aide des courbes de Bézier et de QPainter : un guide étape par étape