Greboca  

DLFP - Dépêches  -  Génération de fichiers AAB Android pour GCompris

 -  Février 2022 - 

GCompris est une application éducative pour les enfants de 2 à 10 ans. À chaque sortie, une dépêche sur LinuxFr.org décrit les changements.
Pour une fois, cette dépêche ne concerne pas une sortie mais est un peu plus technique et raconte mes péripéties pour générer un paquet dans le nouveau format Android AAB (Android App Bundle) sorti en mai 2018 (d’après https://android-developers.googleblog.com/2021/06/the-future-of-android-app-bundles-is.html).

Sommaire

L’historique

Pour l’histoire, GCompris a été ré-écrit en C++/QML/JavaScript en 2014 et est disponible sur Android depuis 2015. Il est aussi disponible sur Windows, Linux, macOS, Ubuntu Touch et la compilation gérait à un moment Sailfish OS (mais bon, ce n’est probablement plus fonctionnel depuis que nous sommes passés à Qt 5.9 minimum).

Il y a deux dépendances externes que nous compilons via un ExternalModule : qml-box2d (qui est un wrapper de Box2D en QML) et OpenSSL (compilé pour Android seulement).

J’avais mis en place la compilation avec CMake depuis quasiment le début en 2014.
J’étais jeune à l’époque et moins expérimenté, donc il y a eu pas mal d’essais, de bidouilles et autres incantations pour que ça marche ©.

GCompris essaie de gérer les 2-3 précédentes versions LTS de Qt. La version actuelle est compilable à partir de la version Qt 5.9.

Pour Android, nous essayons de gérer à la fois Qt 5.12 et Qt 5.15 pour la prochaine version (la 5.12 nous permet de déployer sur les Android 4.1, la 5.15 à partir d'Android 5.0), sachant qu’il y aura une coupure nette avec Qt 6, car il n’y a pas la compatibilité QML entre les versions 5 et 6 (qui elle cible à partir d'Android 6.0).

Qt 5.14 et Android

Pour en revenir au sujet principal, Qt, depuis sa version 5.14 permet de générer à la fois des APK mais aussi des AAB pour Android.
La génération d’un binaire (APK ou AAB) passe par l’outil androiddeployqt qui prend en entrée un fichier JSON définissant les propriétés pour créer le binaire et divers autres paramètres pour créer en mode debug, release, signé…
La version fournie depuis Qt 5.14 possède entre autres une option pour générer un AAB.
Malheureusement, suite aux besoins qui ont évolué, le fichier JSON a aussi été modifié et n’est plus compatible avec l’ancien.
Pareil, pour le fichier de « AndroidManifest.xml » qui a évolué de façon non compatible.

Par défaut CMake permet de ne générer que pour une seule architecture. La solution proposée par Qt est de définir les architectures que l’on veut déployer dans la configuration CMake : ANDROID_BUILD_ABI_abi, avec abi étant dans armeabi-v7a, arm64-v8a, x86 et x86_64. Ensuite il s’occupera tout seul de compiler n fois le logiciel en créant des ExternalProject pour chaque architecture sauf pour l’architecture définie par ANDROID_ABI (qui, elle, sera faite dans le dossier de build par défaut).

Un autre billet de blog, écrit par un autre membre la communauté KDE, permet de voir les changements.

Les embrouilles

Comme indiqué précédemment, GCompris a fait sa propre tambouille pour faire fonctionner les générations. Par exemple, nous disposons de nos propres targets dans CMake (apk_debug, apk_release) alors que Qt fournit par défaut une target APK.
Nous surchargeons aussi le fichier JSON et AndroidManifest.xml pour les remplir à notre façon.
OpenSSL était généré via un script qui ne fonctionnait que pour deux targets (avec un bon if dans le CMakeLists.txt pour choisir).
Les bibliothèques compilées (Box2D, GCompris et OpenSSL) n’étaient pas générées au bon endroit par défaut et ça rendait la création des AAB compliquée.
La ligne pour lancer CMake était… lourde et compliquée :

   cmake -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \
    -DCMAKE_BUILD_TYPE=release \
    -DANDROID_ARCHITECTURE=arm \
     DQt5_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5 \
    -DQt5Qml_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Qml \
    -DQt5Network_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Network \
    -DQt5Core_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Core \
    -DQt5Quick_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Quick \
    -DQt5Gui_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Gui \
    -DQt5Multimedia_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Multimedia \
    -DQt5Svg_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Svg \
    -DQt5Widgets_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Widgets \
    -DQt5LinguistTools_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5LinguistTools \
    -DQt5Sensors_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5Sensors \
    -DQt5AndroidExtras_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5AndroidExtras \
    ..

Un peu de nettoyage

Au lieu de foncer tête la première dans le tas, il a d’abord fallu rendre le code existant un peu plus propre et se remettre en phase le plus possible avec la façon que Qt utilise pour générer les APK.

Comme tout bon développeur, j’ai donc regardé ce qui était fait ailleurs et surtout la doc de Qt.
Dans ce fichier, on voit comment Qt gère les architectures, les arguments qu’il fournit à ses projets pour les autres architectures, les cibles… Bref, le Graal de la documentation !
La variable un peu « magique » pour générer les AAB à garder en mémoire est CMAKE_LIBRARY_OUTPUT_DIRECTORY qui aura dans les sous-projets la valeur du dossier de sortie du projet principal.
La première étape a donc été de nettoyer notre propre code pour pouvoir compiler avec seulement les mêmes options qui sont passées l’ExternalProject (car sinon, nous n’arriverions pas à compiler pour les autres architectures).

cmake -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \
  -DCMAKE_ANDROID_API=16 \
  -DCMAKE_BUILD_TYPE=Release \
  -DANDROID_ABI=$1 \
  -DCMAKE_FIND_ROOT_PATH=${Qt5_BaseDIR}/${QtTarget}/lib/ \
  -DQt5_DIR=${Qt5_BaseDIR}/${QtTarget}/lib/cmake/Qt5

Au lieu de mettre directement CMAKE_PREFIX_PATH, on a gardé Qt5_DIR (et dans le CMakeLists.txt, on met CMAKE_PREFIX_PATH = Qt5_DIR), pour ne pas  impacter la compilation pour les autres les systèmes d’exploitation.

Pour OpenSSL, KDAB fournit des scripts pour compiler sur toutes les plateformes. En s’inspirant de ce qui était fait, on a réussi à tout gérer dans l’ExternalProject, en gérant proprement l’architecture, sans avoir recours à des scripts externes et gérer l’installation dans le bon dossier (le CMAKE_LIBRARY_OUTPUT_DIRECTORY du dessus !).

Jusqu’ici, rien qui ne permet de mieux compiler en Qt 5.15 (que ce soit des APK ou des AAB) mais du code bien plus propre et plus maintenable qui fonctionne toujours sur Qt 5.12.

Mise à jour du code pour gérer Qt 5.14

Le plugin qml-box2d est une dépendance externe qui se base sur qmake pour compiler.
qmake, dans les projets depuis 5.14, génère maintenant cinq fichiers Makefile : un par architecture (Makefile.Armeabi-v7a, Makefile.Arm64-v8a, Makefile.X86 et Makefile.X86_64) et un principal qui va appeler les quatre autres. Évidemment, ce n’est pas ce que l’on souhaite, on veut juste compiler pour notre architecture (et chaque sous-projet compilera pour la sienne). Ce n’est pas grave, nous avons notre petite variable ANDROID_ABI qui contient : armeabi-v7a, arm64-v8a, x86 et x86_64 donc il suffit de le concaténer via CMake au moment d’appeler le build de Box2D et de faire un make -f Makefile.${ANDROID_ABI}.
Le problème, si vous n’avez pas remarqué, est que les Makefile ont leur première lettre de l’architecture en majuscule. Il n’y a pas de solution « facile » pour convertir la première lettre seulement, donc on a dû utiliser une solution pas très belle mais qui marche.

Trop propre ?

Évidemment, après avoir testé, cela fonctionnait presque bien. Il manquait la bibliothèque Box2D dans les paquets… Après avoir réfléchi à comment faire sans toucher aux autres plateformes, une solution extrêmement crade (mais fonctionnelle…) était de rajouter une copie du fichier dans le « bon » dossier.

Autre souci, les APK générés faisaient 10 Mio de plus que la version compilée avec Qt 5.12. Suite au passage à Qt 5.14, il y a eu des changements sur la façon de gérer les « assets » (modules QML…) de Qt. Avant, les fichiers étaient copiés dans le dossier assets directement, mais depuis 5.14, ils mettent tous les fichiers dans un fichier de ressources rcc.
Pour je ne sais quelle raison non expliquée dans la documentation, dans le système de build, ils ont décidé de ne plus compresser les fichiers rcc qui sont dans les assets.

Côté GCompris, on génère des rcc par activité qui contiennent le code QML, JavaScript et les images associées. Il y en a pour 80 Mio actuellement et évidemment, avec la non compression de ces fichiers, on perdait 10 Mio de compression.
La solution « temporaire » a été de dupliquer le fichier build.gradle qui gère la compilation Android pour commenter cette non-compression.

À un certain point, il faudra voir pour migrer les rcc des activités dans des vrais plugins QML. L’avantage premier sera la compilation de ce code (donc plus rapide d’après les posts de blog : https://www.qt.io/blog/the-new-qtquick-compiler-technology et https://www.qt.io/blog/the-numbers-performance-benefits-of-the-new-qt-quick-compiler).
Mais dans ce cas-là, cela voudra dire qu’on compile pour chaque architecture les activités (sachant que les images, qui prennent le plus de place, ne seront pas plus compressées) et donc cela risque d’exploser la taille du fichier AAB (voir dessous pour plus de détails sur son contenu) et ne sera plus portable entre différentes plateformes (on aurait pu imaginer à un certain point créer une activité sous Linux, la copier sur un serveur et pouvoir l’installer puis la charger dynamiquement sur n’importe quel système d’exploitation).

Autres subtilités

Il y a eu d’autres subtilités sur lesquelles je ne vais pas trop m’étaler, par exemple, le dossier dans lequel on génère les assets doit s’appeler android-build (ce qui évite les copies).
On a dû garder nos propres fichiers JSON plutôt que d’utiliser celui fourni par Qt, car, de ce que j’ai compris de la doc, les variables sont des propriétés attachées à la cible de la bibliothèque et je n’ai pas eu le temps de me pencher plus dessus.

Alors, ça marche ?

Et bien oui ! La génération des APK séparément fonctionne en passant les bons paramètres.
Pour les AAB, qui dit « nouveau format » dit « mais comment ça marche et j’en fais quoi de ça moi ».

Il existe un utilitaire bundletool qui permet de générer un fichier APKS à partir d’un fichier AAB. Un APKS est une archive contenant plusieurs APK.
Dans notre cas, il y a trois types d’APK dans l’archive :

  • base-${ANDROID_ABI} (arm64_v8a, armeabi_v7a, x86 et x86_64)
  • base-hdpi, base-ldpi… (un par résolution)
  • base-master.apk (le fichier contenant les assets, commun à toutes les architectures)

Lorsqu’un périphérique demandera à Google de télécharger le jeu, il va se baser sur l’architecture et la résolution pour ne fournir que les trois APK nécessaires et installer ce qu’il faut.

Une documentation basique a été écrite dans le wiki de GCompris pour savoir comment le tester et déployer sur son périphérique Android avec l’outil bundletool.

Sachant que Google a rendu obligatoire depuis août 2021 la création des nouveaux projets sur le Google Play Store à passer obligatoirement par des fichiers AAB, il ne saurait tarder que même les projets qui étaient créés avant doivent aussi y passer bientôt.

Un autre avantage est qu’en utilisant un AAB, la limite de l’APK téléchargé est maintenant de 150 Mio à la place de 100 Mio pour un APK unique (GCompris est très proche de cette limite).

Commentaires : voir le flux Atom ouvrir dans le navigateur

par Johnny_Jazeix, Lawless, BAud, palm123, Benoît Sibaud, Ysabeau

DLFP - Dépêches

LinuxFr.org

Tribune April : Techsoup et Solidatech, instruments d'influence

 -  27 mars - 

Après une première position sur Solidatech en 2020, l'April a passé à nouveau du temps pour étudier et comprendre la place des structures Solidatech (...)


TuxRun et le noyau Linux

 -  27 mars - 

Il y a quelques années, je vous avais présenté TuxMake, un utilitaire pour faciliter la (cross-)compilation du noyau Linux supportant une grande (...)


Retour d’expérience sur l’utilisation de GrapheneOS (ROM Android libre)

 -  18 mars - 

Suite à la dépêche Comparatif : GrapheneOS vs LineageOS, je souhaitais faire part d’un retour d’expérience sur l’utilisation de GrapheneOS sur un (...)


Ubix Linux, le datalab de poche

 -  16 mars - 

Ubix Linux est une distribution Linux libre et open-source dérivée de Debian.Le nom « Ubix » est la forme contractée de « Ubics », acronyme issu de (...)


Open Food Facts : récit d’un contributeur

 -  15 mars - 

Récit de mon aventure en tant que contributeur pour le projet Open Food Facts, la base de donnée alimentaire ouverte et collaborative, où je suis (...)