Compilation croisée Gnunux vers Windows | Blog | Superflu Riteurnz

Compilation croisée Gnunux vers Windows | Blog | Superflu Riteurnz Compilation croisée Gnunux vers Windows | Blog | Superflu Riteurnz
  • Steam
  • Nintendo Switch
  • Itch.io
  • Google Play
Superflu et son assistante Sophie

Blog

Compilation croisée Gnunux vers Windows

2022-02-23

Je ne vous apprends sans doute rien si je vous dis que je développe – et fais aussi l'intégralité de mes activités numériques – sur GNU/Linux. Bien sûr, comme beaucoup jouent sur Microsoft Windows, il est évident que le jeu doit tourner aussi sur cette plate-forme.

L'idée de devoir approcher d'un système Windows sans la présence d'un exorciste ne m'enchantant guère, je me suis très vite penché sur la possibilité de « cross-compiler » mon jeu : produire un exécutable compatible Windows (le fameux .exe) depuis un environnement Gnunux.

MinGW

Minimalist GNU for Windows, MinGW pour les intimes, est un ensemble d'outils de développement et de compilation GNU pour Windows qui a l'avantage d'être disponible sur Gnunux et de permettre donc la compilation croisée (cross-compilation). Il est dispo dans les dépôts des distributions usuelles, on l'installe par exemple sur les Debian et dérivées par :


# apt install mingw-w64

Le paquet fournit un ensemble d'exécutables, qui sont tous de la forme [architecture]-w64-mingw32-[executable], par exemple :

  • x86_64-w64-mingw32-g++ pour compiler du C++ pour une architecture 64 bits
  • i686-w64-mingw32-gcc pour compiler du C pour une architecture 32 bits (aussi compatible 64 bits)
  • x86_64-w64-mingw32-windres pour manipuler les fichiers de ressource Windows (qui permettent notamment de donner une icone à un exécutable)

Et bien d'autres, des outils d'analyse des bibliothèques dynamiques requises par un exécutable, mais aussi des choses comme gprof ou gcov. Bref, y'a de quoi de faire.

On trouvera aussi les bibliothèques logicielles… non pas au format SO mais au format DLL bien sûr, Windows oblige. Pour l'architecture 64 bits, ça se trouve dans /usr/lib/gcc/x86_64-w64-mingw32/9.3-win32/. On y trouvera notamment des choses comme libstdc++-6.dll, la biblio standard du C++.

Bibliothèques tierces

Rappelons que Superflu Riteurnz repose sur 3 bibliothèques tierces :

Bien sûr, elles sont toutes compatibles avec Windows (oui, c'est bien de vérifier ça avant de se lancer dans l'idée de cross-compiler).

La SDL a la gentillesse de fournir directement ses bibliothèques de développement pour MinGW sur sa page de téléchargements (et c'est aussi le cas pour ses composantes annexes).

Pour les deux autres, on peut trouver des versions de dév MinGW dans la base données MSYS2 Packages :

Si vous avez besoin d'autres biblios, c'est un bon endroit pour les chercher.

Configuration & compilation

Bon, on a nos outils de dév et nos biblios, y'a plus qu'à brancher tout ça ensemble. J'utilise CMake comme outil de configuration, et dans le cas de la compilation croisée, on va utiliser ce qu'on appelle un fichier de toolchain, qui va définir un certain nombre de variables :


set(CMAKE_SYSTEM_NAME Windows)

set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)

set(CMAKE_FIND_ROOT_PATH  /usr/i686-w64-mingw32 /home/gee/local/i686-w64-mingw32)

file(GLOB_RECURSE SOSAGE_SYSTEM_DLLS "/usr/lib/gcc/i686-w64-mingw32/9.3-win32/*.dll")
file(GLOB_RECURSE SOSAGE_LOCAL_DLLS "/home/gee/local/i686-w64-mingw32/bin/*.dll")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

J'indique avec CMAKE_FIND_ROOT_PATH les endroits où sont stockées les environnements de développement : j'ai préféré placé les bibliothèques tierces dans un dossier utilisateur local, d'où la présence de /home/gee/local/.

