L'adaptation d'Outer Wonders pour Linux

L'adaptation d'Outer Wonders pour Linux

Bonjour ! La démo jouable d'Outer Wonders est sortie sur itch.io depuis maintenant trois semaines pour Windows et Linux, et nous travaillons déjà à des améliorations la concernant. La prise en charge de Linux en particulier n'a pas été chose aisée, la documentation sur la production d'un jeu pour Linux n'étant pas toujours à jour ou même claire. Nous vous proposons donc aujourd'hui de découvrir le processus que nous avons mis en place pour obtenir une excellente prise en charge de Linux pour Outer Wonders !

La base : la compilation

Lorsque les développeurs d'un jeu utilisent un moteur de jeu commercial, le moteur dispose généralement d'une fonctionnalité permettant de produire des versions pour chaque plate-forme. Linux est pris en charge par la plupart des moteurs commerciaux, même si la qualité de cette prise en charge est variable.

Lorsqu'un un moteur interne est utilisé, plus d'aspects doivent être abordés manuellement, notamment lorsqu'il s'agit de produire une version Linux du jeu.

Tout d'abord, pour des raisons de productivité évidentes, il est indispensable d'avoir recours à des technologies garantissant une excellente prise en charge des plates-formes ciblées. Très souvent, les moteurs internes sont écrits en C ou C++ (moteurs natifs), ou, plus récemment, basés sur des technologies plus accessibles et fournies en fonctionnalités comme HTML5/JavaScript. Chez Utopixel, nous utilisons un moteur interne basé sur le langage Rust, ainsi que SDL2.

Le couple de technologies Rust/SDL2 joue pour nous le rôle de langage commun à toutes les plates-formes, pour reprendre les termes de ce billet de blog. Autrement dit, actuellement, près de 99 % de notre code est indépendant de la plate-forme ciblée, et grâce à l'outillage fourni par Rust, nous pouvons facilement produire des versions pour plusieurs plates-formes, dont Linux.

