User Tools

Site Tools


projtec:cmake

CMake

Hello World

Considérons le petit exemple suivant :

hello.c
#include <stdio.h>
 
int main() {
  printf("Hello World!\n");
  return 0;
}

Voici un fichier CMakeLists.txt minimaliste pour compiler ce petit projet :

CMakeLists.txt
cmake_minimum_required(VERSION 2.6)
project(HelloWorld C)
add_executable(hello hello.c)

Pour générer un Makefile et compiler notre projet “sur place”, il faut taper les commandes suivantes :

cmake .
make clean ; make
make VERBOSE=1   # compilation en mode verbose
make help        # pour voir la liste des cibles possibles

Pour compiler “en dehors des sources” :

mkdir build
cd build
cmake ..
make

Un des avantages de cette méthode est quelle permet de ne pas mélanger les fichiers générés par CMake et Make avec les fichiers sources. Du coup, il suffit de supprimer le répertoire build pour tout nettoyer.

Un peu plus compliqué

Considérons maintenant dans notre projet plusieurs fichiers sources (hello.c, pouet.c, pouet.h) et une bibliothèque extérieure (la library m associé à <math.h> qui contient la définition de la fonction sqrt()).

hello.c
#include <stdio.h>
#include "pouet.h"
 
int main() {
  printf("Hello World!\n");
  pouet();
  return 0;
}
pouet.h
void pouet();
pouet.c
#include <stdio.h>
#include <math.h>
 
void pouet() {
  printf("sqrt(2) = %.6f\n", sqrt(2)); 
}

Voici donc le fichier CMakeLists.txt pour compiler tout cela. On a également ajouté des variables CMAKE par défaut : CMAKE_C_FLAGS et CMAKE_INSTALL_PREFIX pour le répertoire d'installation…

CMakeLists.txt
cmake_minimum_required (VERSION 2.6)
project (HelloWorld C)
 
set(CMAKE_BUILD_TYPE DEBUG)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_C_COMPILER gcc)
set(CMAKE_C_FLAGS "-std=c99 -g -Wall")
set(CMAKE_INSTALL_PREFIX "install")
 
add_executable(hello hello.c pouet.c)
target_link_libraries(hello m)
install(TARGETS hello RUNTIME DESTINATION bin)

Notons que CMake scanne automatiquement les dépendances (sans faire néanmoins de preprocessing). Il en résulte que la dépendance sur pouet.h est ajouté implicitement.

Ensuite, on fait…

cmake .
make 
make install # make install DESTDIR="/some/absolute/path"

Utilisation d'une bibliothèque interne

Considérons maintenant le projet cmake-v2.zip, dont la structure est décrit ci-dessous :

├── CMakeLists.txt
├── hello.c
├── mylib
│   ├── CMakeLists.txt
│   ├── mylib.c
│   └── mylib.h
├── pouet.c
└── pouet.h

Ce projet contient un éxecutable hello qui utilise la bibliothèque interne mylib définit par un second fichier CMakeLists.txt dans le sous-répertoire mylib. Voici donc les deux fichiers CMake :

CMakeLists.txt
cmake_minimum_required (VERSION 2.6)
project (HelloWorld C)
set(CMAKE_C_FLAGS "-std=c99 -g -Wall")
set(CMAKE_INSTALL_PREFIX "install")
include_directories(mylib)
# link_directories(mylib)
add_executable(hello hello.c pouet.c)
target_link_libraries(hello m mylib)
install(TARGETS hello RUNTIME DESTINATION bin)
add_subdirectory(mylib)
mylib/CMakeLists.txt
add_library(mylib mylib.c)
install(TARGETS mylib ARCHIVE DESTINATION lib)
install(FILES mylib.h DESTINATION include)

Ensuite, on fait comme d'habitude…

cmake .
make
make install

On trouve alors dans le répertoire install les fichiers suivants :

├── bin
│   └── hello
├── include
│   └── mylib.h
└── lib
    └── libmylib.a

Tests Dynamiques

On ajoute à notre projet un sous-répertoire tests pour vérifier les fonctions de mylib (cmake-v3.zip).

├── CMakeLists.txt
├── hello.c
├── mylib
│   ├── CMakeLists.txt
│   ├── mylib.c
│   └── mylib.h
├── pouet.c
├── pouet.h
└── tests
    ├── CMakeLists.txt
    ├── test-mylib-bar.c
    └── test-mylib-foo.c

Voici le fichier CMake pour compiler et exécuter les tests. Par defaut, pour chaque test, CMake vérifie que le programme termine normalement (pas de signal SEGV par exemple ) et que la valeur de retour vaut EXIT_SUCCESS (0). On peut également rajouter d'autres propriétés à vérifier comme un timeout ou encore vérifier la sortie standard par une expression régulière.

tests/CMakeLists.txt
include(CTest)
enable_testing()
 
add_executable(test-mylib-foo test-mylib-foo.c)
target_link_libraries(test-mylib-foo mylib)
 
add_executable(test-mylib-bar test-mylib-bar.c)
target_link_libraries(test-mylib-bar mylib)
 
add_test(test1 test-mylib-foo)
set_tests_properties(test1 PROPERTIES PASS_REGULAR_EXPRESSION "foo" TIMEOUT 3)
add_test(test2 test-mylib-bar)
set_tests_properties(test2 PROPERTIES PASS_REGULAR_EXPRESSION "foo" TIMEOUT 3)

Pour lancer les tests :

make test

Fuites Mémoire

Pour aller un peu plus loin, on peut lancer des tests de varification de la mémoire (memcheck) :

make ExperimentalMemCheck

Ces tests se base sur la variable MEMORYCHECK_COMMAND qui est par défault sur /sur/bin/valgrind (enfin s'il est installé…). Les résultats sont dans Testing/<DATE-HOUR>/DynamicAnalysis.xml.

Les logs sont disponibles dans : Testing/Temporary/MemoryChecker.*.log

Il est possible d'ajouter les options suivantes pour obtenir des logs encore plus détaillés…

find_program(MEMORYCHECK_COMMAND valgrind)
set(MEMORYCHECK_COMMAND_OPTIONS "--leak-check=full" CACHE STRING "" FORCE)

Couverture de Code

Pour aller encore plus loin…

tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
 
include(CTest)
enable_testing()
 
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -std=c99 -g --coverage")
 
add_executable(foo foo.c)
 
add_test(test1 ./foo 2)
add_test(test2 ./foo 5)
cmake .
make ExperimentalTest # ou make test
make ExperimentalCoverage
more Testing/CoverageInfo/*

Il est plus élégant de n'activer le coverage que en mode DEBUG…

tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
 
include(CTest)
enable_testing()
 
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -g --coverage")
 
add_executable(foo foo.c)
 
add_test(test1 ./foo 2)
add_test(test2 ./foo 5)

Il faut alors compiler le code en mode DEBUG (et non pas en mode RELEASE) :

cmake -DCMAKE_BUILD_TYPE=DEBUG .
make ExperimentalTest
make ExperimentalCoverage
more Testing/CoverageInfo/*
projtec/cmake.txt · Last modified: 2024/03/18 15:06 by 127.0.0.1