Les commandes file(GLOB_RECURSE ...) permettent de lister les fichiers DLL utilisés, ce qui nous servira dans l'étape suivante.

Une fois le projet configuré avec ce fichier toolchain, on peut simplement lancer la compilation, et TADAM ! Un joli fichier superfluous-returnz.exe est généré !

Installateur généré par CPack

Bon, tout ça c'est très bien, mais bien entendu, si vous distribuez ce .exe tout seul, ça ne risque pas de marcher : un exécutable .exe n'est pas suffisant pour distribuer un jeu vidéo sur Windows, il faut également distribuer les données, les bibliothèques tierces nécessaires, et si possible le faire d'une manière user-friendly. Ça tombe bien : CMake, par l'intermédiaire de CPack, nous offre un mécanisme de création d'installateur automatisé.

Pour cela, on doit tout d'abord expliquer comment/où le logiciel s'installe avec les commandes install() dans le CMakeLists.txt. On peut bien entendu séparer la partie Gnunux de la partie Windows :


if (CMAKE_SYSTEM_NAME STREQUAL Linux)
  target_compile_options(${SOSAGE_EXE_NAME} PUBLIC -DSOSAGE_INSTALL_DATA_FOLDER="../share/${SOSAGE_EXE_NAME}/")
  install(TARGETS ${SOSAGE_EXE_NAME} DESTINATION bin)
  install(DIRECTORY ${SOSAGE_DATA_FOLDER}/data/ DESTINATION share/${SOSAGE_EXE_NAME})
  install(FILES ${SOSAGE_DATA_FOLDER}/resources/icon.svg DESTINATION share/icons/hicolor/scalable/apps/ RENAME ${SOSAGE_EXE_NAME}.svg)
  install(FILES ${SOSAGE_DATA_FOLDER}/resources/${SOSAGE_EXE_NAME}.desktop DESTINATION share/applications/)
  set(CPACK_GENERATOR "DEB;RPM")
elseif (CMAKE_SYSTEM_NAME STREQUAL Windows)
  target_compile_options(${SOSAGE_EXE_NAME} PUBLIC -DSOSAGE_INSTALL_DATA_FOLDER="data/")
  install(TARGETS ${SOSAGE_EXE_NAME} DESTINATION ".")
  install(DIRECTORY ${SOSAGE_DATA_FOLDER}/data/ DESTINATION "./data")
  install(FILES ${SOSAGE_SYSTEM_DLLS} DESTINATION ".")
  install(FILES ${SOSAGE_LOCAL_DLLS} DESTINATION ".")
  set(CPACK_GENERATOR "NSIS")
endif()

Ainsi, si sur Gnunux, on va utiliser les chemins système usuels (les dossiers bin/share du chemin global /usr/bin ou local /usr/local/bin ou autre), sur Windows, on met tout dans le répertoire du jeu : « tout », ça inclue les DLL nécessaires, d'où l'intérêt de les lister à l'étape précédente. Vous pouvez remarquer la déclaration du générateur CPack à utiliser : Deb/RPM pour Gnunux, NSIS pour Windows.

Vous pouvez aussi remarquer que sur Gnunux, il y a l'installation d'un fichier .desktop qui permet de créer une entrée dans le menu d'applications, et une icone. Sur Windows, on va laisser l'installateur s'occuper de la partie raccourcis/lanceurs, mais pour ce qui est de l'icône, on va utiliser un fichier « ressource » sosage.rc, un simple fichier texte qui va contenir une unique ligne :


id ICON "icon.ico"

On aura pris soin, bien sûr, de convertir notre icône PNG en fichier ICO Windows. Ensuite, pour que cette icône soit associée au fichier .exe produit, on utilise les commandes suivantes dans CMake :


