Le processus de compilation consiste à transformer un fichier source écrit dans un langage A en un fichier cible écrit dans un langage B.
Dans le cas présent, on transforme un fichier C ou C++ en un fichier objet (object file) d'extension .o
gcc -c source.c
ou
g++ -c source.cpp
Des options peuvent être ajoutées à cette commande de base :
Voir également man gcc pour plus d'options.
Le processus d'édition de liens consiste à transformer un ensemble de fichiers objets en un exécutable (binaire).
gcc -o monexe a.o b.o ...
ou
g++ -o monexe *.o
On peut également réaliser la compilation suivie de l'édition de liens si les fichiers sources sont les fichiers C ou C++ :
gcc -o monexe a.cpp b.cpp ...
ou
g++ -o monexe *.cpp
Le problème qui se pose consiste, si on modifie un fichier, à ne recompiler que ce qui est nécessaire.
Par exemple, si on modifie le fichier file2.c alors on ne doit recompiler file2.c, puis réaliser l'édition de liens.
Sur l'image suivante on voit un comparatif schématisé de l'automatisation de la compilation avec trois outils différents.
Extrait des transparents de Rodolfo Lima
Le processus de compilation et d'édition de liens peut être automatisé grâce à l'utilitaire make. Il suffit pour cela de créer un fichier nommé makefile ou Makefile dans le répertoire ou on désire compiler. Il est préférable d'organiser les répertoires ainsi:
Répertoire | Description |
src | l'ensemble de fichiers sources (.h .cpp) |
obj | l'ensemble des fichiers objets (.o) |
lib | l'ensemble des librairies utilisateur (.a ou .so) |
bin | l'ensemble des exécutables / binaires (.exe) |
data | l'ensemble des fichiers de données utilisés par les exécutables |
ou alors de créer un répertoire build qui regroupera bin, lib, obj.
Le makefile comporte des règles qui indiquent quelles actions déclencher pour obtenir une cible (fichier ou autre cible) en fonction d'une liste de de cibles existantes ou de fichiers sources :
cible : sources
<tabulation>actions
Dans la partie actions on mettra des commandes shells, on peut également laisser cette partie vide.
Voici des raccourcis de variables prédéfinies liées aux règles que l'on peut utiliser dans la partie actions :
Variable | Role |
$@ | cible |
$% | nom de la cible |
$< | première source |
$? | toutes les sources plus récentes que la cible |
$^ | toutes les sources |
Par exemple pour obtenir l'exécutable main.exe en fonction des fichiers objets a.o, b.o, c.o :
main.exe : a.o b.o c.o
g++ -O2 -o \$@ \$^
# or
main.exe : a.o b.o c.o
g++ -O2 -o main.exe a.o b.o c.o
On peut également définir des règles génériques comme :
obj/%.o : src/%.cpp
g++ -c -O2 -o \$@ \$<
qui se lit : pour obtenir tout fichier d'extension .o dans le répertoire obj, il faut qu'il existe un fichier d'extension .cpp de même nom dans le répertoire src, pour cela on compilera le fichier source en fichier objet (paramètre -c de g++).
Les règles génériques permettent de simplifier l'écriture du makefile. Sans elles on serait contraint d'écrire :
obj/a.o : src/a.cpp
g++ -c -O2 -o \$@ \$<
obj/b.o : src/b.cpp
g++ -c -O2 -o \$@ \$<
obj/c.o : src/c.cpp
g++ -c -O2 -o \$@ \$<
Attnetion : lorsque l'on tape make dans le terminal pour lancer l'exécution des règles, le programme make exécutera la première règle qu'il trouve dans le fichier makefile. Il est préférable de faire en sorte que la règle all soit la première rencontrée. Sinon il faudra explicitement saisir dans le terminal la commande make all
Voici un exemple plus complexe qui utilise le répertoire build pour stocker fichiers objets, librairies, exécutables et tests unitaires. On prend en compte :
Voici l'organisation des répertoires du projet :
.
├── include
│ └── v1_0
│ ├── base
│ │ ├── base.h
│ │ ├── command_line.h
│ │ ├── exception.h
│ │ ├── strings.h
│ │ ├── terminal.h
│ │ └── types.h
│ └── maths
│ └── statistics.h
├── makefile
├── makefile.distrib
├── makefile.libraries
├── makefile.modules
├── src
│ └── v1_0
│ ├── base
│ │ ├── base.h
│ │ ├── command_line.cpp
│ │ ├── command_line.h
│ │ ├── exception.cpp
│ │ ├── exception.h
│ │ ├── makefile.lib
│ │ ├── strings.cpp
│ │ ├── strings.h
│ │ ├── terminal.cpp
│ │ ├── terminal.h
│ │ └── types.h
│ ├── maths
│ │ ├── makefile.lib
│ │ ├── statistics.cpp
│ │ └── statistics.h
│ └── prog
│ ├── makefile.bin
│ ├── prog1.cpp
│ └── prog2.cpp
└── tst
└── v1_0
├── base
│ ├── makefile.tst
│ ├── test_strings.cpp
│ └── test_strings.h
└── maths
On utilise plusieurs makefiles :
Dans les sous-répertoires on trouve :
CMake (abréviation de cross platform make) est qualifié de moteur de production multiplate-forme contrôlé par des fichiers de configuration (appelés CMakeLists.txt) ce qui lui permet de produire des makefile sous Unix ou des fichiers de projet Visual Studio sous Window par exemple.
cmake possède son propre langage de programmation basé sur des variables prédéfinies, des conventions de nommage et des règles implicites.
Sous Ubuntu 14.04, installer le package cmake. On pourra également installer le package cmake-qt-gui qui est une interface graphique mais qui n'est pas très intuitive.
Dans la partie qui suit on s'intéresse à la génération d'un makefile.
On désire à partir d'un fichier situé dans le répertoire courant main.cpp générer l'exécutable main.exe.
La première étape consiste à écrire un fichier CMakeLists.txt qui décrit les étapes à réaliser pour aller des sources à l'exécutable.
Pour obtenir un makefile, exécuter la commande suivante :
\$ cmake -G "Unix Makefiles"
-- The C compiler identification is Clang 3.6.0
-- The CXX compiler identification is Clang 3.6.0
-- Check for working C compiler: /usr/bin/clang-3.6
-- Check for working C compiler: /usr/bin/clang-3.6 -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/clang++-3.6
-- Check for working CXX compiler: /usr/bin/clang++-3.6 -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: [...]/cmake1
Durant l'exécution de la commande, plusieurs fichiers sont générés :
Pour obtenir l'exécutable, il suffit de taper :
\$make
Scanning dependencies of target main.exe
[100%] Building CXX object CMakeFiles/main.exe.dir/main.cpp.o
Linking CXX executable main.exe
[100%] Built target main.exe
On rencontre cependant deux problèmes:
Pour répondre au premier problème, il suffit d'ajouter les lignes suivantes au fichier CMakeLists.txt :
set(
CMAKE_C_COMPILER
"/usr/bin/gcc"
)
set(
CMAKE_CXX_COMPILER
"/usr/bin/g++"
)
La commande set permet d'attribuer une valeur à une variable en l'occurrence les variables prédéfinies CMAKE_C_COMPILER et CMAKE_CXX_COMPILER.
Il faut relancer cmake, mais parfois cela n'est pas suffisant, il faut supprimer le fichier CMakeCache.txt et le sous-répertoire CMakeFiles (a priori il n'existe pas de moyen simple de supprimer ces fichiers si ce n'est un rm):
\$ rm -rf CMakeFiles CMakeCache.txt
\$ cmake -G "Unix Makefiles"
-- The C compiler identification is GNU 4.9.2
-- The CXX compiler identification is GNU 4.9.2
-- Check for working C compiler: /usr/bin/gcc
-- Check for working C compiler: /usr/bin/gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - 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
-- Configuring done
-- Generating done
-- Build files have been written to: [...]/cmake1_1
Pour répondre au second problème, il suffit de lancer make avec l'option VERBOSE=1 :
make VERBOSE=1
...
[100%] Building CXX object CMakeFiles/main.exe.dir/main.cpp.o
/usr/bin/g++ -o CMakeFiles/main.exe.dir/main.cpp.o -c /home/richer/dev/cpp/cmake1_1/main.cpp
Linking CXX executable main.exe
/usr/bin/cmake -E cmake_link_script CMakeFiles/main.exe.dir/link.txt --verbose=1
/usr/bin/g++ CMakeFiles/main.exe.dir/main.cpp.o -o main.exe -rdynamic
...
On s'aperçoit qu'aucune option de compilation (optimisation ou débugage) n'est utilisée.
Il faut choisir un "type de build" (build type en anglais) que l'on peut traduire en français par le mot distribution, on modifie CMakeLists.txt en conséquence :
set(
CMAKE_BUILD_TYPE
Release
)
On a le choix entre :
Par exemple pour définir sa propre distribution :
project(MyProject)
set(
CMAKE_BUILD_TYPE
ma_distribution
)
set(
CMAKE_CXX_FLAGS_MA_DISTRIBUTION
"-O3 -ftree-vectorize -msse2"
)
set(
CMAKE_C_FLAGS_MA_DISTRIBUTION
"-O3 -ftree-vectorize -msse2"
)
Attention les trois dernières instructions doivent être placées après la directive project, sinon elles ne seront pas prises en compte.
On peut également ajouter des flags à une distribution existante :
project(MyProject)
set(
CMAKE_BUILD_TYPE
release
)
set(
CMAKE_CXX_FLAGS_RELEASE
"\${CMAKE_CXX_FLAGS_RELEASE} -Wall -ftree-vectorize -msse2 -std=c++11"
)
On compilera alors avec -O3 -DNDEBUG et -Wall -ftree-vectorize -msse2 -std=c++11.
Généralement on sépare les fichiers sources des exécutables en plaçant les premiers dans un sous-répertoire src et les seconds dans un répertoire bin ou build.
Attention, l'exécutable main.exe sera situé dans le sous-répertoire bin/Release car on a utilisé la directive set(EXECUTABLE_OUTPUT_PATH bin/${CMAKE_BUILD_TYPE}).
On dispose de l'arborescence suivante pour les sources :
src
├── file1.cpp
├── file1.h
├── main.cpp
└── maths
├── statistics.cpp
└── statistics.h
On veut faire en sorte de créer une librairie avec les fichiers du sous-répertoire maths puis obtenir l'exécutable à partir des fichiers file1.cpp et main.cpp en réalisant l'édition de liens avec la librairie produite.
On ajoute les 4 lignes suivantes :
Au final on obtient une lirairie dans le répertoire principal du projet (libmaths.so) qui est liée à l'exécutable main.exe.
On désire réaliser l'édition de lien avec la librairie boost (system, filesystem). Pour cela il suffit d'utiliser find_package et d'inclure les librairies de target_link_libraries.
add_executable(
main.exe
\${source_files}
)
find_package(
Boost
COMPONENTS system filesystem
REQUIRED)
target_link_libraries(
main.exe
maths
\${Boost_FILESYSTEM_LIBRARY_RELEASE}
\${Boost_SYSTEM_LIBRARY_RELEASE}
)
On utilise des tests unitaires situés dans le répertoire tst du projet et on aimerait les générer. On utilise l'instruction foreach pour itérer sur les fichiers et générer des exécutables en incluant la librairie cppunit
Il faudra ajouter dans le répertoire cmake/modules -- car on a utilisé set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") -- le fichier FindCppUnit.cmake qui permet de détecter la librairie CppUnit et de fixer des variables en conséquence.
Au final si on compare cmake à make, on peut considérer que cmake est assez difficile d'utilisation pour au moins deux raisons : d'une part l'existence de variables prédéfinies qu'il faut connaître, d'autre part des commandes pas toujours évidentes pour réaliser ce que l'on souhaite faire.
En outre la portabilité des plateformes (Linux, Windows, OSX, ...) doit être gérée par l'utilisateur.
A titre d'exemple, voici le fichier CMakeLists.txt de 1450 lignes de la SDL (Simple DirectMedia Layer a cross-platform development library designed to provide low level access to audio, keyboard, mouse, joystick, and graphics hardware via OpenGL and Direct3D).
A suite of programming tools designed to assist in making source code packages portable to many Unix-like systems.
SCons is a software construction tool—that is, a superior alternative to the classic "Make" build tool that we all know and love.
SCons is implemented as a Python script and set of modules, and SCons "configuration files" are actually executed as Python scripts. This gives SCons many powerful capabilities not found in other software build tools.
L'équivalent de make pour Java qui utilise une syntaxe XML.