Ces Mecs, ils ne sont pas drôles tous les jours !#
CMake est un constructeur de logiciel multiplate-forme dont les étapes du processus de la construction sont entièrement gérées par des fichiers de configuration ; les CMakeLists.txt
CMake supporte et construit des application écrites pour les langages C, C++, C# (CSharp), CUDA, Objective-C, Objective-C++, Fortran, HIP, ISPC, Swift, ASM, ASM_NASM, ASM_MARMASM, ASM_MASM, and ASM-ATT.
Principes du builder CMake :
- Le concept de target
- appliquer une portée aux target
- Localiser des librairies sur le système, et d’importer leurs objets pour les appliquer à ces target, de les construires si inexistantes
Objectif#
Présenter succinctement l’utilisation de CMake pour la compilation d’une application. Il est présenté ici l’utilisation pour le SGBDR PostgreSQL mais aussi des exemples pour la SDL2 et GTK3. Le cas particulier d’une librairie “IMPORTED” via l’exemple de Ncurses.
Organisation Physique & Logique#
Lors d’un projet de développement, il devient nécessaire de découper un projet important et volumineux, en fonctions et sous-modules. CMake utilise pour cela une approche logique, s’appuyant sur une structure physique.
L’approche physique utilisée est celle de The Pitchfork Layout
Objectif: Organiser les différents composants de l’application selon une structure de répertoires hiérarchiques standardisée :
- Fichiers :
- Sources
- En-têtes (“Public” & “Private”)
- main() & functions()
- Documentation
- Tests
- Bibliothèques externes intégrées dans la structure du projet
- Compléments : “language bindings”, “optional plugins”, “platform bindings”
- (…)
Structure du Projet#
Pour notre projet et exemple cible, nous incluerons, seulement les éléments nécessaires de la structure.
- Au développeur d’opter pour une approche fusionnelle, ou non, des fichiers d’en-tête
- Les fichiers sources peuvent aussi être placés dans un répertoire src, à la racine, ou dans un répertoire libs, à la racine, fonction des choix du projet, de créer ou non, des sous-modules
- Les deux répertoires src et libs ne devraient pas, d’un point de vue philosophique, être présents simultanément à la racine du projet
Projet/
|___build/
|___libs/
|___library1/
| |___include/
| lib__public.h
|
|___library2/
| |___include/
| | lib_sql_functions.h
| |
| |___src/
| lib_sql_function.c
|
|___project/
|___src/
main.c
CMake#
Basis#
Prenons l’exemple simple de la compilation du programme “Hello World” bien connu.
Préparons la structure physique ; composantes : un seul fichier source, deux répertoires à la racine.
Basis/
CMakeLists.txt
|___build/
|___libs/
|___src/
hello.c
mkdir -p Basis/{build/src}
Basis/libs/src/hello.c
#include <stdio.h>
int main(void) {
printf("Bonjour tout le monde !\n");
return 0;
}
Un fichier texte de 3 lignes est le strict minimun pour construire un projet CMake simple.
Basis/CMakeLists.txt
cmake_minimum_required(VERSION 3.27)
project(CMake_Basis)
add_executable(MyExec libs/src/hello.c)
Explications
- cmake_minimal_required() définit les fonctions minimun de la version CMake à utiliser
- project() définit le nom du projet
- add_executable() définit la target, ici l’exécutable MyExec pour la compilation du fichier source hello.c
Générer les fichiers de configuration de la compilation
cd build
cmake -B . -S ../
Output:
-- The C compiler identification is Clang 13.0.0
-- The CXX compiler identification is Clang 13.0.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (1.4s)
-- Generating done (0.0s)
-- Build files have been written to: ~/Coding/CMake/Basis/build
Générer l’exécutable
cmake --build .
Output
[ 50%] Building C object CMakeFiles/hello.dir/libs/src/hello.c.o
[100%] Linking C executable MyExec
[100%] Built target MyExec
Axiomes et bonnes pratiques#
- Des fonctionnalités historiques et pour raisons de compatibilité demeurent, au détriment des bonnes pratiques ; la présence massive de set dans des forums ou pages web, n’est pas gage d’une documentation adaptée au CMake moderne
- Cmake utilise des target, cibles concrêtes (library, executable) ou abstraites (OBJECTS & INTERFACE)
- Des properties (objets) sont appliquées aux target
CMake est un outil de construction qui automatise la “cross compilation” multiplate-forme et système. Le même CMakeLists.txt doit donc pouvoir être utilisé quelque soit, le compilateur, le système d’exploitation ou le matériel.
La raison d’être de CMake est de fournir les informations de configuration au compilateur et au linker, appliquées sur des target. La notion de portée peut-être transitive, fonction des mots clés : INTERFACE, PRIVATE ou PUBLIC.
Instructions de compilation#
Soit l’objectif d’obtenir l’équivalent des paramètres d’utilisation ci-dessous :
clang -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin
Il sera nécessaire de passer ces instructions à utiliser via target_compilation_options() et target_compile_features() :
add_executable(MyExec main.c) # Définir la target MyExec pour le fichier source main.c
target_compile_options(
MyExec PRIVATE
-Wall -Wextra -pedantic -fno-common -fno-builting
) # passe les arguments de compilation pour la target MyExec
target_compile_features(
main PRIVATE c_std_11
) # Compiler la target Myexec en utilisant la norme C std 2011
Instructions de liaisons#
Exemple pour lier la librairies ncurse à la compilation
clang -lform -lncurses
CMake dispose de la fonction Find_package(), permettant de trouver sur le système le package binaire de la distribution. Le cas de Curses / Ncurses est interressant, il n’existe pas de IMPORTED target pour Curses.
Il sera nécessaire d’indiquer les options de compilation, la localisation physique du répertoire include et celle de la librairie en définissant une IMPORTED LIBRARY via add_library :
cmake_minimum_required(VERSION 3.27)
project(
MyProject VERSION 1.0
DESCRIPTION "ncurses training"
LANGUAGES C
)
Find_package(Curses REQUIRED)
add_library(MyCurses::curses INTERFACE IMPORTED)
target_compile_options(MyCurses::curses INTERFACE ${CURSES_CFLAGS})
target_include_directories(MyCurses::curses INTERFACE ${CURSES_INCLUDE_DIRS})
target_link_libraries(MyCurses::curses INTERFACE ${CURSES_LIBRARIES})
add_executable(MyExec libs/src/main.c)
target_compile_options(
MyExec PRIVATE
-Wall -Wextra -pedantic -fno-common -fno-builtin
)
target_compile_features(
MyExec PRIVATE
c_std_17
)
target_link_libraries(MyExec PRIVATE MyCurses::curses)
Remarques :
Find_packages(Curses REQUIRED)
add_library(MyCurses::curses INTERFACE IMPORTED)
target_compile_options(MyCurses::curses INTERFACE ${CURSES_CFLAGS})
target_include_directories(MyCurses::curses INTERFACE ${CURSES_INCLUDE_DIRS})
target_link_libraries(MyCurses::curses INTERFACE ${CURSES_LIBRARIES})
add_executable(MyTarget libs/src/main.c)
target_link_libraries(MyTarget PRIVATE MyCurses::curses)
Est équivalent à l’écriture CMake Moderne d’une librairies avec des IMPORTED target
Find_package(PostgreSQL REQUIRED)
add_library(MyTarget libs/src/main.c) OR add_executable(MyTarget libs/src/main.c)
target_link_libraries(MyTarget PUBLIC PostgreSQL::PostgreSQL)
- Find_package(PostgreSQL REQUIRED) : demande à CMake de trouver sur le système le package binaire installé, dont les librairies
- target_link_libraries(main PUBLIC PostgreSQL::PostgreSSQL) : utilise les IMPORTED target des éléments trouvées par find_package()
- cf. doc CMake
PRIVATE INTERFACE OU PUBLIC
- INTERFACE : indique au compilateur que la cible (librairie MyCurses:curses ici) est utilisée comme INTERFACE et dont la portée est positionnée uniquement sur cette interface
- PRIVATE : indique au compilateur que la portée des arguments s’applique uniquement à la target, MyExec par exemple ici
- PUBLIC : indique au compilateur que les arguments ou objets peuvent se propager à l’invocateur
Mise en Pratique#
Utilisons la librairie libpq#
Une application qui requête une base de données PostgreSQL. L’application utilisera la libpq fournit par les packages PostreSQL, l’application doit nous retourner la liste de légumes disponible au sein d’une base de données “potager”.
Prérequis :
- L’API libpq-fe.h disponible via le package PostgreSQL
- Une base de données PostgreSQL : potager ici
- cmake installé sur le système
Structure Physique et logique :
Potager/
CMakeLists.txt
|___build/
|___libs/
CMakeLists.txt
|___lib_SQL_functions/
| CMakeLists.txt
| |___include/
| | lib_sql_functions.h
| |
| |___src/
| lib_sql_function.c
|
|___project_Potager/
CMakeLists.txt
|___src/
main.c
Explications:
Le CMakeLists.txt à la racine du projet est la racine de l’archtecture du projet. Il indique la structure physique via add_subdirectory(libs) qui appel le sous répertoire libs.
~/Coding/Potager/CMakeLists.txt
cmake_minimum_required(VERSION 3.27)
project(
Potager VERSION 1.0
DESCRIPTION "Gestion de planches potagères"
LANGUAGES C
)
add_subdirectory(libs)
- Project() fournit la description et le langage de programation utilisé, le Langage C
- add_subdirectory(libs) appel le CMakeLists.txt suivant au sein du sous-répertoire libs
~/Coding/Potager/libs/CMakeLists.txt
add_subdirectory(lib_SQL_Functions)
add_subdirectory(project_Potager)
- Appel des CMakeLists.txt suivant au sein des sous-répertoires lib_SQL_Functions
~/Coding/Potager/libs/lib_SQL_Functions/CMakeLists.txt
add_library(
lib_SQL_Functions
src/lib_sql_functions.c
include/lib_sql_functions.h
)
target_include_directories(lib_SQL_Functions PUBLIC include/)
# Include PostgreSQL libpq
find_package(
PostgreSQL REQUIRED
)
target_link_libraries(
lib_SQL_Functions PUBLIC PostgreSQL::PostgreSQL
)
- add_library indique à CMake de construire la librairie lib_SQL_Functions via le header lib_sql_functions.h et le source lib_sql_functions.c
- target_include_directory spécifie les répertoires d’inclusion à utiliser lors de la compilation d’une cible donnée. La target nommée doit avoir été créée par une commande telle que add_executable() ou add_library() et ne doit pas être une cible ALIAS
- find_package() fournit les IMPORTED target (objets) trouvés
- target_link_libraries lie ces IMPORTED target à la target lib_SQL_functions construite précedemment
- La portée PUBLIC est définie ici, la librairie doit être propagée à l’éxécutable principal de l’application ~/Coding/libs/src/main.c
~/Coding/Potager/libs/lib_SQL_functions/include/lib_sql_functions.h
#ifndef LIB_SQL_FUNCTIONS_H
#define LIB_SQL_FUNCTIONS_H
extern void db_exit(PGconn *conn);
extern void db_request(PGconn *conn);
#endif
~/Coding/Potager/libs/lib_SQL_functions/src/lib_sql_functions.c
#include <stdlib.h>
#include <libpq-fe.h>
#include "lib_sql_functions.h"
void db_exit(PGconn *conn) {
PQfinish(conn);
exit(1);
}
void db_request(PGconn *conn) {
PGresult *res = PQexec(conn, "SELECT name FROM plante ORDER BY name");
if (PQresultStatus(res) != PGRES_TUPLES_OK) {
printf("No data retrieved\n");
PQclear(res);
db_exit(conn);
}
// Print result line by line
int rows = PQntuples(res);
for (int i=0; i<rows; i++) {
printf("%s\n", PQgetvalue(res, i, 0));
}
// Clean
PQclear(res);
}
~/Coding/Potager/libs/project_Potager/CMakeLists.txt
add_executable(potager src/main.c)
target_compile_options(
potager PRIVATE
-Wall -Wextra -pedantic -fno-common -fno-builtin
)
target_compile_features(
potager PRIVATE
c_std_17
)
target_link_libraries(potager PRIVATE lib_SQL_Functions)
- target_link_libraries() lie la target executable potager à la librairie lib_SQL_Functions construite dans le cadre du projet. La portée ici est PRIVATE.
~/Coding/Potager/libs/project_Potager/src/main.c
#include <stdio.h>
#include <stdlib.h>
#include <libpq-fe.h>
#include "lib_sql_functions.h"
int main(void) {
// connect to pgsql
PGconn *conn = PQconnectdb("host=127.0.0.1 port=**** user=******** password=********* dbname=potager");
if (PQstatus(conn) == CONNECTION_BAD) {
fprintf(stderr, "Connection to database failed: %s\n",
PQerrorMessage(conn));
db_exit(conn);
}
// Request database
db_request(conn);
// disconnect to pgsql
PQfinish(conn);
return 0;
}
Construire :
cd ~/Coding/Potager/build
cmake -B . -S ..
Output :
-- The C compiler identification is Clang 13.0.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Found PostgreSQL: /usr/local/lib/libpq.so.6.14 (found version "15.4")**
-- Configuring done (0.8s)
-- Generating done (0.0s)
-- Build files have been written to: ~/Coding/Potager/build
CMake a bien trouvé la bibliothèque externe libpq.so.6.14
Compilation
make
Ou
cmake --build .
Output :
[ 25%] Building C object libs/lib_SQL_Functions/CMakeFiles/lib_SQL_Functions.dir/src/lib_sql_functions.c.o
[ 50%] Linking C static library liblib_SQL_Functions.a
[ 50%] Built target lib_SQL_Functions
[ 75%] Building C object libs/project_Potager/CMakeFiles/potager.dir/src/main.c.o
[100%] Linking C executable potager
[100%] Built target potager
Remarque :
L’éxécutable se situera ici dans ~/Coding/Potager/build/project_Potager/ lors de la liaison par le compilateur, ce dernier, ajoute le préfix lib, il est préférable ici de nommer ses librairies ainsi : _SQL_Functions pour obtenir une librairie lib_SQL_functions.a
Exemples de CMakeLists:#
SDL2#
CMakeLists.txt
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
project(
SDL2Test
VERSION 1.0
LANGUAGES C
)
# Include SDL2
find_package(SDL2 REQUIRED)
add_executable(sdl2test src/main.c)
target_compile_options(
sdl2test PRIVATE
-Wall -Wextra -pedantic -fno-common -fno-builtin
)
target_compile_features(
sdl2test PRIVATE
c_std_17
}
target_link_libraries(sdl2test PRIVATE SDL2::SDL2)
main.c
#include <SDL.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
int statut = EXIT_FAILURE;
SDL_Color orange = {255, 127, 40, 255};
/* Initialisation, cr\xc3\xa9ation de la fen\xc3\xaatre et du renderer. */
if(0 != SDL_Init(SDL_INIT_VIDEO)) {
fprintf(stderr, "Erreur SDL_Init : %s", SDL_GetError());
goto Quit;
}
window = SDL_CreateWindow("SDL2", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
640, 480, SDL_WINDOW_SHOWN);
if(NULL == window) {
fprintf(stderr, "Erreur SDL_CreateWindow : %s", SDL_GetError());
goto Quit;
}
if(NULL == renderer) {
fprintf(stderr, "Erreur SDL_CreateRenderer : %s", SDL_GetError());
goto Quit;
}
/* C'est à partir de maintenant que ça se passe. */
if(0 != SDL_SetRenderDrawColor(renderer, orange.r, orange.g, orange.b, orange.a)) {
fprintf(stderr, "Erreur SDL_SetRenderDrawColor : %s", SDL_GetError());
goto Quit;
}
if(0 != SDL_RenderClear(renderer)) {
fprintf(stderr, "Erreur SDL_SetRenderDrawColor : %s", SDL_GetError());
goto Quit;
}
SDL_Delay(500);
SDL_RenderPresent(renderer);
SDL_Delay(500);
statut = EXIT_SUCCESS;
Quit:
if(NULL != renderer)
SDL_DestroyRenderer(renderer);
if(NULL != window)
SDL_DestroyWindow(window);
SDL_Quit();
return statut;
}
GTK3#
CMakeLists.txt
cmake_minimum_required (VERSION 3.27)
project (
Hello-GTK
VERSION 1.0
DESCRIPTION "Hello en GTK"
LANGUAGES C
)
# Use the package PkgConfig to detect GTK+ headers/library files
Find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED gtkmm-3.0)
add_executable (hellogtk src/main.c)
target_include_directories(hellogtk PRIVATE ${GTK_INCLUDE_DIRS})
target_link_directories(hellogtk PRIVATE ${GTK_LIBRARY_DIRS})
target_compile_options (hellogtk PRIVATE ${GTK_CFLAGS_OTHER}) # Add other flags to compiler
target_link_libraries(hellogtk PRIVATE ${GTK_LIBRARIES})
main.c
#include <gtk/gtk.h>
static void activate(GtkApplication* app, gpointer user_data) {
GtkWidget *window;
window = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (window), "Window");
gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);
gtk_widget_show_all (window);
}
int main(int argc, char** argv) {
GtkApplication *app;
int status;
app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}