set(SOSAGE_SRC ${SOSAGE_SRC} "${SOSAGE_DATA_FOLDER}/resources/sosage.rc")
set(CMAKE_RC_COMPILER_INIT windres)
enable_language(RC)
set(CMAKE_RC_COMPILE_OBJECT
"<CMAKE_RC_COMPILER> <FLAGS> -O coff <DEFINES> -i <SOURCE> -o <OBJECT>")

Après avoir compilé le jeu, on peut lancer la commande cpack pour générer l'installateur. Notez que CPack utilise tout un tas de variables optionnelles ou non pour personnaliser l'installateur. Elles sont listées dans la doc de CMake, et voici celles que j'utilise personnellement :


set(CPACK_NSIS_DISPLAY_NAME ${SOSAGE_NAME})
set(CPACK_NSIS_PACKAGE_NAME ${SOSAGE_NAME})
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${SOSAGE_EXE_NAME})
set(CPACK_NSIS_URL_INFO_ABOUT ${SOSAGE_URL})
set(CPACK_NSIS_MUI_ICON "${SOSAGE_DATA_FOLDER}/resources/icon.ico")
set(CPACK_NSIS_CONTACT ${SOSAGE_AUTHOR})
set(CPACK_NSIS_WELCOME_TITLE "Welcome to the installer of ${SOSAGE_NAME}")
set(CPACK_NSIS_FINISH_TITLE "The installation ${SOSAGE_NAME} is now finised")
set(CPACK_NSIS_UNINSTALL_NAME "uninstall-${SOSAGE_EXE_NAME}")
set(CPACK_NSIS_CREATE_ICONS_EXTRA
    "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${SOSAGE_NAME}.lnk' '$INSTDIR\\\\${SOSAGE_EXE_NAME}.exe'"
)

Je fais la différence entre ${SOSAGE_EXE_NAME}, qui est la version « standardisée » qui sert à nommer l'exécutable, soit superfluous-returnz, et ${SOSAGE_NAME} qui est le nom écrit de manière plus classique Superfluous Returnz et est utilisé pour les raccourcis.

CPack nous produit un installer standard pour Windows sous forme d'un fichier .exe qui, lui, est bien auto-suffisant et peut être distribué tel quel pour une installation sur les ordinateurs des personnes qui veulent jouer !

Test sur machine virtuelle

Pouvoir cross-compiler depuis Gnunux, c'est super, mais à un moment donné, il va bien falloir tester le bousin, et là, on n'y coupe pas : il faut utiliser un système Windows. Comme je n'ai plus d'ordinateur tournant sur Windows depuis plus de 10 ans, je me suis tout naturellement penché sur les machines virtuelles.

Et ça tombe bien : Microsoft fournit des machines virtuelles Windows gratuites spécialement conçues pour le développement ! En l'occurrence, je préfère continuer le développement sur Gnunux, mais ça permet de facilement tester le jeu.

Il n'y a plus qu'à télécharger tout ça (20 Gio quand même, vous avez le temps de boire un café), à importer la VM dans VirtualBox, et c'est parti ! On y transfert ensuite l'installateur via la méthode son choix (un dossier partagé entre hôte et VM, par exemple), et on lance le tout.

L'installateur a une tête classique, peut-être un peu vieillotte, mais franchement, c'est pas bien grave :

Installer

On a bien nos raccourcis ajoutés dans le menu :

Menu

Et le jeu se lance !

Game

Débuguer

Bon, d'accord, là je me la raconte pour l'article, mais ça ne vous surprendra sans doute pas si je vous avoue que le jeu ne s'est pas lancé correctement dès le premier essai… oui, comme tout le reste, un portage, ça se teste et ça se débugue avant de fonctionner.

Alors oui, il est moins connu que celui de Gnunux, mais Windows a bien son propre émulateur de terminal pour récupérer les messages d'erreur. L'ennui, c'est que les sorties C++ standards std::cout et std::cerr ne s'y affichent pas… on ne voudrait quand même pas nous faciliter les choses.

À partir de là, vous pouvez soit regarder comment fonctionnent les IO console sur Windows… soit faire confiance à la SDL pour le faire à votre place et utiliser les fonctions cross-platform fournies, comme SDL_Log(). C'est ce que j'ai fait.

