Les gestionnaires de projet

In [1]:
source .notebooksrc

Les principaux outils

Make

  • Make est un outils Linux qui vous aide à construire votre programme en exprimant des recettes de compilation à partir de règles de dépendance.
  • L'essentiel de la gestion de votre projet est décrite par le fichier Makefile.
  • Quand le projet grossit, ce fichier peut rapidement être complexe. Il devient délicat à écrire et à maintenir, en particulier quand les dépendances sont nombreuses et varient d'un système d'exploitation à l'autre.

Autotools

Pour les gros projets, GNU proprose un ensemble d'outils regroupés sous le nom d'autotools (Make en fait partie). Il s'agit d'une chaîne assez complexe d'outils qui vise à écrire un Makefile qui s'adapte l'architecture et au système d'exploitation :

  • AutoGen
  • Autoconf
  • Automake
  • Libtool
  • Make.

Fonctionnement des Autotools (source Wikipedia, licence CC BY-SA 3.0)

CMake

CMake (pour cross platform make) est un moteur de production multiplate-forme. À partir d'un fichier de configuration CMakeLists.txt (généralement un par projet), CMake génère des fichiers fichiers descriptifs qui permettent de construire le projet. Par dédaut, le fichier descriptif est Makefile mais CMakepeut générer d'autres types de fichiers destinés aux IDE comme Visual Studio.

Dans le cadre de ce cours, nous allons explorer Make et CMake.

Construire un Makefile (à la main)

L'invocation de la commande make seule (sans cible) va chercher à réaliser la première règle rencontrée dans le fichier Makefile.

Un fichier Makefile ressemblant à ça :

# Définition des variables
variable1 = valeur1
variable2 = valeur2

# Définition des règles
# règle 1
cible1: dépendance1
    recette1

# règle 2
cible2: dépendance2
    recette2

Important : recette1 est précédée d'une tabulation.

La règle 1 donne plusieurs informations :

  • elle dit que la cible1 (en général un fichier) est obsolète et doit être regénérée si elle n'existe pas ou si la dépendance1 (un autre fichier) est plus récente que cible1
  • elle dit comment générer une nouvelle version de la cible1 en invoquant la recette1.

Dans le cas où cible2 = dépendence1, alors la règle 2 va vérifier que la cible2 est bien réalisée avant de réaliser la règle 1.

Exemple du projet maillage

Make fonctionne avec un système de dépendances chaînées donc il est plus facile d'écrire un Makefile en commençant par la cible finale :

In [2]:
cd $ROOTDIR/maillage
# On affiche les 3 premières lignes de Makefile_1
pygmentize -l make Makefile | head -n3
# Options de compilation
CXX := g++
CXXFLAGS := -std=c++11 -Wall

Les dépendances de main.e sont obtenues en compilant chaque fichier source pour produire un objet :

In [3]:
# On affiche les lignes suivantes de Makefile_1
pygmentize -l make Makefile_1 |sed -n '5,9p'
# On veut un objet compilé à partir d'un fichier .cpp
# et qui dépend aussi de deux fichiers d'entête
main.o: main.cpp maillage.hpp quadrangle.hpp
	g++ -std=c++11 -Wall -o main.o -c main.cpp

Il faut répéter ce type de règles pour tous les fichiers objets :

In [4]:
# On affiche les autres lignes de Makefile_1
pygmentize -l make Makefile_1 |sed -n '10,24p'
point.o: point.cpp point.hpp
	g++ -std=c++11 -Wall -o point.o -c point.cpp

segment.o: segment.cpp segment.hpp
	g++ -std=c++11 -Wall -o segment.o -c segment.cpp

triangle.o: triangle.cpp triangle.hpp
	g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp

quadrangle.o: quadrangle.cpp quadrangle.hpp
	g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp

polygone.o: polygone.cpp polygone.hpp
	g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp

On ajoute également une régle pour effacer les fichiers produits par Make

In [5]:
pygmentize -l make Makefile_1 | tail -n 4
# On efface les fichiers compilés
clean:
	rm -f main.e *.o *.a

Le fichier complet compile correctement :

In [6]:
make -f Makefile_1 clean
make -f Makefile_1
rm -f main.e *.o *.a
g++ -std=c++11 -Wall -o main.o -c main.cpp
g++ -std=c++11 -Wall -o point.o -c point.cpp
g++ -std=c++11 -Wall -o segment.o -c segment.cpp
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

Note : remarquez la syntaxe make -f Makefile_1 pour utiliser le fichier Makefile_1.

mais il contient beaucoup de répétitions, sources potentielles d'erreurs :

In [7]:
pygmentize -l make Makefile_1
# On veut l'exécutable main.e produit en liant les objets 
main.e: main.o point.o segment.o triangle.o quadrangle.o polygone.o
	g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

# On veut un objet compilé à partir d'un fichier .cpp
# et qui dépend aussi de deux fichiers d'entête
main.o: main.cpp maillage.hpp quadrangle.hpp
	g++ -std=c++11 -Wall -o main.o -c main.cpp

point.o: point.cpp point.hpp
	g++ -std=c++11 -Wall -o point.o -c point.cpp

segment.o: segment.cpp segment.hpp
	g++ -std=c++11 -Wall -o segment.o -c segment.cpp

triangle.o: triangle.cpp triangle.hpp
	g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp

quadrangle.o: quadrangle.cpp quadrangle.hpp
	g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp

polygone.o: polygone.cpp polygone.hpp
	g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp

# On efface les fichiers compilés
clean:
	rm -f main.e *.o *.a

On commence par définir les variables suivantes :

In [8]:
pygmentize -l make Makefile_2
# Options de compilation
CXX := g++
CXXFLAGS := -std=c++11 -Wall

# On veut l'exécutable main.e produit en liant les objets 
main.e: main.o point.o segment.o triangle.o quadrangle.o polygone.o maillage.hpp
	$(CXX) $(CXXFLAGS) -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

main.o: main.cpp maillage.hpp
	$(CXX) $(CXXFLAGS) -o main.o -c main.cpp

point.o: point.cpp point.hpp
	$(CXX) $(CXXFLAGS) -o point.o -c point.cpp

segment.o: segment.cpp segment.hpp
	$(CXX) $(CXXFLAGS) -o segment.o -c segment.cpp

triangle.o: triangle.cpp triangle.hpp
	$(CXX) $(CXXFLAGS) -o triangle.o -c triangle.cpp

quadrangle.o: quadrangle.cpp quadrangle.hpp
	$(CXX) $(CXXFLAGS) -o quadrangle.o -c quadrangle.cpp

polygone.o: polygone.cpp polygone.hpp
	$(CXX) $(CXXFLAGS) -o polygone.o -c polygone.cpp

