Bonnes pratiques C++ (2/n)

Ce billet appartient à une série consacrée aux bonnes pratiques de développement C++. Certains sont toutefois applicables à d'autres langages. Je réunirai sans doute tous ces billets dans un futur petit ouvrage, plus facile à consulter qu'un blog.

Utilisez un système de build automatisé

Il est impératif d’utiliser un système de build entièrement automatisé. À partir du moment où il y a ne serait-ce qu’une seule étape manuelle pour compiler et livrer le projet, par exemple lancer un script, fixer une variable d’environnement, copier une librairie au « bon endroit » ou installer un package, alors :

  • Vous ne pouvez mettre en place ni intégration continue, ni déploiement continu, ni tests automatiques ;
  • Lorsqu’un nouveau développeur arrive dans l’équipe, il ne pourra pas commencer à travailler avant d’avoir été formé à la manière de compiler le projet ;
  • Si vous devez reprendre le code quelques années plus tard alors qu’il ne reste plus personne de l’équipe originale dans l’entreprise, vous ne serez même pas capable de le compiler et de faire une livraison ;
  • S’il y a intervention humaine, il y a possibilité d’erreur ou de fausse manœuvre, donc la build n’est pas reproductible à coup sûr. Dans n’importe quelle entreprise ayant la certification ISO 9001, ne pas être capable de garantir la reproductibilité d’une livraison est une non-conformité.

J’ai vu des projets (y compris dans de grosses entreprises très réputées) où la compilation demandait littéralement des dizaines d’étapes manuelles : compiler chaque module un par un dans le bon ordre, renommer une librairie parce que pour des raisons historiques, le script qui la produisait ne lui donnait pas le nom attendu par le script qui la consommait, taper des lignes de commandes obscures… C’est une source d’agacement et d’erreur. Un développeur reconstruit le projet pour tester son code plusieurs dizaines de fois par jour, il n’a ni temps à perdre avec une procédure manuelle aussi alambiquée, ni attention à consacrer aux problèmes de compilation.

Un système de build automatique ne doit pas être trop verbeux. Si la compilation produit des milliers de messages (le cas est fréquent…), il devient impossible de les lire, donc c’est inutile. Pire, vous prendrez l’habitude d’ignorer toutes ces lignes de log incompréhensibles et si un jour un message d’erreur important se glisse dedans, vous ne le verrez pas. Dans l’idéal, la compilation est déclenchée par un simple bouton ou par une commande en ligne, et le résultat est juste OK ou KO – et dans ce dernier cas, bien sûr, un message indique quelle est l’erreur et où elle se trouve. Il n’y a besoin de rien de plus. Ninja est probablement l’un des meilleurs outils à l’heure actuelle : rapide, fiable et très peu verbeux.

Un système de build doit typiquement proposer deux types de compilation : une incrémentale et une complète. La compilation incrémentale consiste à ne recompiler que ce qui a besoin de l’être. C’est celle que le développeur utilise au quotidien, surtout sur les gros projets où une compilation complète peut prendre plusieurs heures. La compilation complète, en revanche, recompile tous les fichiers sans exception et reconstruit entièrement l’application depuis zéro. C’est celle qui sera utilisée pour la livraison, et éventuellement pour les tests nocturnes automatisés.

Un bon moyen pour tester une compilation incrémentale est de la lancer deux fois de suite. Si la deuxième fois reconstruit ne serait-ce qu’un seul fichier, c’est qu’un makefile est défectueux. Pour tester la compilation complète, il suffit de la lancer sur une machine vierge : si elle échoue, c’est qu’il manque des fichiers, ou des bibliothèques, ou des outils, ou des règles dans un makefile.

Il existe énormément d’outils pour automatiser les builds. Pratiquement chaque éditeur d’IDE propose le sien, sans compter les innombrables outils indépendants. Choisissez celui qui vous convient le mieux et mettez-le en place dès le début du projet. Si vous attendez trop, l’effort que cela demandera sera rédhibitoire. Voici quelques exemples d’outils utilisables :

  • make (ou ses variantes nmake et gmake) sont les briques de base pour la compilation automatique et incrémentale, il est probablement inutile de les décrire ici. Leur principal inconvénient est qu’écrire un makefile correct peut être assez fastidieux.
  • ninja fonctionne sur le même principe que make, mais est à la fois moins verbeux et beaucoup plus performant, en particulier sur les machines multi-processeurs. De plus, il est capable de scanner les fichiers sources pour y détecter les dépendances dues aux fichiers inclus, ce qui dispense de décrire ces dépendances dans les makefiles – et surtout de les mettre à jour lorsqu’elles changent. C’est l’outil que je recommande.
  • cmake est un outil permettant de générer des makefile qui seront à leur tour consommés par make ou ninja. Il a l’avantage d’être relativement indépendant de la plateforme : le même fichier de description de projet peut être utilisé pour générer un projet Visual Studio pour Windows, un makefile pour Linux, un projet Xcode pour macOS, etc. Couplé avec ninja ou make, c’est le système le plus performant aujourd’hui et sans surprise, c’est aussi le plus utilisé.
  • Jenkins est un gestionnaire de build. Il permet de créer des scripts décrivant les opérations à effectuer pour compiler un projet : exécuter des commandes, installer tel ou tel package, compiler avec tel ou tel compilateur, copier le résultat de la build dans un répertoire précis, lancer des tests unitaires, effectuer le packaging, etc. Grâce à un système de plugins permettant d’étendre les fonctionnalités de l’outil, il est possible de faire des opérations extrêmement évoluées, par exemple lancer des déploiements et des tests sur des machines distantes, y compris des smartphones ou des systèmes embarqués.
  • Team Foundation Server est l’outil de gestion de projet de Microsoft. En plus de cette fonctionnalité de base, il offre également un contrôleur de code source (basé sur Git) et un gestionnaire de build. Son point fort est de centraliser dans une seule application toutes les tâches liées au développement logiciel : spécifications, scrum board, gestion d’équipe, contrôleur de code source, builds, tests, bug reports, etc.
  • Artifactory et Nexus sont des outils permettant de stocker et de distribuer des binaires versionnés. Ils sont utiles pour mettre des librairies pré-compilées sur un serveur à la disposition des scripts de build qui tournent sur les autres machines.
  • Conan fonctionne de pair avec Artifactory et cmake, et permet d’intégrer facilement des librairies externes à un projet.

