Je me suis dit qu'essayer de porter une application pour l'architecture AArch64 (Armv8/Arm 64 bits) pourrait être intéressant, voilà ce que j'en ai appris.
Avant-propos : Je suis actuellement employé par Arm mais ces recherches et expérimentations ont été réalisées sur mon temps libre, sans incitation ni lien avec mon employeur.
Il faut parfois trouver de quoi s'occuper, et ce week-end s'annonçait ainsi. Cependant en explorant la liste des exécutables et paquets disponibles d'un logiciel, j'ai remarqué un absent : l'architecture Arm. Ne programmant pas beaucoup ces derniers temps et n'ayant jamais vraiment de fait de cross-compilation (la compilation pour un système ayant une architecture différente), je me suis dit que c'était l'occasion d'expérimenter !
La cible
Présentation
Le logiciel en question est Cockatrice. Il permet de jouer des parties de Magic The Gathering® de façon très flexible, en ligne comme en local, gérer des decks et probablement moult autres choses. C'est un projet Open Source à l'aspect certes un peu sommaire mais qui reste un excellent moyen de jouer au jeu sur l'ordinateur.
Je ne l'ai pas beaucoup utilisé moi-même mais mon groupe d'ami l'utilise extensivement depuis les confinements.
Pourquoi Cockatrice ?
Un des premiers point décisif est que c'est un projet Open Source : il aurait été très compliqué de porter un logiciel propriétaire, en tous cas de façon propre et fiable.
Le deuxième point décisif est la simplicité apparente du projet : seulement deux dépendances, elles-aussi Open Source, un langage et un système de compilation que je connais bien : C++ et CMake.
Pour d'autres points qui ont validé le choix est qu'il était déjà multiplateforme (Windows,Mac,Linux;32,64 bits)
et que mes amis l'utilisant j'avais accès à des cobayes testeurs.
Après un git clone
et une compilation de test sur ma machine x86_64, il était temps de s'y mettre.
Cross-compilation
Mise en place
Tout d'abord il me fallait la suite de compilation complète pour AArch64. Après une recherche rapide
parmi les paquets Arch Linux, j'ai simplement installé les paquets aarch64-linux-gnu-{gcc,binutils}
.
Un rapide test a confirmé que je pouvais compiler du C++ pour AArch64 :
a.out: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0
Les deux dépendances principales de Cockatrice sont Protocol Buffers (protobuf) et Qt5. Pour être sûr qu'elles pourraient être compilées, j'ai vérifié qu'il existait déjà des paquets Arm C'était le cas, je les ai donc téléchargé pour les cross-compiler elles-aussi, en ayant besoin et ne trouvant pas d'archive toute faite.
Protocol Buffers
En suivant les instructions de compilation pour protobuf il s'est avéré que le projet ne dépendait pas d'autres bibliothèques et avait un système de compilation relativement classique (proposant même un CMake), j'ai donc décidé de commencer par là.
CMake
Étant plus familier avec CMake j'ai décidé de regarder de ce côté, malgré le README de cette partie notant explicitement qu'il était présent pour Windows...
This directory contains CMake files that can be used to build protobuf with MSVC on Windows. You can build the project from Command Prompt and using an Visual Studio IDE.
Mon intuition était que j'aurais principalement à changer les variables indiquant à CMake quel compilateur
utiliser pour qu'il choisisse celui avec le préfixe aarch64-linux-gnu-
plutôt que le natif. La
documentation CMake
a confirmé ce sentiment en détaillant d'autres opérations nécessaires pour que la compilation se passe
bien, comme ne sélectionner que les bibliothèques correspondant à la cible. La documentation présente
un exemple de fichier de toolchain, mais plutôt limité. Au cas où, j'ai utilisé le fichier proposé par
ce blog
en remplaçant les ${BAREMETAL_ARM_TOOLCHAIN_PATH}arm-none-eabi-gcc${CMAKE_EXECUTABLE_SUFFIX}
par simplement aarch64-linux-gnu-gcc
(pour l'exemple de GCC) et en commentant toutes les options de
compilation, en les gardant au cas où j'en aurais besoin. (Le blog explique de façon claire les raisons
des différentes options et l'alternative au fichier de toolchain)
À ma grande surprise, CMake était bien content de préparer la compilation avec mon cross-compilateur !
Mais le compilateur, lui, n'était pas de cet avis. Comme dit précédemment, le CMake de Protocol Buffer est principalement destiné à compiler sous Windows et manquait probablement des configurations nécessaires à Linux : la compilation ne fonctionnait pas. Après quelques recherches infructueuses, j'ai enfin suivi la méthode décrite dans le README avec autotools.
Autotools
Contrairement à CMake, je n'ai jamais écrit de écrit de projet avec autotools ni configuré au delà de ce qui est décrit dans des instructions d'installation, je n'avais donc pas de point de départ.
Après un peu de recherche un commentaire sur Stack Overflow m'a pointé vers cette réponse
détaillant les différentes options et leur sens. Cependant le point de vue de la réponse est un peu perturbant,
et j'ai donc pensé qu'il me fallait utiliser l'option --target
: ma machine devant être l'hôte et
l'architecture Arm la cible. Cependant, dans les logs du script de configuration j'ai noté qu'il allait
utiliser le compilateur natif. Une autre réponse plus bas m'a
pointé dans la bonne direction pour comprendre et le faire fonctionner.
En effet, il semblerait qu'autotools se place du point de vue d'un compilateur. L'option --build
permettant de spécifier la machine sur laquelle la compilation du projet se fait, --host
la machine
sur laquelle le compilateur produit sur la machine --build
sera utilisé et --target
la machine
sur laquelle les produits du compilateur seront exécutés.
Dans un cas général, --target
n'a pas l'air d'être très utile.
Quelques exemples pour la clarté :
build=host=target=x86
: sur une machine x86, compiler une toolchain qui va produire de l'x86 depuis une machine x86 elle aussi;build=host=x86, target=arm
: sur une machine x86, compiler une toolchain qui va produire de l'Arm depuis une machine x86. C'est comme ça que l'on produit le cross-compilateuraarch64-linux-gnu-gcc
par exemple;build=x86, host=target=arm
: sur une machine x86, compiler une toolchain qui va produire de l'Arm depuis une machine Arm. Par exemple, clang pour AArch64 compilé depuis une machine x86.
Ici nous sommes dans le troisième cas et non pas le deuxième vu que l'on veut que Protocol Buffers
tourne sous aarch64. J'ai donc spécifié --host=aarch64-linux-gnu
ainsi qu'un préfixe d'installation
avec --prefix
afin qu'il ne soit pas installé ailleurs dans mon système, remplaçant potentiellement
la version déjà installée.
Un make
et quelques minutes plus tard... Succès !
Une dépendance de compilée avec succès, un bon présage pour la cross-compilation et la moitié du travail de fait, passons donc à Qt !
Qt
La documentation de Qt était plus... Tortueuse à naviguer. Dans un premier temps la page sur la compilation pour Linux a été utile, cependant celle du wiki sur compiler depuis le dépôt Git s'est avéré la plus pertinente, listant les dépendances et options utiles.
Il semblerait que Qt utilise aussi autotools mais, en regardant les options disponibles, un peu modifié.
Typiquement pour la cross-compilation, les options précédentes --host
et compagnie n'étaient pas
disponibles En cherchant sur Stack Overflow et la documentation Qt,
j'ai trouvé que l'option -xplatform
(le x
étant probablement pour "cross") était celle dont j'avais
besoin. Mais, en mettant le préfixe de mon compilateur...
ERROR: Invalid target platform 'aarch64-linux-gnu'.
Donc en plus d'avoir des options différentes, elles ont un sens différent ! En creusant un peu plus,
j'ai trouvé que Qt a une liste précise des plateformes supportées et des toolchains utilisées et demande
exactement ces plateformes. Elles se trouvent sous qtbase/mkspecs
et consistent principalement de deux
fichiers : un pour définir les outils utilisés (qmake.conf
), un autre pour des
définitions/configurations (qplatformdefs.h
).
Je pense que pour des cas pas trop éloignés il serait possible de copier une configuration existante et l'adapter, cependant je savais que Qt supportait aarch64 donc j'ai préféré chercher de ce côté.
Initialement j'avais récupéré la branche 5.5, le minimum demandé par Cockatrice, et n'avais aucune plateforme aarch64 disponible Ayant réussi à compiler avec Qt 5.15 disponible sur ma machine, j'ai avancé jusqu'à la dernière LTS Open Source de Qt, la 5.12, et y ai trouvé une plateforme adéquate.
Avec -xplatform linux-aarch64-gnu-g++
j'ai pu configurer correctement Qt ! Mais la compilation a bloqué
immédiatement vu que des dépendances pour aarch64 n'étaient pas disponible La page précédente du wiki
ainsi que celle des bibliothèques requises pour Linux
donnent les bibliothèques que je devrais installer dans le préfixe aarch64 pour que Qt puisse les trouver
et compiler. Mais... Qt est un projet assez énorme, comptant de nombreuses dépendances. Bien trop nombreuses
pour que je les télécharges et installe à la main; nécessaire vu que Arch Linux ne propose que des paquets
x86_64/x86.
Cross-compiler Qt ne semblait pas être la meilleure idée, j'ai décidé d'abandonner la cross-compilation et de porter Cockatrice autrement.
Compilation native
Il me restait donc comme option compiler directement depuis Arm pour Arm. La solution la plus simple serait de le faire sur du matériel Arm 64 bits, comme une Raspberry Pi (3/4). Par chance, j'en possède une ! L'autre solution serait d'émuler un processeur aarch64 sur mon ordinateur x86. Ce serait plus lent et plus complexe qu'avec ma Raspberry Pi, nécessitant en plus de faire fonctionner un environnement graphique le tout en émulant une architecture différente...
Émulation
QEMU
C'est donc la solution que j'ai choisi ! Les justifications étant que ce serait plus enrichissant, et que je n'ai pas prévu d'installer une interface graphique dessus pour le moment.
La façon la plus simple d'émuler un système aarch64 est d'utiliser QEMU, un émulateur Open Source générique permettant de créer des machines virtuelles émulant moult processeurs et architectures. La page sur l'émulation Arm présente les détails de base sur ce qui est nécessaire, en particulier quelle machine émuler. Le meilleur choix pour une machine générique dont j'ai besoin est la machine "virt".
La page du wiki Arch Linux sur QEMU s'est prouvée très utile pour préparer la machine virtuelle et comprendre le fonctionnement de base.
Machine virtuelle et compilation
J'ai choisi d'utiliser Arch Linux Arm, afin de garder la même logique pour
les paquets que sur ma propre machine et pour avoir une image de taille réduite. (Alors que je vais
installer un gestionnaire de bureau...)
Cependant pour une installation générique la page d'insutrctions est plutôt limitée. Mon premier essai n'a
donc absolument pas réussi. Par chance, j'ai trouvé une liste d'instruction
originellement produite pour les nouveaux Mac M1 réalisant exactement ce que je voulais !
(À l'émulation graphique près) Le suivre s'est avéré sans peine, et j'ai pu observer le boot se passer
avec succès !
La procédure standard s'appliquait donc : installer les outils de compilation, les différentes dépendances (dont Qt, pas besoin de le compiler ici) et enfin lancer la compilation de Cockatrice !
Et d'attendre.
Un certain temps.
En effet comme évoqué plus haut, émuler une architecture différente ralenti pas mal les choses. Par dessus cela, je n'ai alloué que deux cœurs de mon ordinateur à QEMU afin de pouvoir continuer à l'utiliser en parallèle.
Au bout de deux heures, le méfait était accompli : Cockatrice était compilé ! Mais sans interface graphique, impossible de tester le fonctionnement.
Interface graphique
En soit, installer et lancer une interface graphique n'est pas le plus compliqué. Le plus souvent,
il suffit d'installer le paquet adapté pour un gestionnaire de bureau et tout se fait automatiquement.
Non, ici le problème était autre : comment émuler une interface graphique avec QEMU ? On notera que dans
la commande proposée par les instructions, on trouve l'option -no-graphics
. Un bon premier pas :
l'enlever !
Ok, on a donc une fenêtre QEMU et plus juste un terminal, fantastique. Maintenant, il fallait la transformer
en un écran pour Linux. Pour cela rien de plus simple d'après la documentation, rajouter une carte graphique
virtuelle ! La documentation de QEMU recommande -device virtio-gpu-pci
, avec lequel j'ai pu en effet
arriver à un TTY normal, comme si j'avais connecté un écran à une machine. Parfait !
Moins parfait : le clavier ne répond pas. Dans les options on trouve -device virtio-keyboard-device
,
visiblement on utiliserait un clavier virtuel, ce qui peut expliquer que notre clavier réel n'ai pas
beaucoup d'effet.
Après quelques recherches, le meilleur moyen semblait de passer les périphériques USB directement à la
machine virtuelle. Pour une machine Arm, il faut préciser -usb -device usb-ehci
pour activer l'USB.
Ensuite, il y a plusieurs possibilités pour autoriser des périphériques particuliers sur la machine
virtuelle. La première que j'ai essayé est avec le numéro de bus et de périphérique, mais j'ai fini par
utiliser les identifiant de vendeur et de produit directement, comme montré sur la page USB
de la documentation de QEMU. Par exemple pour mon clavier :
-device usb-host,vendorid=0x413c,productid=0x2010
.
Je suis resté sur ce point pendant bien trop longtemps, sans clavier fonctionnel, à tester différentes
cartes graphiques virtuelles et autres manipulations. Au final, il me manquait le rajout du périphérique
usb lui-même : -device usb-kbd
. Il semblerait que la première option ne fasse qu'exposer le clavier à
la machine virtuelle, alors que la deuxième permette au système d'exploitation virtuel de le considérer
correctement, le "brancher".
Pour la souris, la meilleure option semble être -device usb-tablet
: elle permet de prendre la souris
en compte comme un pointeur x,y
et non pas comme une souris normale, rapportant des déplacements. Vu
que notre machine virtuelle est bien plus lente que nos déplacements de souris, il est facile de les
accumuler sans faire exprès et se retrouver avec des déplacements ératiques. En "mode tablette", le curseur
virtuel se positionnera simplement au même endroit que le curseur réel.
Ne restait plus alors qu'à installer et configurer un gestionnaire de bureau pour avoir une interface graphique. J'ai choisi KDE, sachant que de nos jours il est plutôt léger tout en étant un des plus commun. Je ne me faisais donc pas de soucis sur l'installation et la configuration de base.
Pour qu'il soit lancé correctement, j'ai du modifier le /etc/X11/xinit/xinitrc
pour lancer KDE au
démarrage de X11, et modifier /etc/profile
afin que xinit
soit appelé automatiquement. Le tout,
encore une fois à l'aide du splendide Wiki Arch Linux.
Une fois cela fait et finalement en possession d'un bureau émulé pour Arm, je pouvais enfin tester si la compilation de Cockatrice avait fonctionné...
Conclusion
Résultat
Et elle avait fonctionné ! Le lancement s'est effectué comme après la compilation pour x86, a suivi les même étapes, téléchargé et configuré les mêmes éléments. Le chargement de deck fonctionnait, j'ai même pu rapidement tester que la connexion à un serveur ainsi que la partie "jeu" en elle-même étaient fonctionnelles avec l'aide d'un ami habitué au logiciel. La compilation a donc été un succès !
Réalisation
Après ce succès, je suis retourner explorer le dépôt Github de Cockatrice et franchement je ne sais plus pourquoi. Peut-être même pour retrouver des liens à mettre dans ce poste. Toujours est-il que, caché sous quelques niveaux de navigation, j'ai découvert une page expliquant... Comment compiler sur une Raspberry Pi ! Ah ! Et bien, oui, en effet : les chances que je rencontre un problème étaient maigres...
Rétrospective et continuation
Malgré cette "boutade", l'expérience entière a été très enrichissante et intéressante Quelques moments de frustration, mais au final très satisfait d'avoir réussi le challenge initial que de compiler une application non-triviale pour Arm depuis ma machine x86, même si elle le supportait déjà, sans paquet officiel. J'ai maintenant une meilleure connaissance des options disponibles ainsi que de l'utilisation de QEMU, ce qui peut être utile dans de nombreuses situations !
Je sais aussi que si je devais le refaire, j'allouerai plus de cœurs à la compilation dans QEMU et laisserai faire en allant m'occuper ailleurs, afin de minimiser la durée.
Il y a cependant deux choses que j'aimerais encore faire :
- Essayer d'intégrer la compilation AArch64 à l'intégration continue (CI) de Cockatrice
- Réaliser un vrai portage demandant du travail. C'est une expérience que j'envisage enrichissante et utile d'un point de vue pratique de la programmation.
Mais tout ceci est encore à venir !