clean:
	rm -f main.e *.o *.a

On compile à nouveau :

In [9]:
make -f Makefile_2 clean
make -f Makefile_2
rm -f main.e *.o *.a
g++ -std=c++11 -Wall -o main.o -c main.cpp
g++ -std=c++11 -Wall -o point.o -c point.cpp
g++ -std=c++11 -Wall -o segment.o -c segment.cpp
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

On peut aller plus loin en utilisant :

  • les motifs de nom de fichier avec le caractère %
  • les variables automatiques

Voici les principales variables automatiques fournies par make :

  • $@ : la cible
  • $< : la première dépendance
  • $? : toutes les dépendances plus récentes que la cible
  • $^ : toutes les dépendances

On va les utiliser pour construire la règle générique qui capture tous les fichiers .o :

%.o: %.cpp %.hpp
  $(CXX) $(CXXFLAGS) -o $@ -c $<

Ca simplifie beaucoup le Makefile !

In [10]:
pygmentize -l make Makefile_3
# Options de compilation
CXX := g++
CXXFLAGS := -std=c++11 -Wall

# On veut l'exécutable main.e produit en liant les objets 
main.e: main.o point.o segment.o triangle.o quadrangle.o polygone.o
	$(CXX) $(CXXFLAGS) -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

# main.o dépend de main.cpp mais aussi des fichiers d'en-tête qu'il inclut
main.o: main.cpp maillage.hpp quadrangle.hpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

# les autres objets ne dépendent que de leur source et en-tête respectives
%.o: %.cpp %.hpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

clean:
	rm -f main.e *.o *.a

On vérifie la compilation :

In [11]:
make -f Makefile_3 clean
make -f Makefile_3
rm -f main.e *.o *.a
g++ -std=c++11 -Wall -o main.o -c main.cpp
g++ -std=c++11 -Wall -o point.o -c point.cpp
g++ -std=c++11 -Wall -o segment.o -c segment.cpp
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

On peut faire encore plus générique et plus clair en remplaçant la règle en utilisant $^ pour désigner toutes les dépendances (et en regroupant la liste des objets dans une variable).

In [12]:
pygmentize -l make Makefile_4
# Options de compilation
CXX := g++
CXXFLAGS := -std=c++11 -Wall
objects = point.o segment.o triangle.o quadrangle.o polygone.o

# On veut l'exécutable main.e produit en liant les objets 
main.e: main.o $(objects)
	$(CXX) $(CXXFLAGS) -o $@ $^

# main.o dépend de main.cpp mais aussi des fichiers d'en-tête qu'il inclut
main.o: main.cpp maillage.hpp quadrangle.hpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

# les autres objets ne dépendent que de leur source et en-tête respectives
%.o: %.cpp %.hpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

clean:
	rm -f main.e *.o *.a

On compile pour vérifier :

In [13]:
make -f Makefile_4 clean
make -f Makefile_4
rm -f main.e *.o *.a
g++ -std=c++11 -Wall -o main.o -c main.cpp
g++ -std=c++11 -Wall -o point.o -c point.cpp
g++ -std=c++11 -Wall -o segment.o -c segment.cpp
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

La gestion manuelle des dépendances est délicate et source d'erreurs, même pour les petits projets. Make permet de générer automatiquement les dépendances entre les fichiers :

In [14]:
pygmentize Makefile
# Options de compilation
CXX := g++
CXXFLAGS := -std=c++11 -Wall
srcs = point.cpp segment.cpp triangle.cpp quadrangle.cpp polygone.cpp
# On construit la liste des objets par substitution
objects = $(srcs:.cpp=.o)

# On veut l'exécutable main.e produit en liant les objets 
main.e: main.o $(objects)
	$(CXX) $(CXXFLAGS) -o $@ $^

# La règle qui produit les objets à partir des sources
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