Ne compilez pas dans le répertoire des sources

La compilation d’un projet C++ génère beaucoup de fichiers : des fichiers objets, des librairies, parfois des fichiers sources générés à partir d’autres fichiers sources (lex, yacc, protobuf, etc.). Un outil comme cmake génère lui aussi beaucoup de fichiers temporaires et de fichiers de configuration. Conserver dans des répertoires séparés les sources d’un côté et les produits de compilation de l’autre présente de nombreux avantages.

  • Simplifier le travail. La plupart des IDE et des éditeurs de code proposent un mini-navigateur affichant tous les répertoires du projet et les fichiers qui s’y trouvent. Il est beaucoup plus facile de naviguer dans cette arborescence si elle n’est pas polluée par des centaines de fichiers étrangers au code source proprement dit.
  • Simplifier l’usage du contrôleur de code source. Les produits de compilation ne doivent pas être stockés dans le contrôleur de code source. Si ces derniers se trouvent mélangés aux fichiers sources, ne pas les inclure à chaque commit demande un effort supplémentaire.
  • Simplifier le nettoyage. Il suffit de supprimer purement et simplement le répertoire de compilation pour nettoyer le projet et refaire une compilation depuis zéro. C’est bien plus facile et cela demande bien moins de travail de maintenance que la méthode historique qui consistait à implémenter une target clean dans les makefiles.

En pratique, tout dépend de votre système de build. Des outils comme cmake ou Xcode obligent par construction à compiler dans un répertoire séparé, donc la question ne se pose pas. Qt Creator propose une option « shadow build » qu’il faut activer et configurer à la première ouverture d’un projet. Visual Studio compile dans un sous-répertoire Debug ou Release selon la configuration active, ce n’est pas aussi parfait que de compiler dans un répertoire totalement séparé, mais c’est un bon début. Le cas échéant, reportez-vous à la documentation de vos outils.

Réservez une machine pour les livraisons

Les développeurs ont des machines spéciales. Ils ont plus d’outils, de librairies et de SDKs installés que le commun des mortels et de plus, ils ont souvent customisé ces outils à leur goût. Si la compilation dépend d’une telle configuration inhabituelle, il est bon de le savoir au plus tôt pour pouvoir l’intégrer correctement dans la chaine de build ou dans le packaging final de l’application – ou le cas échéant, pour corriger le problème. Le meilleur moyen pour découvrir de telles dépendances cachées est de compiler sur une machine vierge. (Ou presque vierge : il faut tout de même avoir les outils de build installés, bien sûr !)

De plus, comme deux développeurs différents ont probablement customisé leur installation de façons différentes, vous avez un risque qu’une livraison réalisée sur la machine du développeur A soit différente d’une livraison réalisée sur la machine du développeur B. Comme déjà indiqué, ne pas être capable de produire des livraisons fiables et reproductibles est une non-conformité dans beaucoup d’entreprises, sans parler des conséquences en production d’une telle incertitude sur la qualité de ce qui est livré.

Une autre raison pour faire les livraisons sur une machine séparée est de détecter au plus tôt les problèmes de compilation : fichier oublié lors du dernier commit (cela arrive très souvent !), makefile incomplet ou défectueux, bibliothèque ou SDK manquant, etc.

Enfin, il est courant que certaines fonctionnalités de l’application soient désactivées sur les machines de développement, parce que dans le cas contraire, le débogage serait fastidieux ou impossible. Par exemple, il peut s’agit de sauter l’étape de vérification de la licence du logiciel, ou bien de pré-remplir un formulaire d’authentification avec les identifiants du développeur pour que ce dernier n’ait pas à les retaper des dizaines de fois par jour, ou bien de désactiver certains contrôles d’intégrité qui échoueraient pendant le débogage. Ceci s’obtient habituellement par l’intermédiaire d’une variable d’environnement ou d’un fichier au nom spécifique, que le système de build ou le logiciel lui-même vont lire pour modifier leur comportement. Bien sûr, cette porte dérobée doit être désactivée pour la livraison et les tests de l’assurance qualité, et vous aurez beaucoup moins de risque d’oublier de le faire si la compilation finale se passe sur une machine séparée.

Au pire, si avoir une machine dédiée aux livraisons n’est pas possible, effectuez au minimum les builds dans un répertoire isolé et en partant d’un check out frais, plutôt que de partir de la copie des sources sur laquelle vous travaillez quotidiennement.