En effet, en tant que langage de programmation natif (c'est-à-dire permettant de produire un programme directement compréhensible par la plate-forme ciblée), Rust fournit un mécanisme de compilation, pour de multiples plates-formes.

Schéma montrant la mécanique de compilation pour de multiples plates-formes, telles que Windows et Linux. À partir d'une base commune, appelée le code source, il est possible produire une application pour Windows grâce à un compilateur d'applications Windows et, de la même façon, il estpossible de produire une application grâce à un compilateur d'applications pour Linux.

Processus spécifique à la version Linux

Malgré d'importants efforts de vulgarisation, cette section est pointue et technique et vous intéressera surtout si les aspects informatiques et technologiques du développement d'un jeu vidéo vous passionnent. Dans le cas contraire, nous vous recommandons de passer à la section suivante : Des tests stricts.

Développant Outer Wonders par l'intermédiaire d'une machine sous Windows, il nous a été nécessaire de consulter une grande quantité de documentation pour obtenir une compilation fiable pour Linux.

La première documentation que nous avons consultée pour produire une version Linux d'Outer Wonders a été la documentation d'itch.io destinée aux développeurs (en anglais), qui donne de nombreux bons conseils.

Nous avons suivi la plupart de ces conseils, et ceux que nous ne pouvions pas suivre ont fait l'objet de procédés alternatifs pour remplir le même but : une excellente compatibilité entre les diverses distributions de Linux.

Lister les dépendances extérieures

La première étape importante pour obtenir une bonne compatibilité avec la majorité des distributions Linux consiste à connaître exactement les dépendances extérieures requises par le jeu, c'est-à-dire les bibliothèques et paquets qui doivent être installés sur le système du joueur pour que le jeu fonctionne.

Si vous êtes utilisateur de Linux, la notion de dépendance vous parle probablement : il s'agit des fichiers installés par défaut sur le système, ou installés par des commandes telles que apt install paquet, dnf install paquet ou pacman -S paquet.

En cas de doute, il est possible de connaître les dépendances d'une application. Les outils objdump, ldd et readelf (présents dans le paquet binutils proposé par la plupart des distributions Linux) permettent d'en savoir plus sur les dépendances de l'application.

Ainsi, dans le cas d'une application se contentant d'afficher le message "Hello, world!" dans un terminal, la commande indiquée dans l'exemple ci-dessous :

Terminal Linux
$ readelf -d /chemin/vers/application

La section dynamique à l'offset 0x2dc8 contient 27 entrées :
Étiquettes         Type                 Nom/Valeur
0x0000000000000001 (NEEDED)             Bibliothèque partagée: [libc.so.6]
0x000000000000000c (INIT)               0x1000
0x000000000000000d (FINI)               0x11e8
...

nous indique que notre application ne dépend que libc (un composant essentiel installé par défaut sur tous les systèmes Linux).

Dans le cas d'Outer Wonders, nous avons pris la décision de ne conserver qu'un très petit nombre de dépendances extérieures. SDL2 est l'une d'entre elles ; le reste est composé de bibliothèques qui sont installées par défaut sur tous les systèmes Linux, telles que libm, libdl et libc. Ceci limite grandement les risques d'incompatibilité, même si une attention particulière doit être accordée au cas de libc.

En tant que programme Rust, Outer Wonders a tout de même des dépendances à des éléments écrits en Rust, mais le fonctionnement de Rust est tel que ces dépendances sont directement intégrées à l'application créée, ce qui, à nouveau, réduit fortement le risque d'incompatibilité.

Tout ceci nous a amenés à porter principalement notre attention sur 2 dépendances extérieures: SDL2 et glibc. Le processus de production que nous avons mis en place nécessite quelques aménagements spécifiques pour traiter correctement ces 2 dépendances.

Garder un œil sur la version minimale requise de glibc

L'une des plus grandes sources d'incompatibilité concerne libc, ou plutôt glibc, sa variante installée par défaut sur la majorité des distributions de Linux. La majorité des applications dépendant de libc dépendent en réalité de glibc.

Il existe de très nombreuses versions de glibc (vous en trouverez une liste exhaustive ici) et, plus la version dont l'application dépend sera basse, meilleure sera sa compatibilité avec une grande variété de distributions de Linux.

Étant donné que les outils de compilation se basent en partie sur la version de glibc installée sur le système du développeur, la documentation d'itch.io destinée aux développeurs précise donc qu'il est préférable de produire une application Linux depuis une distribution ancienne.

Une fois une application produite, il est possible de déterminer la version minimale de glibc requise par l'application. La commande ci-dessous :

Terminal Linux
$ objdump -T /chemin/vers/application

/chemin/vers/application:     format de fichier elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*	0000000000000000              _ITM_deregisterTMCloneTable
0000000000000000      DF *UND*	0000000000000000  GLIBC_2.2.5 puts
0000000000000000      DF *UND*	0000000000000000  GLIBC_2.2.5 __libc_start_main
0000000000000000  w   D  *UND*	0000000000000000              __gmon_start__
0000000000000000  w   D  *UND*	0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*	0000000000000000  GLIBC_2.2.5 __cxa_finalize

nous indique plus d'informations sur la dépendance à libc d'une application. Dans l'exemple ci-dessus, notre application dépend par exemple d'une fonctionnalité appelée puts fournie par la version 2.2.5 de glibc. En observant bien les lignes contenant une mention GLIBC_, nous pouvons déterminer la version minimale requise : il s'agit de la version la plus élevée que vous trouverez dans la liste.

Dans l'exemple ci-dessus, la version minimale requise de glibc est la version 2.2.5, datée de 2002. La compatibilité sera donc excellente !

Dans des exemples d'applications plus abouties, ce sera rarement le cas, cependant. Par exemple, les dépendances extérieures (comme SDL2, dans le cas d'Outer Wonders) dépendent généralement aussi de glibc. Il conviendra donc de surveiller aussi la version de glibc requise par les dépendances.

Une fois cette première analyse effectuée, il est alors utile de lister les fonctionnalités qui sont à l'origine d'une dépendance à des versions trop récentes de glibc.

Dans le cas d'Outer Wonders, la mise en place de distributions anciennes de Linux n'a pas été fructueuse, car l'installation de telles distributions dépend de dépôts en ligne où sont stockées les dépendances installables, et les dépôts liés à des distributions trop anciennes sont supprimés ou archivés. Nous nous sommes alors rabattus sur Ubuntu 20.04.