# On génère des fichiers makefile contenant les dépendances
%.d: %.cpp
	@set -e; rm -f $@; \
	$(CXX) -M $(CXXFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

# On inclut les fichiers makefile de dépendances
-include $(srcs:.cpp=.d)

# On efface tout y compris les dépendances
clean:
	rm -f main.e *.o *.a *.d

On compile une première fois :

In [15]:
make clean
make
rm -f main.e *.o *.a *.d
g++ -std=c++11 -Wall -o main.o -c main.cpp
g++ -std=c++11 -Wall -o point.o -c point.cpp
g++ -std=c++11 -Wall -o segment.o -c segment.cpp
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o polygone.o -c polygone.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

On vérifie que la compilation prend en compte les dépendances en modifiant triangle.hpp qui est inclus dans quadrangle.cpp :

In [16]:
touch triangle.hpp
make
g++ -std=c++11 -Wall -o triangle.o -c triangle.cpp
g++ -std=c++11 -Wall -o quadrangle.o -c quadrangle.cpp
g++ -std=c++11 -Wall -o main.e main.o point.o segment.o triangle.o quadrangle.o polygone.o

Exercice

Dans le répertoire maillage, modifier le fichier Makefile pour :

  1. construire la bibliothèque statique libmaillage.a
  2. faire l'édition de lien avec libmaillage.a pour construire l'exécutable main.e

Correction

voir fichier Makefile_arch.

Remarque : la variable automatique $? désigne la liste des dépendances plus récentes que la cible.

In [17]:
pygmentize -l make Makefile_arch
# Options de compilation
CXX := g++
CXXFLAGS := -std=c++11 -Wall
srcs = point.cpp segment.cpp triangle.cpp quadrangle.cpp polygone.cpp
# On construit la liste des objets par substitution
objects = $(srcs:.cpp=.o)

# main.e est produit en liant main.o avec la bibliothèque libmaillage.a
main.e: main.o libmaillage.a
	$(CXX) $(CXXFLAGS) -o $@ $< -L. -lmaillage

# La bibliothèque libmaillage.a est produite à partir des objets
libmaillage.a: $(objects)
	ar r $@ $?

# La règle qui produit les objets à partir des sources
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

# On génère des fichiers makefile contenant les dépendances
%.d: %.cpp
	@set -e; rm -f $@; \
	$(CXX) -M $(CXXFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

# On inclut les fichiers makefile de dépendances
-include $(srcs:.cpp=.d)

# On efface tout y compris les dépendances
clean:
	rm -f main.e *.o *.a *.d
In [18]:
make -f Makefile_arch
./main.e
ar r libmaillage.a point.o segment.o triangle.o quadrangle.o polygone.o
ar: creating libmaillage.a
g++ -std=c++11 -Wall -o main.e main.o -L. -lmaillage
Test cout sur maillage :
Type de mailles : triangles
Nombre max de voisins : 3
Nombre de noeuds : 4
Nombre de mailles : 2

 Tests unitaires réussis, maillage : 4/4

Pour aller plus loin avec Make

Des projets organisés en sous-répertoires

  • Les projets réels sont généralement structurés en sous-dossiers, les sources étant séparées des fichiers d'en-tête
  • Ils produisent plusieurs exécutables, par exemple une suite de tests où chaque test est un exécutable

Ca complique l'écriture du Makefile !

Dans le répertoire maillage_struct, on propose un Makefile adapté à l'organisation suivante des fichiers :

In [19]:
cd $ROOTDIR
make -s -C maillage_struct clean
tree maillage_struct
maillage_struct
├── include
│   ├── maillage.hpp
│   ├── point.hpp
│   ├── polygone.hpp
│   ├── quadrangle.hpp
│   ├── segment.hpp
│   └── triangle.hpp
├── main.cpp
├── Makefile
├── src
│   ├── point.cpp
│   ├── polygone.cpp
│   ├── quadrangle.cpp
│   ├── segment.cpp
│   └── triangle.cpp
└── test
    └── test_maillage.cpp

3 directories, 14 files

Le Makefile correspondant est sensiblement plus compliqué :

In [20]:
cd $ROOTDIR/maillage_struct
pygmentize Makefile
# Compiler options
CXX := g++
CXXFLAGS := -std=c++11 -Wall
INC := include/  # .hpp files will be in this directory
CXXFLAGS := $(CXXFLAGS) -I$(INC)

# Files definition
exec := main.e
libname := maillage
libfile := lib$(libname).a

# Build list of cpp files:
srcs := $(wildcard src/*.cpp)
# Build objects list from src/ content
objects := $(srcs:.cpp=.o)
# Build tests list from test/ content
testsrcs := $(wildcard test/*.cpp)
tests = $(testsrcs:.cpp=.e)
# Build list of dependencies
depssrcs := $(srcs) $(testsrcs) main.cpp
deps = $(depssrcs:.cpp=.d)

# These dependencies are not files
.PHONY: all clean tests

# Build main.e and test/test_*.e
all: $(exec) $(tests) 

# Produce main.e by linking with main.o and libmaillage.a
$(exec): main.o $(libfile)
	$(CXX) $(CXXFLAGS) -o $@ $< -L. -l$(libname)

# Produce test exec by linking with libmaillage.a
test/%.e: test/%.o $(libfile)
	$(CXX) $(CXXFLAGS) -o $@ $< -L. -l$(libname)

# The static library libmaillage.a is built from object files
$(libfile): $(objects)
	  ar r $@ $?

# A rule to build dependencies
%.d: %.cpp
	@set -e; rm -f $@; \
	$(CXX) -M $(CXXFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

# Include dependency makefiles
-include $(deps)

# Say how to compile an .o file
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -o $@ -c $<

clean:
	rm -f $(exec) $(objects) $(libfile) $(tests) $(deps) *.o

L'avantage est que si l'on rajoute un fichier source de test, il est automatiquement pris en compte par le Makefile. On rajoute par exemple le fichier test/test_triangle.cpp :

In [21]:
# On compile d'abord le projet :
make clean
make
# Puis on rajoute le fichier source :
cat >test/test_triangle.cpp <<EOL
#include <iostream>
#include "triangle.hpp"

int main() {
    triangle t;
    t.all_testsu();
    return 0;
}
EOL
rm -f main.e src/polygone.o src/triangle.o src/segment.o src/quadrangle.o src/point.o libmaillage.a test/test_maillage.e src/polygone.d src/triangle.d src/segment.d src/quadrangle.d src/point.d test/test_maillage.d main.d *.o
g++ -std=c++11 -Wall -Iinclude/   -o main.o -c main.cpp
g++ -std=c++11 -Wall -Iinclude/   -o src/polygone.o -c src/polygone.cpp
g++ -std=c++11 -Wall -Iinclude/   -o src/triangle.o -c src/triangle.cpp
g++ -std=c++11 -Wall -Iinclude/   -o src/segment.o -c src/segment.cpp
g++ -std=c++11 -Wall -Iinclude/   -o src/quadrangle.o -c src/quadrangle.cpp
g++ -std=c++11 -Wall -Iinclude/   -o src/point.o -c src/point.cpp
ar r libmaillage.a src/polygone.o src/triangle.o src/segment.o src/quadrangle.o src/point.o
ar: creating libmaillage.a
g++ -std=c++11 -Wall -Iinclude/   -o main.e main.o -L. -lmaillage
g++ -std=c++11 -Wall -Iinclude/   -o test/test_maillage.o -c test/test_maillage.cpp
g++ -std=c++11 -Wall -Iinclude/   -o test/test_maillage.e test/test_maillage.o -L. -lmaillage
rm test/test_maillage.o

On vérifie qu'il est pris en compte :

In [22]:
make
./test/test_triangle.e
g++ -std=c++11 -Wall -Iinclude/   -o test/test_triangle.o -c test/test_triangle.cpp
g++ -std=c++11 -Wall -Iinclude/   -o test/test_triangle.e test/test_triangle.o -L. -lmaillage
rm test/test_triangle.o
 Tests unitaires réussis, triangle : 3/3

La compilation parallèle

make connaît les dépendances donc il peut déduire les compilations indépendantes et les exécuter en parallèle.

  • sur les gros projets, la compilation parallèle est indispensable !
  • sur les petits, vous pouvez gagner du temps dans la phase de développement en réduisant le temps entre l'édition et l'exécution.

Pour laisser le système choisir le nombre de processus parallèles :

make -j

Pour forcer à n processus :

make -j 4

Comparons les temps de compilation en mode séquentiel et parallèle :

In [23]:
cd $ROOTDIR/maillage
make clean
echo "Temps séquentiel [s] :"
time make -s
rm -f main.e *.o *.a *.d
Temps séquentiel [s] :
2.73
In [24]:
make clean
echo "Temps parallèle [s] :"
time make -s -j
rm -f main.e *.o *.a *.d
Temps parallèle [s] :
0.72

L'accélération est appréciable, surtout pendant les phases de développement !

Ensuite ?

Cette section n'est qu'un aperçu de l'utilisation de Make. On pourrait aller bien plus loin en suivant la documentation officielle : https://www.gnu.org/software/make/manual/.

Toutefois, on a constaté que l'écriture et la maintenance d'un Makefile pouvait se révéler délicate et longue, même pour de petits projets. On va maintenant voir comment simplifier cet travail en utilisant CMake.

Gérer un projet avec CMake

CMake possède son propre langage de programmation. Dans cette partie, nous allons découvrir les rudiments de ce langage. Pour aller plus loin, on peut se référer au site CMake qui fournit :

Un exemple simple

Dans le répertoire cmake_point/, on a rassemblé 3 fichiers sources :

In [25]:
cd $ROOTDIR
cd cmake_point
ls *pp
main.cpp  point.cpp  point.hpp
  • les fichiers point.* sont des copies des fichiers du même nom dans le répertoire maillage/.
  • main.cpp contient l'exécution des tests unitaires de la classe point :
In [26]:
pygmentize main.cpp
#include <iostream>
#include "point.hpp"

int main() {
    point p;
    p.all_testsu();
    return 0;
}

On commence par écrire le fichier CMakeLists.txt qui décrit les étapes pour générer l'exécutable point.e à partir des fichiers sources.

In [27]:
# On crée un lien vers le fichier CMakeLists.txt_basic
rm -f CMakeLists.txt && ln -s CMakeLists.txt_basic CMakeLists.txt
pygmentize ./CMakeLists.txt
cmake_minimum_required(VERSION 3.3)

project(Point)

add_executable(point.e main.cpp point.cpp)

CMake est conçu pour compiler dans un répertoire séparé des sources. C'est une bonne pratique qui permet d'isoler les fichiers générés par la compilation des fichiers sources. On crée donc le sous-répertoire build/ :

In [28]:
rm -rf build  # nettoyage préalable
mkdir build
cd build

On invoque la commande cmake en indiquant le chemin du répertoire où se trouve le fichier CMakeLists.txt :

In [29]:
cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/cmake_point/build
  • Cette commande donne des indications sur le compilateur qui sera utilisé lors de la compilation
  • Elle a généré plusieurs fichiers :
In [30]:
ls -l
total 32
-rw-r--r-- 1 jovyan users 13833 Feb 16 15:05 CMakeCache.txt
drwxr-xr-x 5 jovyan users  4096 Feb 16 15:05 CMakeFiles
-rw-r--r-- 1 jovyan users  1556 Feb 16 15:05 cmake_install.cmake
-rw-r--r-- 1 jovyan users  5529 Feb 16 15:05 Makefile
  • Le fichier CMakeCache.txt et le répertoire CMakeFiles contiennent des informations propres au projet et à la plateforme
  • cmake_install.cmake indique où seront installés les exécutables générés
  • Le fichier Makefile permet à make de gérer la compilation

CMakeCache.txt et CMakeFiles ne sont pas regénérés entièrement à chaque invocation de la commande cmake.

In [31]:
cmake ..
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/cmake_point/build

Si on veut reprendre la génération de zéro, il faut les effacer.

In [32]:
rm -rf CMakeCache.txt CMakeFiles  # On efface CMakeCache.txt et CMakeFiles
cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/cmake_point/build

Grâce à la présence du Makefile, on peut maintenant constuire le projet :

In [33]:
make
Scanning dependencies of target point.e
[ 33%] Building CXX object CMakeFiles/point.e.dir/main.cpp.o
[ 66%] Building CXX object CMakeFiles/point.e.dir/point.cpp.o
[100%] Linking CXX executable point.e
[100%] Built target point.e

puis exécuter le fichier point.e :

In [34]:
./point.e
 Tests unitaires réussis, point : 3/3

On peut obtenir le détail des commandes de compilation utilisée par make avec make VERBOSE=1 :

In [35]:
make clean
make VERBOSE=1
/usr/bin/cmake -S/builds/boileau/outils-dev-log/notebooks/cmake_point -B/builds/boileau/outils-dev-log/notebooks/cmake_point/build --check-build-system CMakeFiles/Makefile.cmake 0
/usr/bin/cmake -E cmake_progress_start /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/progress.marks
make -f CMakeFiles/Makefile2 all
make[1]: Entering directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
make -f CMakeFiles/point.e.dir/build.make CMakeFiles/point.e.dir/depend
make[2]: Entering directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
cd /builds/boileau/outils-dev-log/notebooks/cmake_point/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /builds/boileau/outils-dev-log/notebooks/cmake_point /builds/boileau/outils-dev-log/notebooks/cmake_point /builds/boileau/outils-dev-log/notebooks/cmake_point/build /builds/boileau/outils-dev-log/notebooks/cmake_point/build /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/point.e.dir/DependInfo.cmake --color=
make[2]: Leaving directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
make -f CMakeFiles/point.e.dir/build.make CMakeFiles/point.e.dir/build
make[2]: Entering directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
[ 33%] Building CXX object CMakeFiles/point.e.dir/main.cpp.o
/usr/bin/g++     -o CMakeFiles/point.e.dir/main.cpp.o -c /builds/boileau/outils-dev-log/notebooks/cmake_point/main.cpp
[ 66%] Building CXX object CMakeFiles/point.e.dir/point.cpp.o
/usr/bin/g++     -o CMakeFiles/point.e.dir/point.cpp.o -c /builds/boileau/outils-dev-log/notebooks/cmake_point/point.cpp
[100%] Linking CXX executable point.e
/usr/bin/cmake -E cmake_link_script CMakeFiles/point.e.dir/link.txt --verbose=1
/usr/bin/g++    -rdynamic CMakeFiles/point.e.dir/main.cpp.o CMakeFiles/point.e.dir/point.cpp.o  -o point.e 
make[2]: Leaving directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
[100%] Built target point.e
make[1]: Leaving directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
/usr/bin/cmake -E cmake_progress_start /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles 0

Quelques options du CMakeLists.txt

  • gérer la version du compilateur
  • gérer le type de construction (Debug, Release, etc.)
  • gérer des options de compilation
  • afficher des messages lors la génération
In [36]:
# On crée un lien vers le fichier CMakeLists.txt_options
cd ..
rm -f CMakeLists.txt && ln -s CMakeLists.txt_options CMakeLists.txt
pygmentize ./CMakeLists.txt
cmake_minimum_required(VERSION 3.3)

project(Point)

# On fixe la version du compilateur
set(CMAKE_C_COMPILER "clang++")

# On fixe un type de compilation par défaut
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release)
endif()

# Options de compilation communes
set(CMAKE_CXX_FLAGS "-std=c++11")
# Options de compilation pour le type Release
set(CMAKE_CXX_FLAGS_RELEASE "-O3")
# Options de compilation pour le type Debug
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g")

# Messages lors de la génération
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message("   Compiler CXX flags:         ${CMAKE_CXX_FLAGS}")
message("   Compiler CXX debug flags:   ${CMAKE_CXX_FLAGS_DEBUG}")
message("   Compiler CXX release flags: ${CMAKE_CXX_FLAGS_RELEASE}")

add_executable(point.e main.cpp point.cpp)

On génère avec le type de construction par défaut Release :

In [37]:
cd $ROOTDIR/cmake_point/build
rm -rf CMakeCache.txt CMakeFiles  # On efface CMakeCache.txt et CMakeFiles
cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Build type: Release
   Compiler CXX flags:         -std=c++11
   Compiler CXX debug flags:   -O0 -g
   Compiler CXX release flags: -O3
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/cmake_point/build

Ou avec le type Debug en spécifiant l'option en ligne de commande :

In [38]:
rm -rf CMakeCache.txt CMakeFiles  # On efface CMakeCache.txt et CMakeFiles
cmake -D CMAKE_BUILD_TYPE=Debug ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Build type: Debug
   Compiler CXX flags:         -std=c++11
   Compiler CXX debug flags:   -O0 -g
   Compiler CXX release flags: -O3
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/cmake_point/build

On vérifie que les options de compilation pour Debug sont bien prises en compte :

In [39]:
make VERBOSE=1
/usr/bin/cmake -S/builds/boileau/outils-dev-log/notebooks/cmake_point -B/builds/boileau/outils-dev-log/notebooks/cmake_point/build --check-build-system CMakeFiles/Makefile.cmake 0
/usr/bin/cmake -E cmake_progress_start /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/progress.marks
make -f CMakeFiles/Makefile2 all
make[1]: Entering directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
make -f CMakeFiles/point.e.dir/build.make CMakeFiles/point.e.dir/depend
make[2]: Entering directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
cd /builds/boileau/outils-dev-log/notebooks/cmake_point/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /builds/boileau/outils-dev-log/notebooks/cmake_point /builds/boileau/outils-dev-log/notebooks/cmake_point /builds/boileau/outils-dev-log/notebooks/cmake_point/build /builds/boileau/outils-dev-log/notebooks/cmake_point/build /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/point.e.dir/DependInfo.cmake --color=
Dependee "/builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/point.e.dir/DependInfo.cmake" is newer than depender "/builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/point.e.dir/depend.internal".
Dependee "/builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/CMakeDirectoryInformation.cmake" is newer than depender "/builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles/point.e.dir/depend.internal".
Scanning dependencies of target point.e
make[2]: Leaving directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
make -f CMakeFiles/point.e.dir/build.make CMakeFiles/point.e.dir/build
make[2]: Entering directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
[ 33%] Building CXX object CMakeFiles/point.e.dir/main.cpp.o
/usr/bin/g++    -std=c++11 -O0 -g   -o CMakeFiles/point.e.dir/main.cpp.o -c /builds/boileau/outils-dev-log/notebooks/cmake_point/main.cpp
[ 66%] Building CXX object CMakeFiles/point.e.dir/point.cpp.o
/usr/bin/g++    -std=c++11 -O0 -g   -o CMakeFiles/point.e.dir/point.cpp.o -c /builds/boileau/outils-dev-log/notebooks/cmake_point/point.cpp
[100%] Linking CXX executable point.e
/usr/bin/cmake -E cmake_link_script CMakeFiles/point.e.dir/link.txt --verbose=1
/usr/bin/g++  -std=c++11 -O0 -g  -rdynamic CMakeFiles/point.e.dir/main.cpp.o CMakeFiles/point.e.dir/point.cpp.o  -o point.e 
make[2]: Leaving directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
[100%] Built target point.e
make[1]: Leaving directory '/builds/boileau/outils-dev-log/notebooks/cmake_point/build'
/usr/bin/cmake -E cmake_progress_start /builds/boileau/outils-dev-log/notebooks/cmake_point/build/CMakeFiles 0

Gérer des bibliothèques

CMake se révèle particulièrement intéressant pour les projets plus complexes. Comme on l'a vu précédemment :

  • ces projets sont structurés en sous-répertoires (include/, src/, etc.)
  • la construction de ces projets passe généralement par une phase de génération d'une bibliothèque d'objets.

Prenons par exemple le projet maillage_cmake/

In [40]:
cd $ROOTDIR
tree -L 1 maillage_cmake
maillage_cmake
├── CMakeLists.txt_base
├── CMakeLists.txt_test
├── include
├── src
└── test

3 directories, 2 files

Ce projet contient un fichier CMakeLists.txt que nous allons détailler :

In [41]:
cd maillage_cmake
rm -f test/test_echec.cpp
rm -f CMakeLists.txt
ln -s CMakeLists.txt_base CMakeLists.txt
pygmentize CMakeLists.txt
cmake_minimum_required(VERSION 3.3)

project(Maillage)

# On indique où se trouvent les fichiers d'en-tête
include_directories(include)

# On construit la liste des fichiers sources à partir du contenu de src/
file(GLOB_RECURSE SOURCE_FILES src/*.cpp)

# On indique les options de compilations
set(CMAKE_CXX_FLAGS "-std=c++11")

# On crée la bibliothèque libmaillage.a
add_library(maillage ${SOURCE_FILES})

# On génère l'exécutable maillage.e à partir du fichier src/main.cpp
add_executable(maillage.e src/main.cpp)

# On lie l'exécutable avec la bibliothèque libmaillage.a
target_link_libraries(maillage.e maillage)

Générons le projet CMake :

In [42]:
rm -rf build
mkdir build
cd build
cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build

Compilons :

In [43]:
make -j
Scanning dependencies of target maillage
[ 11%] Building CXX object CMakeFiles/maillage.dir/src/main.cpp.o
[ 22%] Building CXX object CMakeFiles/maillage.dir/src/point.cpp.o
[ 33%] Building CXX object CMakeFiles/maillage.dir/src/quadrangle.cpp.o
[ 44%] Building CXX object CMakeFiles/maillage.dir/src/segment.cpp.o
[ 55%] Building CXX object CMakeFiles/maillage.dir/src/polygone.cpp.o
[ 66%] Building CXX object CMakeFiles/maillage.dir/src/triangle.cpp.o
[ 77%] Linking CXX static library libmaillage.a
[ 77%] Built target maillage
Scanning dependencies of target maillage.e
[ 88%] Building CXX object CMakeFiles/maillage.e.dir/src/main.cpp.o
[100%] Linking CXX executable maillage.e
[100%] Built target maillage.e

Nous avons créé la bibliothèque libmaillage.a et l'exécutable maillage.a :

In [44]:
tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── cmake_install.cmake
├── libmaillage.a
├── maillage.e
└── Makefile

1 directory, 5 files
In [45]:
./maillage.e
Test cout sur maillage :
Type de mailles : triangles
Nombre max de voisins : 3
Nombre de noeuds : 4
Nombre de mailles : 2

 Tests unitaires réussis, maillage : 4/4

Avec un fichier descripteur CMakeLists.txt de seulement 8 lignes, CMake construit l'ensemble du projet et gère automatiquement ses dépendances en ne recompilant que les fichiers nécessaires :

In [46]:
touch ../include/point.hpp
make -j
Scanning dependencies of target maillage
[ 11%] Building CXX object CMakeFiles/maillage.dir/src/main.cpp.o
[ 22%] Building CXX object CMakeFiles/maillage.dir/src/segment.cpp.o
[ 33%] Building CXX object CMakeFiles/maillage.dir/src/point.cpp.o
[ 44%] Building CXX object CMakeFiles/maillage.dir/src/triangle.cpp.o
[ 55%] Building CXX object CMakeFiles/maillage.dir/src/polygone.cpp.o
[ 66%] Building CXX object CMakeFiles/maillage.dir/src/quadrangle.cpp.o
[ 77%] Linking CXX static library libmaillage.a
[ 77%] Built target maillage
Scanning dependencies of target maillage.e
[ 88%] Building CXX object CMakeFiles/maillage.e.dir/src/main.cpp.o
[100%] Linking CXX executable maillage.e
[100%] Built target maillage.e
In [47]:
touch ../include/quadrangle.hpp
make -j
Scanning dependencies of target maillage
[ 11%] Building CXX object CMakeFiles/maillage.dir/src/main.cpp.o
[ 22%] Building CXX object CMakeFiles/maillage.dir/src/quadrangle.cpp.o
[ 33%] Linking CXX static library libmaillage.a
[ 77%] Built target maillage
Scanning dependencies of target maillage.e
[ 88%] Building CXX object CMakeFiles/maillage.e.dir/src/main.cpp.o
[100%] Linking CXX executable maillage.e
[100%] Built target maillage.e

CTest

CMake propose un moteur de tests unitaires efficace et facile à mettre en oeuvre.

Exemple avec maillage_cmake

On regroupe les tests unitaires dans le répertoire test/ :

In [48]:
cd $ROOTDIR/maillage_cmake
tree test
test
├── test_maillage.cpp
├── test_point.cpp
├── test_polygone.cpp
├── test_quadrangle.cpp
├── test_segment.cpp
└── test_triangle.cpp

0 directories, 6 files

Chaque fichier source est un programme principal qui exécute les tests unitaires de la classe du même nom. Par exemple :

In [49]:
pygmentize test/test_point.cpp
#include "point.hpp"

int main() {
    point p;
    return p.all_testsu();
}

Pour utiliser CTest, on ajoute les lignes suivantes au fichier CMakeLists.txt :

In [50]:
cd $ROOTDIR/maillage_cmake
rm -f CMakeLists.txt
ln -s CMakeLists.txt_test CMakeLists.txt
tail -n 15 CMakeLists.txt |pygmentize -l cmake
enable_testing()

# On construit la liste des fichiers de test
message(STATUS "Ajout des tests :")
file(GLOB_RECURSE TEST_FILES test/*.cpp)
foreach(TEST_FILE ${TEST_FILES})
  # On récupère le nom sans l'extension
  get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)
  # On crée l'exécutable test_toto.e à partir du fichier test_toto.cpp
  add_executable(${TEST_NAME}.e ${TEST_FILE})
  target_link_libraries(${TEST_NAME}.e maillage)
  # On ajoute le test "test_toto" qui utilise l'exécutable "test_toto.e"
  add_test(${TEST_NAME} ${TEST_NAME}.e)
  message("     - ${TEST_NAME}")
endforeach()

On génère le projet CMake correspondant :

In [51]:
rm -rf build_ctest
mkdir build_ctest
cd build_ctest
cmake ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Ajout des tests :
     - test_maillage
     - test_point
     - test_polygone
     - test_quadrangle
     - test_segment
     - test_triangle
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest

On compile :

In [52]:
make -j
Scanning dependencies of target maillage
[  4%] Building CXX object CMakeFiles/maillage.dir/src/point.cpp.o
[ 14%] Building CXX object CMakeFiles/maillage.dir/src/polygone.cpp.o
[ 14%] Building CXX object CMakeFiles/maillage.dir/src/quadrangle.cpp.o
[ 19%] Building CXX object CMakeFiles/maillage.dir/src/main.cpp.o
[ 23%] Building CXX object CMakeFiles/maillage.dir/src/triangle.cpp.o
[ 28%] Building CXX object CMakeFiles/maillage.dir/src/segment.cpp.o
[ 33%] Linking CXX static library libmaillage.a
[ 33%] Built target maillage
Scanning dependencies of target test_triangle.e
Scanning dependencies of target test_segment.e
Scanning dependencies of target test_polygone.e
Scanning dependencies of target test_point.e
Scanning dependencies of target test_quadrangle.e
Scanning dependencies of target test_maillage.e
Scanning dependencies of target maillage.e
[ 38%] Building CXX object CMakeFiles/test_segment.e.dir/test/test_segment.cpp.o
[ 42%] Building CXX object CMakeFiles/test_polygone.e.dir/test/test_polygone.cpp.o
[ 47%] Building CXX object CMakeFiles/test_triangle.e.dir/test/test_triangle.cpp.o
[ 52%] Building CXX object CMakeFiles/test_point.e.dir/test/test_point.cpp.o
[ 57%] Building CXX object CMakeFiles/test_quadrangle.e.dir/test/test_quadrangle.cpp.o
[ 61%] Building CXX object CMakeFiles/test_maillage.e.dir/test/test_maillage.cpp.o
[ 66%] Building CXX object CMakeFiles/maillage.e.dir/src/main.cpp.o
[ 71%] Linking CXX executable test_segment.e
[ 76%] Linking CXX executable test_polygone.e
[ 80%] Linking CXX executable test_point.e
[ 85%] Linking CXX executable test_triangle.e
[ 90%] Linking CXX executable test_quadrangle.e
[ 90%] Built target test_segment.e
[ 90%] Built target test_polygone.e
[ 90%] Built target test_point.e
[ 90%] Built target test_triangle.e
[ 90%] Built target test_quadrangle.e
[ 95%] Linking CXX executable maillage.e
[100%] Linking CXX executable test_maillage.e
[100%] Built target maillage.e
[100%] Built target test_maillage.e

On lance CTest :

In [53]:
ctest
Test project /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest
    Start 1: test_maillage
1/6 Test #1: test_maillage ....................   Passed    0.00 sec
    Start 2: test_point
2/6 Test #2: test_point .......................   Passed    0.00 sec
    Start 3: test_polygone
3/6 Test #3: test_polygone ....................   Passed    0.00 sec
    Start 4: test_quadrangle
4/6 Test #4: test_quadrangle ..................   Passed    0.00 sec
    Start 5: test_segment
5/6 Test #5: test_segment .....................   Passed    0.00 sec
    Start 6: test_triangle
6/6 Test #6: test_triangle ....................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 6

Total Test time (real) =   0.02 sec

Pour afficher la sortie des tests, on peut exécuter CTest en mode verbeux :

In [54]:
ctest -V
UpdateCTestConfiguration  from :/builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/DartConfiguration.tcl
UpdateCTestConfiguration  from :/builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/DartConfiguration.tcl
Test project /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
test 1
    Start 1: test_maillage

1: Test command: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/test_maillage.e
1: Test timeout computed to be: 10000000
1: Test cout sur maillage :
1: Type de mailles : triangles
1: Nombre max de voisins : 3
1: Nombre de noeuds : 4
1: Nombre de mailles : 2
1: 
1:  Tests unitaires réussis, maillage : 4/4
1/6 Test #1: test_maillage ....................   Passed    0.00 sec
test 2
    Start 2: test_point

2: Test command: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/test_point.e
2: Test timeout computed to be: 10000000
2:  Tests unitaires réussis, point : 3/3
2/6 Test #2: test_point .......................   Passed    0.00 sec
test 3
    Start 3: test_polygone

3: Test command: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/test_polygone.e
3: Test timeout computed to be: 10000000
3:  Tests unitaires réussis, polygone : 3/3
3/6 Test #3: test_polygone ....................   Passed    0.00 sec
test 4
    Start 4: test_quadrangle

4: Test command: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/test_quadrangle.e
4: Test timeout computed to be: 10000000
4:  Tests unitaires réussis, quadrangle : 3/3
4/6 Test #4: test_quadrangle ..................   Passed    0.00 sec
test 5
    Start 5: test_segment

5: Test command: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/test_segment.e
5: Test timeout computed to be: 10000000
5:  Tests unitaires réussis, segment : 4/4
5/6 Test #5: test_segment .....................   Passed    0.00 sec
test 6
    Start 6: test_triangle

6: Test command: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest/test_triangle.e
6: Test timeout computed to be: 10000000
6:  Tests unitaires réussis, triangle : 3/3
6/6 Test #6: test_triangle ....................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 6

Total Test time (real) =   0.02 sec

Gestion des échecs

Créons un test qui échoue :

In [55]:
cat > ../test/test_echec.cpp <<EOL
#include "point.hpp"
int main() {return 1;}
EOL

On met à jour le projet :

In [56]:
cmake ..
make -j
-- Ajout des tests :
     - test_echec
     - test_maillage
     - test_point
     - test_polygone
     - test_quadrangle
     - test_segment
     - test_triangle
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest
[ 30%] Built target maillage
Scanning dependencies of target test_echec.e
[ 43%] Building CXX object CMakeFiles/test_echec.e.dir/test/test_echec.cpp.o
[ 43%] Built target test_segment.e
[ 60%] Built target test_polygone.e
[ 60%] Built target test_triangle.e
[ 78%] Built target test_quadrangle.e
[ 78%] Built target maillage.e
[ 86%] Built target test_point.e
[ 95%] Built target test_maillage.e
[100%] Linking CXX executable test_echec.e
[100%] Built target test_echec.e

L'exécution de CTest fait apparaître un échec mais ne s'interrompt pas :

In [57]:
ctest
Test project /builds/boileau/outils-dev-log/notebooks/maillage_cmake/build_ctest
    Start 1: test_echec
1/7 Test #1: test_echec .......................***Failed    0.00 sec
    Start 2: test_maillage
2/7 Test #2: test_maillage ....................   Passed    0.00 sec
    Start 3: test_point
3/7 Test #3: test_point .......................   Passed    0.00 sec
    Start 4: test_polygone
4/7 Test #4: test_polygone ....................   Passed    0.00 sec
    Start 5: test_quadrangle
5/7 Test #5: test_quadrangle ..................   Passed    0.00 sec
    Start 6: test_segment
6/7 Test #6: test_segment .....................   Passed    0.00 sec
    Start 7: test_triangle
7/7 Test #7: test_triangle ....................   Passed    0.00 sec

86% tests passed, 1 tests failed out of 7

Total Test time (real) =   0.02 sec

The following tests FAILED:
	  1 - test_echec (Failed)
Errors while running CTest

CMake avec une bibliothèque externe

CMake possède un système de recherche de paquets externes (bibliothèques, fichiers d'en-tête, etc.) qui permet de gérer l'installation des dépendances sur différents systèmes.

Dans le cadre de ce cours, nous allons prendre l'exemple de la bibliothèque matplotlib-cpp qui permet de tracer des graphes directement depuis le programme C++.

Ce choix présente plusieurs avantages :

  • on a souvent besoin d'une bibliothèque graphique
  • matplotlib-cpp s'appuie sur la bibliothèque matplotlib pour Python qui est largement utilisée
  • l'installation est simple car tout est décrit par un unique fichier d'en-tête
  • l'installation nécessite toutefois des dépendances avec les bibliothèques Python, ce qui permettra d'illustrer le mécanisme de recherche de CMake.

Nous allons utiliser matplotlib pour représenter un maillage généré par notre bibliothèque.

Note : Il s'agit seulement d'un exercice et non d'une utilisation optimale de matplotlib !

L'exemple se trouve dans le projet maillage_plot qui contient l'en-tête matplotlibcpp.h dans le sous-répertoire ext/ :

In [58]:
cd $ROOTDIR
tree -L 2 maillage_plot/ext
maillage_plot/ext
└── matplotlibcpp.h

0 directories, 1 file

Par rapport au CMakeLists.txt du projet maillage_cmake, on ajoute les lignes suivantes :

In [59]:
cd maillage_plot
pygmentize CMakeLists.txt |sed '5,28!d'
# Recherche des dépendances Python
message(STATUS "Find Python dependencies using CMake ${CMAKE_VERSION}")
if(CMAKE_VERSION VERSION_LESS 3.12)  # Pour CMake < 3.12
  find_package(PythonInterp)
  find_package(PythonLibs)
  set(Python_NumPy_INCLUDE_DIRS "/opt/conda/lib/python3.7/site-packages/numpy/core/include")
  set(Python_LIBRARIES ${PYTHON_LIBRARIES})
  set(Python_INCLUDE_DIRS ${PYTHON_INCLUDE_DIRS} ${Python_NumPy_INCLUDE_DIRS})
else()
  if(APPLE)  # Pour MacOS seulement
    set(CMAKE_FIND_FRAMEWORK NEVER)
  endif()
  # Nécessite CMake >= 3.12
  find_package(Python3 COMPONENTS Interpreter Development required)
  set(Python_LIBRARIES ${Python3_LIBRARIES})
  set(Python_INCLUDE_DIRS ${Python3_INCLUDE_DIRS})
endif()
message("   Python_INCLUDE_DIRS      : ${Python_INCLUDE_DIRS}")
message("   Python_LIBRARIES         : ${Python_LIBRARIES}")

# On ajoute les en-têtes des biliothèques internes
include_directories(ext ${Python_INCLUDE_DIRS})

# On ajoute les en-têtes du projet

On ajoute la fonction trace() dans le fichier include/maillage.hpp :

In [60]:
pygmentize include/maillage.hpp | sed '176,213!d'
// Trace le maillage
template<class T, int n> void maillage<T, n>::trace() {
    T une_maille = getMaille(0);
    int nb_sommets = une_maille.get_nb_sommets();
    std::vector<double> x(m_nb_noeuds);
    std::vector<double> y(m_nb_noeuds);
    std::vector<double> xs(nb_sommets+1);
    std::vector<double> ys(nb_sommets+1);

    plt::figure();  // sinon: "segmentation fault" sur MacOS
    // Tracé des arêtes
    for(int i = 0; i < m_nb_mailles; i++) {
       T maille = getMaille(i);
       for(int j = 0; j < nb_sommets; j++) {
         xs[j] = maille.getPoint(j).get_x();
         ys[j] = maille.getPoint(j).get_y();
       };
       // dernier sommet = premier sommet
       xs[nb_sommets] = maille.getPoint(0).get_x();
       ys[nb_sommets] = maille.getPoint(0).get_y();
       plt::plot(xs, ys);
    }
    // Tracé des noeuds
    for(int i = 0; i < m_nb_noeuds; i++) {
       x[i] = m_noeuds[i].get_x();
       y[i] = m_noeuds[i].get_y();
    }
    // On veut des symboles circulaires
    plt::plot(x, y, "o");

    // Finalisation du tracé
    plt::title("Maillage");
    plt::axis("equal");
    plt::xlabel("x");
    plt::ylabel("y");
    plt::save("maillage.png");
    //plt::show();
    cout << "Tracé sauvé dans maillage.png" << endl;

On appelle la fonction trace() depuis le programme principal :

In [61]:
pygmentize src/main.cpp
#include <iostream>

#include "quadrangle.hpp"
#include "maillage.hpp"

int main() {
    // On crée un maillage triangle régulier
    maillage<triangle, 3> m;
    m.maillage_regulier(5);
    cout << m << endl;
    // On trace le maillage
    m.trace();
    return 0;
}

Comme d'habitude avec CMake, on compile dans un répertoire build/ séparé :

In [62]:
rm -rf build
mkdir build
cd build

Sur Mac, on aide CMake à trouver l'installation Python :

In [63]:
cmake -D Python3_ROOT_DIR=~/anaconda3 ..
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Find Python dependencies using CMake 3.16.3
-- Could NOT find Python3 (missing: required) (found version "3.9.1")
   Python_INCLUDE_DIRS      : /opt/conda/envs/outils-dev-log/include/python3.9
   Python_LIBRARIES         : /opt/conda/envs/outils-dev-log/lib/libpython3.9.so
-- Configuring done
-- Generating done
-- Build files have been written to: /builds/boileau/outils-dev-log/notebooks/maillage_plot/build

On compile :

In [64]:
make -j
Scanning dependencies of target maillage
[  4%] Building CXX object CMakeFiles/maillage.dir/src/main.cpp.o
[ 14%] Building CXX object CMakeFiles/maillage.dir/src/polygone.cpp.o
[ 14%] Building CXX object CMakeFiles/maillage.dir/src/point.cpp.o
[ 19%] Building CXX object CMakeFiles/maillage.dir/src/segment.cpp.o
[ 23%] Building CXX object CMakeFiles/maillage.dir/src/triangle.cpp.o
[ 28%] Building CXX object CMakeFiles/maillage.dir/src/quadrangle.cpp.o
[ 33%] Linking CXX static library libmaillage.a
[ 33%] Built target maillage
Scanning dependencies of target test_triangle.e
Scanning dependencies of target test_quadrangle.e
Scanning dependencies of target maillage.e
Scanning dependencies of target test_segment.e
Scanning dependencies of target test_polygone.e
Scanning dependencies of target test_maillage.e
Scanning dependencies of target test_point.e
[ 38%] Building CXX object CMakeFiles/test_triangle.e.dir/test/test_triangle.cpp.o
[ 42%] Building CXX object CMakeFiles/test_quadrangle.e.dir/test/test_quadrangle.cpp.o
[ 47%] Building CXX object CMakeFiles/test_segment.e.dir/test/test_segment.cpp.o
[ 57%] Building CXX object CMakeFiles/test_polygone.e.dir/test/test_polygone.cpp.o
[ 57%] Building CXX object CMakeFiles/test_point.e.dir/test/test_point.cpp.o
[ 61%] Building CXX object CMakeFiles/maillage.e.dir/src/main.cpp.o
[ 66%] Building CXX object CMakeFiles/test_maillage.e.dir/test/test_maillage.cpp.o
[ 71%] Linking CXX executable test_triangle.e
[ 76%] Linking CXX executable test_quadrangle.e
[ 85%] Linking CXX executable test_segment.e
[ 85%] Linking CXX executable test_polygone.e
[ 85%] Built target test_triangle.e
[ 90%] Linking CXX executable test_point.e
[ 90%] Built target test_quadrangle.e
[ 90%] Built target test_segment.e
[ 90%] Built target test_point.e
[ 90%] Built target test_polygone.e
[ 95%] Linking CXX executable test_maillage.e
[100%] Linking CXX executable maillage.e
[100%] Built target test_maillage.e
[100%] Built target maillage.e

On exécute le programme principal qui crée un maillage triangulère régulier et le trace dans le fichier maillage.png :

In [65]:
./maillage.e
Type de mailles : triangles
Nombre max de voisins : 3
Nombre de noeuds : 36
Nombre de mailles : 50

Tracé sauvé dans maillage.png

On affiche le résultat :

In [66]:
cat maillage.png | display

Conclusion

  • Nous avons vu l'outil historique Make qui permet de constuire un projet en gérant les dépendances entre fichiers.
  • Make seul est compliqué à mettre en oeuvre et à maintenir pour de gros projets et pour des plateformes multiples.
  • Pour arrêter de bricoler, on dispose de CMake, un outil de plus haut niveau :
    • qui gère automatiquement les dépendances internes,
    • qui aide à découvrir les dépendances externes,
    • qui facilite la gestion des tests unitaires.