Toolchain automatisée avec CMake
Le problème à résoudre
Compiler un projet, c’est jamais facile, à chaque fois il manque un logiciel, une librairie, une dépendance et ça marche pas ! C’est encore pire sur de l’embarqué parce qu’il faut un compilateur particulier, et puis ensuite il faut pouvoir flash l’appareil, ah et puis t’as pas les drivers et AHHHHH ! Ça ne fini jamais ! Les instructions JUSTE pour installer font 5 lignes !
Voilà à quoi ça ressemble la compilation chez Sharp’Attack :
cmake -B build -S . -GNinja # On configure le projet
cmake --build build # On build
ET C’EST TOUT !
L’ancienne solution
Les années précédentes, nous utilisions Conan pour sa fonctionnalité de création d’environnement virtuel. L’idée d’un environnement virtuel c’est de créer un dossier dans lequel on installe tous ses logiciels et ses dépendances. On viens ensuite modifier les variables d’environnement du shell en cours d’utilisation pour rechercher en priorité (voir exclusivement) dans ce dossier et le tour est joué !
Nous avons donc crée un répertoire git pour y mettre nos recettes Conan et utilisé la fonctionnalité de repository de GitLab pour y pousser nos recettes compilées.
Les instructions de compilation du projet ressemblaient alors à ceci :
# Installation et configuration de conan
python3 -m pip install conan
conan remote add gitlab https://gitlab.com/api/v4/packages/conan
# Création et activation de la toolchain
mkdir toolchain && cd toolchain
conan install .. --build=missing
source ./activate_run.sh # L'étape la plus importante, c'est là où on active l'environnement virtuel
cd ..
# Compliation
cmake -B build -S . -GNinja # On configure le projet
cmake --build build # On build
Ouf ! Il y en a des étapes ! Plus il y a d’étapes, plus c’est facile de faire des bêtises !
Enfin bon, ça fonctionnait « bien » la plupart du temps et on a fait avec.
Le début des ennuis
Il y a quelques mois, Conan 2.0 a été publié, ça a l’air sympa et tout mais il y a trois points assez problématiques :
- Tout est à refaire, les recettes ne sont pas compatibles
- La fonctionnalité d’environnement virtuel ne permet plus le même fonctionnement, il faut maintenant toujours utiliser un fichier de toolchain CMake
- À l’heure où j’écris ces lignes, le repository de GitLab n’est pas compatible avec Conan 2.0
En plus de toujours devoir spécifier une version spécifique à chaque installation de Conan, tout est à refaire si nous restons sur la même technologie. Il était l’heure de se renseigner sur les alternatives et les autres manières de faire.
Les critères à respecter
Avant de comparer les alternatives, il est intéressant de d’abord poser à plat nos exigences. La solution que nous recherchons doit répondre aux critères suivants :
- Être facile d’utilisation
- Être bien documentée
- Permettre la création de recettes personnalisées si les paquets que je recherche n’existent pas
- Doit me permettre d’au moins installer
- Le compilateur
gcc-arm-none-eabi
- L’outil de build
ninja
- Le logiciel pour contrôler le debugger
openocd
- Le compilateur
Une nouvelle solution
Après avoir considéré plusieurs solutions dont Docker et Nix, nous avons opté pour vcpkg et détourné un peu son usage. Son fonctionnement est assez similaire à Conan 2.0 dans le sens où il est directement chargé par CMake durant la phase de compilation mais il a l’avantage de seulement nécessiter un répertoire git pour ses recettes. Il est également très simple d’installation puisqu’il peut être lui même installé dans un dossier du projet sans dépendance externe telle que Python.
Voici les étapes classique d’utilisation d’après la documentation officielle :
# Installation du logiciel
git clone https://github.com/Microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
# Compilation du projet
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build
Ça ressemble déjà pas mal à ce qu’on veut.
Spécifier les dépendances
Les dépendances sont contrôlées par le fichier vcpkg.json
, commençons par un exemple minimal en incluant ninja.
Les prérequis sont les suivant:
- Ninja n’est pas installé (sinon on peut pas vraiment tester)
- vcpkg a été téléchargé dans un dossier à la racine du projet
- On a un projet qui compile déjà et qui est basé sur CMake
Ajoutons ceci dans un fichier vcpkg.json
{
"dependencies": [
"vcpkg-tool-ninja",
"vcpkg-cmake"
]
}
Compilons:
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake -GNinja
cmake --build build
Et HOP ! Ça télécharge tout tout seul ! Incroyable ! Et même ninja fonctionne !
Ajouter de nouvelles recettes
Ninja c’est bien, mais comme énoncé précédemment, on a besoin du compilateur, c’est donc l’heure de l’ajouter. Petit problème, pas de compilateur disponible à l’horizon sur leur site. Il faut dire que c’est pas vraiment fait pour faire ce que nous en faisons à la base… Tant pis, rien ne peux nous arrêter !
On commence évidemment par lire la doc, elle n’est pas toujours très clair mais s’avère très utile.
Les recettes ou plutôt ports sont écrit en CMake. Il existe tout un tas de macro fournis pour aider à télécharger, détecter le système et ajouter dans le PATH.
Écrire la recette
Le but de la recette est de télécharger la toolchain GNU ARM, vous pouvez retrouver le port complet sur notre GitLab mais globalement, il se compose de deux fichiers principaux :
# portfile.cmake
set(VCPKG_POLICY_CMAKE_HELPER_PORT enabled)
set(VERSION "10.3-2021.10")
set(USER_AGENT "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0")
set(FOLDER_NAME "gcc-arm-none-eabi-${VERSION}")
set(ARCHIVE_NAME "")
set(DOWNLOAD_URL "")
set(HASH "")
# D'abord, on détecte le système sur lequel on se trouve pour télécharger le bon fichier depuis le site du constructeur
if(VCPKG_CMAKE_SYSTEM_NAME STREQUAL "Linux")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(ARCHIVE_NAME "gcc-arm-none-eabi-${VERSION}-x86_64-linux.tar.bz2")
set(DOWNLOAD_URL "https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2?rev=78196d3461ba4c9089a67b5f33edf82a&hash=D484B37FF37D6FC3597EBE2877FB666A41D5253B")
set(HASH "2383e4eb4ea23f248d33adc70dc3227e")
elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "arm|aarch64")
set(ARCHIVE_NAME "gcc-arm-none-eabi-${VERSION}-aarch64-linux.tar.bz2")
set(DOWNLOAD_URL "https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-aarch64-linux.tar.bz2?rev=b748c39178c043b4915b04645d7774d8&hash=572217C8AFE83F1010753EA3E3A7EC2307DADD58")
set(HASH "3fe3d8bb693bd0a6e4615b6569443d0d")
endif()
elseif(VCPKG_CMAKE_SYSTEM_NAME STREQUAL "Darwin")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(ARCHIVE_NAME "gcc-arm-none-eabi-${VERSION}-mac.tar.bz2")
set(DOWNLOAD_URL "https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-mac.tar.bz2?rev=58ed196feb7b4ada8288ea521fa87ad5&hash=62C9BE56E5F15D7C2D98F48BFCF2E839D7933597")
set(HASH "7f2a7b7b23797302a9d6182c6e482449")
endif()
elseif(VCPKG_CMAKE_SYSTEM_NAME STREQUAL "WindowsStore")
set(ARCHIVE_NAME "gcc-arm-none-eabi-${version}-win32.exe")
set(DOWNLOAD_URL "https://developer.arm.com/-/media/Files/downloads/gnu-rm/10.3-2021.10/gcc-arm-none-eabi-10.3-2021.10-win32.exe?rev=29bb46cfa0434fbda93abb33c1d480e6&hash=3C58D05EA5D32EF127B9E4D13B3244D26188713C")
set(HASH "8d0f75f33f9e3d5f9600197626297212")
endif()
if(ARCHIVE_NAME STREQUAL "")
vcpkg_fail_port_install(MESSAGE "Your system ${VCPKG_CMAKE_SYSTEM_NAME}-${VCPKG_TARGET_ARCHITECTURE} is not supported")
endif()
# On télécharge
file(DOWNLOAD ${DOWNLOAD_URL}
${ARCHIVE_NAME}
EXPECTED_HASH MD5=${HASH}
HTTPHEADER "User-Agent: ${USER_AGENT}"
)
# On extrait dans un dossier
# On retrouve généralement le dossier tools et un dossier share
# Le dossier tools sert à mettre les binaires, le share à mettre les méta-données du port
file(ARCHIVE_EXTRACT INPUT ${ARCHIVE_NAME} DESTINATION "${CURRENT_PACKAGES_DIR}/tools/")
# Il faut toujours un fichier de copyright, sinon vcpkg considère le port invalide
file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/copyright" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
# Vcpkg fait des vérifications après l'installation et considère que des dossiers vides sont un signe d'échec de copie
# On supprime ces dossiers
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/tools/gcc-arm-none-eabi-${VERSION}/arm-none-eabi/include/bits" "${CURRENT_PACKAGES_DIR}/tools/gcc-arm-none-eabi-${VERSION}/arm-none-eabi/include/rpc")
# On copie les fichiers de configuration
configure_file("${CMAKE_CURRENT_LIST_DIR}/gcc-arm-none-eabi-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/gcc-arm-none-eabi-config.cmake" @ONLY)
configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-port-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake" @ONLY)
# gcc-arm-none-eabi-config.cmake
# Ce fichier rend accessible le port via la macro find_package de CMake
set(ARM_GCC_PATH "${CMAKE_CURRENT_LIST_DIR}/../../tools/gcc-arm-none-eabi-@VERSION@/bin")
N’oubliez pas les fichiers de copyright, le vcpkg.json
et le vcpkg-port-config.cmake
, ils sont tous accessibles sur le répertoire git fourni.
Une fois le dossier du port terminé, il est l’heure de tester. Il suffit d’ajouter le port dans le fichier de dépendance du projet à compiler et à lancer une installation des dépendances vcpkg en désignant où trouver les ports manquants :
./vcpkg/vcpkg install --overlay-ports=le/dossier/vers/mon/port/
Si il y a des erreurs, on analyse et on recommence, le tout jusqu’à ce que tout fonctionne.
Créer un registre
Bon, le port fonctionne, il s’installe bien mais maintenant il faut créer un registre où vcpkg va aller le chercher. Le mieux est vraiment de lire la doc à ce propos. Hop hop hop ! On part la lire, sinon vous n’allez rien comprendre à ce qu’on raconte.
Ce qu’il faut retenir, c’est qu’il est possible de créer un registre en mode filesystem (que nous conseillons d’utiliser pour commencer) et en mode git qui est ce qui est le mieux à la fin. Si vous lisez notre GitLab, vous aurez un exemple de comment fabriquer un registre en mode git, lisez attentivement la doc pour bien comprendre sa structure
Une fois le répertoire créé, il est temps de le lier à notre projet à compiler. On retourne sur celui-ci et on y crée un fichier vcpkg-configuration.json
. Celui-ci va contrôler les sources d’installation de nos paquets. On peut y définir une source par défaut et une source secondaire avec une liste des ports qu’on va trouver dessus. On indique une baseline qui correspond au hash du commit que vcpkg va utiliser ainsi que toutes les autres caractéristiques comme indiqué dans la documentation
Le vcpkg-configuration.json
:
{
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json",
"default-registry": {
"kind": "git",
"repository": "https://github.com/Microsoft/vcpkg",
"baseline": "7e7c62d863b1bf599c1d104b76cd8b74475844d4"
},
"registries": [
{
"kind": "git",
"repository": "https://gitlab.com/sharpattack/vcpkg_ports.git",
"baseline": "285d9f5d9e16c879288f9b1e08f67abec29b98dc",
"packages": [
"gcc-arm-none-eabi"
]
}
]
}
N’oublions pas le contenu de notre vcpkg.json
:
{
"dependencies": [
"vcpkg-tool-ninja",
"vcpkg-cmake",
"gcc-arm-none-eabi"
]
}
Et on recommence la configuration du projet :
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake -GNinja
cmake --build build
Et si tout a bien été fait, ça devrais bien tout télécharger comme il faut !
Utiliser le package dans le CMake
Alors super, c’est installé, fantastique… Mais ça ne s’en sert pas pour l’instant ! Pour ça il va falloir ajouter quelques lignes au CMakeLists.txt du projet.
# Blah blah blah des trucs du CMake
# On récupère le port, ça va charger le contenu du fichier -config.cmake du port et donc la variable ARM_GCC_PATH
find_package(gcc-arm-none-eabi REQUIRED)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_VERSION 1)
# On spécifie le chemin du compilateur
set(CMAKE_C_COMPILER ${ARM_GCC_PATH}/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER ${ARM_GCC_PATH}/arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER ${ARM_GCC_PATH}/arm-none-eabi-gcc)
set(CMAKE_AR ${ARM_GCC_PATH}/arm-none-eabi-ar)
set(CMAKE_OBJCOPY ${ARM_GCC_PATH}/arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP ${ARM_GCC_PATH}/arm-none-eabi-objdump)
set(SIZE ${ARM_GCC_PATH}/arm-none-eabi-size)
project(MonProjet C CXX ASM) # La définition du projet et son nom, on doit modifier le compilateur avant
# Blah blah blah d'autres trucs
Et là on compile… EEEEEEEEEEETTTTTTTTTTTTTTTTT c’est ÉCLATÉ ! Il y a des erreurs partout ! Pourquoi CMake il dit qu’il arrive pas à compiler ?! Pourquoi il me dit que j’ai changé des trucs que j’avais pas le droit ?!
Il se trouve que changer le compilateur, c’est pas anodin, on ne peut modifier ces variables qu’une seule fois une fois le projet défini (la ligne project).
Il se trouve que puisque nous utilisons -DCMAKE_TOOLCHAIN_FILE
, tout à la fin du fichier cmake, la toolchain est chargée et elle se met à changer toutes les variables de compilateur ! Et là ça EXPLOSE !
La solution ? C’est de charger la toolchain avant ! Mais peut-on faire ça ? Évidemment !
Fabriquons un fichier à inclure, appelons le vcpkg.cmake
:
# VCPKG des fois il n'est pas content parce qu'il n'arrive pas à détecter le « triplet » de l'os, alors on va l'aider
if(NOT UNIX)
message(FATAL_ERROR "This can only build on unix based systems")
endif()
if(FreeBSD)
message(FATAL_ERROR "We can not compile on FreeBSD")
endif()
set(TRIPLET_SYSTEM "linux")
if(APPLE)
set(TRIPLET_SYSTEM "osx")
endif()
EXECUTE_PROCESS( COMMAND uname -m COMMAND tr -d '\n' OUTPUT_VARIABLE ARCHITECTURE )
if(ARCHITECTURE STREQUAL "x86_64")
set(TRIPLET_ARCH "x64")
elseif(ARCHITECTURE MATCHES "arm|aarch64")
set(TRIPLET_ARCH "arm")
else()
message(FATAL_ERROR "Architecture ${ARCHITECTURE} is not supported")
endif()
# Là, on a trouvé le triplet, on va le configurer
set(VCPKG_TARGET_TRIPLET "${TRIPLET_ARCH}-${TRIPLET_SYSTEM}")
# Et enfin, on charge de force la toolchain et ça arrive précisément au moment où on le souhaite !
include(${CMAKE_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake)
Et enfin, il ne reste plus qu’à inclure ce fichier au début de notre CMakeLists.txt comme ceci :
# Blah blah blah des trucs du CMake
# La nouvelle ligne très importante !
include(${CMAKE_SOURCE_DIR}/vcpkg.cmake)
# On récupère le port, ça va charger le contenu du fichier -config.cmake du port et donc la variable ARM_GCC_PATH
find_package(gcc-arm-none-eabi REQUIRED)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_VERSION 1)
# On spécifie le chemin du compilateur
set(CMAKE_C_COMPILER ${ARM_GCC_PATH}/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER ${ARM_GCC_PATH}/arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER ${ARM_GCC_PATH}/arm-none-eabi-gcc)
set(CMAKE_AR ${ARM_GCC_PATH}/arm-none-eabi-ar)
set(CMAKE_OBJCOPY ${ARM_GCC_PATH}/arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP ${ARM_GCC_PATH}/arm-none-eabi-objdump)
set(SIZE ${ARM_GCC_PATH}/arm-none-eabi-size)
project(MonProjet C CXX ASM) # La définition du projet et son nom, on doit modifier le compilateur avant
# Blah blah blah d'autres trucs
Et il ne reste plus qu’à reconfigurer notre projet CMake, cette fois-ci, sans même avoir besoin de préciser la toolchain ! On y est presque !!!
cmake -B build -S . -GNinja
cmake --build build
Automatiser l’installation de vcpkg
Vous ne trouvez pas qu’il reste une étape en trop ? Mais si, rappelez vous nos instructions de compilation… Jamais nous n’avons parlé d’installer vcpkg !
En effet, nous avons également automatisé l’installation même de vcpkg en installant tout dans le dossier de build à la configuration du projet.
C’est la partie la plus simple et ce grâce à une super script disponible sur github.
Il suffit de le télécharger souns le nom automate-vcpkg.cmake
et d’aller modifier notre petit script vcpkg.cmake
pour appeler les macros qui s’y trouvent.
# VCPKG des fois il n'est pas content parce qu'il n'arrive pas à détecter le « triplet » de l'os, alors on va l'aider
if(NOT UNIX)
message(FATAL_ERROR "This can only build on unix based systems")
endif()
if(FreeBSD)
message(FATAL_ERROR "We can not compile on FreeBSD")
endif()
set(TRIPLET_SYSTEM "linux")
if(APPLE)
set(TRIPLET_SYSTEM "osx")
endif()
EXECUTE_PROCESS( COMMAND uname -m COMMAND tr -d '\n' OUTPUT_VARIABLE ARCHITECTURE )
if(ARCHITECTURE STREQUAL "x86_64")
set(TRIPLET_ARCH "x64")
elseif(ARCHITECTURE MATCHES "arm|aarch64")
set(TRIPLET_ARCH "arm")
else()
message(FATAL_ERROR "Architecture ${ARCHITECTURE} is not supported")
endif()
# Là, on a trouvé le triplet, on va le configurer
set(VCPKG_TARGET_TRIPLET "${TRIPLET_ARCH}-${TRIPLET_SYSTEM}")
# On charge le script d'installation de vcpkg
include(${CMAKE_SOURCE_DIR}/automate-vcpkg.cmake)
# On l'installe
vcpkg_bootstrap()
Et on répète tous en cœur la commande magique de compilation :
cmake -B build -S . -GNinja
cmake --build build
Et tout compile !
Conclusions
Cette nouvelle façon de gérer nos dépendances nous a ouvert de nouvelles possibilité et simplifié énormément l’étape de compilation et d’installation des dépendances.
Il se trouve même que si il vous manque des paquets systèmes, vcpkg sera assez gentil pour vous indiquer ce qu’il faut installer et même avec quelle commande le faire.
Ça reste quand même une utilisation assez détournée de l’outil qui est plutôt à la base fait pour faciliter l’installation de librairies logicielles, pas de programmes !
N’hésitez pas à explorer notre travail qui est bien plus exhaustif que ce petit article de blog.
Bon courage !