Rappelez-vous que le même problème s'était posé sur Android, on peut donc simplement ajouter une variante à notre fonction d'affichage de messages :


#if defined(SOSAGE_ANDROID)
inline void debug (const std::string& str)
{
  __android_log_print (ANDROID_LOG_INFO, "Sosage Info", "%s", str.c_str());
}
#elif defined(SOSAGE_WINDOWS)
inline void debug (const std::string& str)
{
  SDL_Log("%s", str().c_str());
}
#else
inline void debug (const std::string& str)
{
  std::cerr << str << std::endl;
}
#endif

En réalité, on pourrait utiliser SDL_Log() dans tous les cas, mais c'est aussi chouette de montrer comment gérer ça de différentes manières.

Adaptations spécifiques à Windows

Encore une fois, même si l'immense majorité de notre code va compiler et fonctionner tel quel sur Windows, il arrive que certaines portions nécessitent des adaptations. Tout comme j'avais du le faire pour Android, j'ai de nouveau dû adapter la section de code qui gère la langue. En effet, pour une raison ou une autre, Windows a décidé de ne pas se conformer à la norme de la bibliothèque standard std::locale (exorciste, où es-tu ?) et on doit donc faire appel à des fonctions de la bibliothèque Windows.

En pratique, ça donne ça :


#if defined(SOSAGE_ANDROID)
#  include <jni.h>
#elif defined(SOSAGE_WINDOWS)
#  include <wchar.h>
#  include <winnls.h>
#else
#  include <locale>
#endif

std::string get_locale()
{
#if defined(SOSAGE_ANDROID)
  JNIEnv* env = (JNIEnv*)SDL_AndroidGetJNIEnv();
  jobject activity = (jobject)SDL_AndroidGetActivity();
  jclass jni_class(env->GetObjectClass(activity));
  jmethodID method_id = env->GetStaticMethodID(jni_class, "getLocale", "()Ljava/lang/String;");
  jstring jstr = (jstring) env->CallStaticObjectMethod(jni_class, method_id);
  const char *str = env->GetStringUTFChars(jstr, 0);
  std::string out (str);
  env->ReleaseStringUTFChars(jstr, str);
  env->DeleteLocalRef(jstr);
  env->DeleteLocalRef(jni_class);
  return out;
#elif defined(SOSAGE_WINDOWS)
  wchar_t name[LOCALE_NAME_MAX_LENGTH];
  int l = GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH);
  if (l == 0)
    return "";
  std::wstring ws (name, name + l);
  return std::string(ws.begin(), ws.end());
#else
  return std::locale("").name();
#endif
}

Une autre petite subtilité qui m'a donné un peu de fil à retordre : la fameuse fonctionne GetUserDefaultLocaleName() n'est disponible qu'à partir d'une certaine version de Windows (en l'occurrence, Windows Vista) qui n'est pas activée par MinGW par défaut. On va donc devoir définir la variable WINVER avec le bon numéro de version lorsqu'on est sur Windows (on peut trouver la liste dans la doc de Microsoft) :


#if defined(_WIN32)
#define SOSAGE_WINDOWS
#define WINVER 0x0600
#endif

Une fois cette petite définition ajoutée, tout marche comme sur des roulettes !

Conclusion

Même si on a bien fini par devoir utiliser un système Windows pour la partie « test », la compilation croisée permet de gérer toute la partie développement/build sur Gnunux, et c'est un énorme avantage : ainsi, lorsque je veux tester une nouvelle version du code, je peux, d'un même script, lancer la compilation pour Gnunux, pour Android et pour Windows !

Et même… pour Mac. Oui, car je termine par cette petite annonce : j'ai réussi à faire un portage fonctionnel pour MacOS. Comment ? Eh bien, de l'exacte même manière : en cross-compilant depuis Gnunux. C'était un poil plus compliqué et moins bien documenté, mais c'est faisable. Je vous en cause dans un prochain article :)

i