Nous avons alors constaté qu'Outer Wonders dépendait de certaines fonctionnalités mathématiques liées à la notion de puissance (pow, exp, et log), qui introduisent systématiquement une dépendance à glibc 2.27, une version datée de 2018, ce qui est loin d'être idéal pour la compatibilité. De plus, si nous parvenions à éliminer ces dépendances, la version minimale requise de glibc chuterait à la version 2.18, datée de 2013, ce qui est bien plus acceptable !

Nous nous sommes donc attelés à la recherche et à l'élimination de toute référence à ces fonctions. Nous avons alors constaté que des fonctionnalités n'étaient utilisées qu'une seule fois : au moment de lire des nombres depuis des fichiers du jeu. En théorie, ces nombres peuvent avoir un format tel que 1e15, ce qui signifie "10 à la puissance 15", d'où le recours à des fonctions liées à la notion de puissance. Or, gérer ce scénario n'était pas pertinent dans notre cas car nous n'avions besoin de lire que des nombres entiers relativement faibles (à moins de vouloir tenir compte d'écrans qui, dans un futur lointain, auraient des résolutions de 16.000.000×9.000.000 pixels ?). Nous avons donc retiré le code gérant ces scénarios non-pertinents et ainsi atteint une compatibilité avec glibc 2.18 !

Fournir SDL2 sous une forme portable

SDL2 est central dans le moteur sur lequel Outer Wonders est basé. Néanmoins, SDL2 est une dépendance complètement externe, dans le sens où il s'agit d'une bibliothèque qui n'est généralement pas installée par défaut sur la majorité des distributions de Linux.

Dans ces conditions, comment faire en sorte qu'Outer Wonders ait accès à SDL2 pour fonctionner sur le système du joueur ? 3 approches nous viennent à l'esprit :

  • Obliger le joueur à installer la dépendance libsdl2 lui-même avant de lancer le jeu ;
  • Au démarrage du jeu, examiner le système pour déterminer si la dépendance libsdl2 est installée, et, si ce n'est pas le cas, l'installer automatiquement avant de poursuivre le lancement du jeu ;
  • Fournir une copie de la dépendance libsdl2 empaquetée avec Outer Wonders.

Ces 3 approches ont toutes leurs avantages et leurs inconvénients, et aucune n'est parfaite :

  • La première approche est pratique pour le développeur, mais loin d'être idéale pour le joueur, même si elle lui permet d'économiser un peu d'espace sur son ordinateur. L'adopter signifie que, si le joueur n'a pas la dépendance installée sur son système, il recevra un message d'erreur qui ne sera probablement pas d'une grande clarté pour lui. Le risque de perdre le joueur à ce stade est très important.
  • La seconde approche est intéressante, mais doublement risquée. Elle permet au joueur d'économiser un peu d'espace disque tout comme la première approche, mais complexifie fortement le travail du développeur (l'installation automatique de dépendances n'est pas standardisée entre les diverses distributions de Linux !) et alourdit considérablement l'expérience du premier démarrage du jeu (le joueur doit attendre l'installation de la dépendance avant de pouvoir jouer).
  • La dernière approche permet une grande portabilité (pas de souci de démarrage), mais demande un travail potentiellement considérable pour le développeur (il faut s'assurer que la dépendance fournie avec le jeu est effectivement portable entre les distributions, et, si la dépendance fait l'objet de correctifs, le fardeau de la mise à jour revient en partie au développeur de l'application !) et contribue à une plus grande utilisation de l'espace disque du joueur (chaque jeu optant pour cette approche contribue à l'installation d'une nouvelle copie de la dépendance alors que cette dernière pourrait n'être installée qu'une fois !)

L'approche recommandée pour produire un jeu pour itch.io est en fait la troisième approche, car il s'agit de celle qui propose la meilleure portabilité, et fournit la meilleure expérience au joueur.

Cependant, cette approche implique de distribuer les dépendances externes avec l'application, ce qui est un travail potentiellement long. Pour assurer une portabilité maximale, il est en effet préférable que le développeur produise sa propre version de la dépendance, indépendante de la distribution de Linux ciblée.

Dans le cas de SDL2, ce processus est heureusement bien documenté. Il suffit de télécharger le code source de SDL2, d'installer les dépendances indiquées sur cette page, et d'exécuter la commande :

Terminal Linux
./configure.sh; make; sudo make install

depuis le répertoire racine du code source de SDL2, afin d'obtenir un fichier libSDL2-2.0.so.0 dans un dossier précis (généralement /usr/local/bin). Ce fichier, prévu pour un fonctionnement sur toute distribution de Linux, peut alors être copié et fourni avec l'application.

Mais fournir ce fichier avec l'application ne suffira pas encore à lier l'application à SDL2 de telle sorte que l'application en utilise les fonctionnalités. Ce lien entre l'application et la dépendance doit être explicitement déclaré.

Une façon d'effectuer cette déclaration consiste à définir ce que Linux appelle le chemin de recherche des bibliothèques (plus souvent appelé rpath) de l'application. Il s'agit du dossier dans lequel Linux va rechercher les dépendances en premier, au moment de démarrer une application.

Il existe pour cela un outil pratique appelé patchelf. Cet outil permet d'effectuer certaines modifications bien spécifiques sur les applications, parmi lesquelles la suivante :

Terminal Linux
patchelf --set-rpath '$ORIGIN' /chemin/vers/application

La commande ci-dessus permet de définir le rpath de l'application à $ORIGIN. Cette valeur $ORIGIN représente en fait le dossier où l'application elle-même se trouve. Pour résumer, la commande ci-dessus signifie : "Modifie l'application pour que, quand on la démarrera, Linux recherche les dépendances dans le dossier de l'application en premier".

Cette manipulation est la dernière étape permettant de rendre l'application complètement portable ! Il suffit désormais de fournir l'application avec le fichier libSDL2-2.0.so.0 précédemment produit, et l'application fonctionnera sur un large éventail de distributions de Linux !

Dans le cas d'Outer Wonders, nous adopté ce processus avec succès, ce qui nous a permis de déployer Outer Wonders sur itch.io. Pour améliorer la portabilité du jeu, nous avons été amenés à supprimer certaines fonctionnalités mineures de SDL2. En effet, SDL2 fournit un accès aux fonctions mathématiques liées à la notion de puissance précédemment mentionnés (pow, exp, et log), ce qui introduisait à nouveau une dépendance indésirable à la version 2.27 de glibc.

Nous n'avons eu aucun signalement de dysfonctionnement d'Outer Wonders sur Linux jusqu'ici, y compris sur des distributions demandant une certaine expertise, comme Arch Linux. Nous considérons donc ce processus comme fiable !

Des tests stricts

Rien ne vaut des tests stricts pour s'assurer de la fiabilité d'un processus aussi complexe.

Le processus de test est très simple : il suffit de démarrer l'application sur une installation par défaut de Linux, ce qui permet de tester le comportement de l'application sur une machine où les dépendances de notre application ne sont pas installées.

Si vous obtenez un message d'erreur tel que :

Terminal Linux
/chemin/vers/application: error while loading shared libraries: libSDL2-2.0.so.0: cannot open shared object file: No such file or directory

cela veut dire que, soit la manipulation utilisant la commande patchelf n'a pas été correctement effectuée, soit la dépendance (notre fichier libSDL2-2.0.so.0) n'a pas été correctement déployée (la manipulation utilisant la commande patchelf est telle que la dépendance doit figurer dans le même dossier que l'application elle-même).

En revanche, si l'application démarre :

Capture d'écran du bureau d'Ubuntu, avec une fenêtre affichant l'écran-titre d'Outer Wonders.

il y a alors de bonnes chances pour que l'application soit fonctionnelle et portable !

Ainsi s'achève ce billet de blog technique ! Rejoignez notre serveur Discord, suivez-nous sur Twitter, Facebook et Instagram pour suivre nos actualités et avoir une nouvelle énigme tous les mercredis ! Suivez aussi notre flux RSS pour être tenus au courant de la publication de nouveaux billets de blog.

À bientôt !