CMAKe is a piece of cake!#
CMake is a cross-platform software builder whose build process steps are entirely managed by configuration files; the CMakeLists.txt files.
CMake supports and builds applications written for C, C++, C# (CSharp), CUDA, Objective-C, Objective-C++, Fortran, HIP, ISPC, Swift, ASM, ASM_NASM, ASM_MARMASM, ASM_MASM, and ASM-ATT.
Principles of the CMake builder:
- target concept
- Apply scope to _target_s
- Locate libraries on the system, and import their objects to apply to these _target_s, or build them if they don’t exist
Objectives#
A brief introduction to using CMake to compile an application. The use of CMake for PostgreSQL RDBMS is presented, along with examples for SDL2 and GTK3. The special case of an “IMPORTED” library, using Ncurses as an example.
Physical & logical organization#
During a development project, it becomes necessary to break down a large, voluminous project into functions and sub-modules. CMake uses a logical approach, based on a physical structure.
The physical approach used is that of The Pitchfork Layout
Goal: Organize the various application components according to a standardized hierarchical directory structure:
- Files :
- Sources
- En-têtes (“Public” & “Private”)
- main() & functions()
- Documentation
- Tests
- External libraries integrated into the project structure
- Complements: “language bindings”, “optional plugins”, “platform bindings”.
- (…)
Project Structure#
For our project and target example, we’ll include only the necessary structural elements.
- It’s up to the developer to choose whether or not to merge header files.
- Source files can also be placed in a src directory at the root, or in a libs directory at the root, depending on the project’s choice of whether or not to create sub-modules.
- The two directories src and libs should not, from a philosophical point of view, be present simultaneously at the root of the project.
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#
Let’s take the simple example of compiling the well-known “Hello World” program.
Let’s prepare the physical structure: a single source file, two root directories.
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("Hello world !\n");
return 0;
}
A 3-line text file is the bare minimum required to build a simple CMake project.
Basis/CMakeLists.txt
cmake_minimum_required(VERSION 3.27)
project(CMake_Basis)
add_executable(MyExec libs/src/hello.c)
Explanations
- cmake_minimal_required() defines the minimum functions of the CMake version to be used
- project() defines the project name
- add_executable() defines the target, here the MyExec executable for compiling the hello.c source file
Generate compilation configuration files
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
Generate executable
cmake --build .
Output
[ 50%] Building C object CMakeFiles/hello.dir/libs/src/hello.c.o
[100%] Linking C executable MyExec
[100%] Built target MyExec
Axioms and best practices#
- Historical functionalities remain for compatibility reasons, to the detriment of good practices; the massive presence of sets in forums or web pages is no guarantee of documentation adapted to modern CMake
- Cmake uses target, concrete targets (library, executable) or abstract targets (OBJECTS & INTERFACE)
- Objects properties are applied to target
CMake is a construction tool that automates cross-platform and cross-system compilation. The same CMakeLists.txt must therefore be usable regardless of compiler, operating system or hardware.
CMake’s raison d’être is to provide configuration information to the compiler and linker, applied to target. The notion of scope can be transitive, depending on the keywords: INTERFACE, PRIVATE or PUBLIC.
Compilation instructions#
The aim is to obtain the equivalent of the following operating parameters:
clang -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin
It will be necessary to pass these instructions for use via target_compilation_options() and 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
Linking instructions#
Example for linking the ncurse library to compilation
clang -lform -lncurses
CMake features the Find_package() function, which allows you to find the distribution’s binary package on the system. The case of Curses / Ncurses is interesting: there is no IMPORTED target for Curses.
It will be necessary to specify the compilation options, the physical location of the include directory and that of the library by defining an 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)
Comments:
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)
Is equivalent to writing a modern CMake library with 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): asks CMake to find the installed binary package on the system, including the libraries.
- target_link_libraries(main PUBLIC PostgreSQL::PostgreSSQL): uses the IMPORTED target elements found by find_package().
- cf. doc CMake
PRIVATE INTERFACE OR PUBLIC
- INTERFACE: tells the compiler that the target (MyCurses:curses library in this case) is used as an INTERFACE and whose scope is set solely on this interface.
- PRIVATE : indicates to the compiler that the scope of the arguments applies only to the target, such as MyExec here
- PUBLIC: tells the compiler that arguments or objects can be propagated to the invocator
Into practice#
Let’s use the libpq library#
An application that requests a PostgreSQL database. The application will use the libpq provided by PostreSQL packages, the application must return the list of vegetables available within a “potager” database.
Prerequisites:
- The libpq-fe.h API available via the PostgreSQL package.
- A PostgreSQL database: potager here
- cmake installed on the system
Physical and logical structure:
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:
The CMakeLists.txt at the root of the project is the root of the project archive. It indicates the physical structure via add_subdirectory(libs), which calls the libs subdirectory.
~/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() provides the description and the programming language used, the C language
- add_subdirectory(libs) calls up the following CMakeLists.txt in the libs subdirectory
~/Coding/Potager/libs/CMakeLists.txt
add_subdirectory(lib_SQL_Functions)
add_subdirectory(project_Potager)
- Calling up the following CMakeLists.txt in the lib_SQL_Functions subdirectories
~/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 tells CMake to build the lib_SQL_Functions library via the lib_sql_functions.h header and the lib_sql_functions.c source.
- target_include_directory specifies the include directories to be used when compiling a given target. The named target must have been created by a command such as add_executable() or add_library() and must not be an ALIAS target.
- find_package() provides the IMPORTED target (objects) found
- target_link_libraries links these IMPORTED target to the previously constructed target lib_SQL_functions
- PUBLIC scope is defined here, the library must be propagated to the application’s main executable ~/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() links the target executable potager to the lib_SQL_Functions library built as part of the project. The scope here is 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;
}
Building :
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 has found the external library libpq.so.6.14.
Compiling
make
Or
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
Comments:
The executable will be located in ~/Coding/Potager/build/project_Potager/. When the compiler links the libraries, it adds the lib prefix: _SQL_Functions to obtain a lib_SQL_functions.a library.
Examples of 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;
}