Blog
Compilation croisée Gnunux vers MacOS
La dernière fois, je vous avais expliquer comment compiler et distribuer un jeu pour Windows depuis Gnunux. Cette fois, on va faire la même chose, mais avec MacOS comme système cible, une procédure un peu moins bien documentée sur Internet.
OSXCross
Là où MinGW nous permettait de compiler des programmes Windows depuis Gnunux, c'est un logiciel du nom de OSXCross qui va nous permettre de faire de même pour des programmes MacOS. Contrairement à MinGW, ce dernier n'est pas inclus dans les dépôts des distributions usuelles, et on va donc devoir l'installer à la main.
En premier lieu, il faut télécharger XCode sur le site officiel, ce qui requiert de créer un compte (groumpf, mais passons). Attention, le fichier est volumineux (dans les 10 Gio), ne vous plantez donc pas de version (vérifiez bien qui est compatible avec quoi, entre OSXCross, XCode et le MacOS le plus ancien que vous voulez supporter). Personnellement, j'utilise XCode 11, qui permet de supporter MacOS Catalina.
Ensuite, il suffit de suivre la procédure décrite sur le Github de
OSXCross, on package le
SDK, on le déplace dans le dossier tarballs
, etc.
On compile OSXCross tout simplement avec build.sh
.
On prend ensuite soin d'ajouter le chemin des exécutables installés,
personnellement j'ai mis tout ça dans /home/gee/local/osxcross
. On
précise également la version cible de MacOS, tout cela dans notre
.bashrc
, .zshrc.
ou autre :
export MACOSX_DEPLOYMENT_TARGET=10.15
export PATH=/home/gee/local/osxcross/bin:$PATH
Si vous jetez un œil au contenu de ${INSTALL_PATH}/bin
, vous
trouverez des choses qui ressemblent beaucoup à ce qu'on avait avec
MinGW :
x86_64-apple-darwin19-clang++
pour compiler du C++ pour une architecture 64 bitsx86_64-apple-darwin19-clang
pareil pour du Cx86_64-apple-darwin19-otool
qui permet d'afficher les objets et bibliothèques utilisées par un exécutable (on va en rappeler plus bas)
Bref, des outils de développement MacOS mais sur votre Gnunux !
Bibliothèques tierces
Rappelons à nouveau que Superflu Riteurnz repose sur 3 bibliothèques tierces :
Ces bibliothèques fonctionnent bien sur Gnunux, Windows… et MacOS. Pour les installer, OSXCross fournit un portage des Macports, et on peut donc très simplement faire :
$ osxcross-macports install libsdl2 libsdl2_image libsdl2_mixer libsdl2_ttf libyaml lz4
Les bibliothèques en question sont installées avec l'arborescence
habituelle (lib
, include
, etc.) au sein du dossier
{INSTALL_PATH}/macports/pkgs/opt/local/
.
Configuration & compilation
Encore une fois, ça va vous rappeler l'article sur MinGW, puisqu'on va utiliser un fichier de toolchain CMake :
set(CMAKE_SYSTEM_NAME Darwin)
set(TOOLCHAIN_PREFIX x86_64-apple-darwin19)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-clang)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-clang++)
set(CMAKE_C_COMPILER_AR ${TOOLCHAIN_PREFIX}-ar)
set(CMAKE_CXX_COMPILER_AR ${TOOLCHAIN_PREFIX}-ar)
set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}-ld)
set(CMAKE_FIND_ROOT_PATH /home/gee/local/osxcross/macports/pkgs/opt/local/)
set(CMAKE_OSX_SYSROOT /home/gee/local/osxcross/SDK/MacOSX10.15.sdk/)
set(CMAKE_REQUIRED_MAC_LIBS "libyaml-0.2.dylib;libSDL2-2.0.0.dylib;libSDL2_image-2.0.0.dylib;libSDL2_ttf-2.0.0.dylib;libSDL2_mixer-2.0.0.dylib;liblz4.1.dylib;liblz4.1.9.3.dylib;libpng16.16.dylib;libjpeg.8.dylib;libjpeg.8.2.2.dylib;libtiff.5.dylib;libz.1.dylib;libz.1.2.11.dylib;libwebp.7.dylib;libmodplug.1.dylib;libvorbisfile.3.dylib;libvorbis.0.dylib;libFLAC.8.dylib;libmpg123.0.dylib;libopusfile.0.dylib;libzstd.1.dylib;libzstd.1.5.2.dylib;liblzma.5.dylib;libogg.0.dylib;libopus.0.dylib")
La variable CMAKE_REQUIRED_MAC_LIBS
remplie à la main, je n'en suis
pas vraiment satisfait, mais je n'ai pas trouvé mieux : il s'agit de
toutes les bibliothèques nécessaires pour faire tourner
l'exécutable. Pour les trouver, il suffit d'utiliser la commande
x86_64-apple-darwin19-otool
sur l'exécutable, de regarder quelles
bibliothèques s'affichent, et de recommencer sur chaque bibliothèque
jusqu'à avoir fait le tour. Oui, ça se scripte bien aussi.
Ensuite, il suffit de lancer CMake avec ce fichier toolchain, puis
on compile normalement avec make
, ce qui nous donne un exécutable
superfluous-returnz
compatible MacOS !
Installateur fait maison
Vous connaissez la rengaine : un exécutable, c'est bien joli, mais ça ne suffit pas pour partager un jeu. Pour Windows, on avait pu profiter du générateur CPack NSIS, malheureusement le générateur « classique » DragNDrop pour MacOS n'est disponible… que sur MacOS. Qu'à cela ne tienne : on va s'en occuper à l'ancienne, à la main !
Pas mal d'applis sur MacOS se distribuent sous la forme d'un fichier
DMG, qui correspond en gros à un dossier finissant en .app
avec une
certaine arborescence normalisée qu'on va compresser. L'arborescence
qu'on va utiliser nous ressemble à ça :
superfluous-returnz.app
└── Contents
├── Info.plist # Liste de propriétés
├── MacOS
│ └── superfluous-returnz # L'exécutable
└── Resources
├── data
| ├── # Les données du jeu
│ └── # (...)
├── icon.icns # L'icone de l'exécutable
└── libs
├── # Les bibliothèques tierces
└── # (...)
L'icône est sous le format .icns
: on peut facilement convertir un
PNG en ICNS avec le programme png2icns
disponible dans le paquet
icnsutils
(sur Debian et dérivées, mais j'imagine que c'est aussi
dispo dans les autres distributions). Le fichier Info.plist
est un
fichier au format XML qui contient un ensemble de propriétées :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>superfluous-returnz</string>
<key>CFBundleDisplayName</key>
<string>Superfluous Returnz</string>
<key>CFBundleIdentifier</key>
<string>net.ptilouk.superfluous</string>
<key>CFBundleVersion</key>
<string>1.1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>sflu</string>
<key>CFBundleExecutable</key>
<string>superfluous-returnz</string>
<key>CFBundleIconFile</key>
<string>icon</string>
</dict>
</plist>
Même si le générateur CPack pour créer un DMG n'est pas dispo sur Gnunux, on peut malgré tout indiquer une procédure d'installation qui génère l'arborescence voulue via CMake :
if (CMAKE_SYSTEM_NAME STREQUAL Darwin)
target_compile_options(${SOSAGE_EXE_NAME} PUBLIC -DSOSAGE_INSTALL_DATA_FOLDER="data/")
install(TARGETS ${SOSAGE_EXE_NAME} DESTINATION "${SOSAGE_EXE_NAME}.app/Contents/MacOS")
install(DIRECTORY ${SOSAGE_DATA_FOLDER}/data/ DESTINATION "${SOSAGE_EXE_NAME}.app/Contents/Resources/data")
foreach(lib ${CMAKE_REQUIRED_MAC_LIBS})
install(FILES ${CMAKE_FIND_ROOT_PATH}/lib/${lib} DESTINATION "${SOSAGE_EXE_NAME}.app/Contents/libs/")
endforeach()
install(FILES "${SOSAGE_DATA_FOLDER}/resources/Info.plist" DESTINATION "${SOSAGE_EXE_NAME}.app/Contents/")
install(FILES "${SOSAGE_DATA_FOLDER}/resources/icon.icns" DESTINATION "${SOSAGE_EXE_NAME}.app/Contents/Resources")
endif()
On voit ici l'intérêt d'avoir listé les bibliothèques nécessaires dans
la variable CMAKE_REQUIRED_MAC_LIBS
.
Là, on se dit qu'il devrait suffire de compresser ce dossier au format
DMG pour avoir notre installateur… mais bien sûr, ce n'est pas aussi
simple. Le nœud du problème, c'est que votre programme est linké au
bibliothèques tierces avec un chemin absolu qui pointe sur les
fichiers .dylib
de votre système… sauf que bien sûr, on voudrait
pointer sur ceux fournis dans le DMG !
J'ai essayé pas mal de méthodes pour faire ça via CMake mais je ne
suis arrivé à rien de concluant, j'ai donc fini par faire un script
qui utilise les outils fournis par OSXCross : otool
pour analyser
les fichiers binaires, et install_name_tool
pour changer le lien
pointé.
En pratique, on va simplement vouloir remplacer des liens au format
/opt/local/lib/[library.dylib]
par un format
@executable_path/../libs/[library.dylib]
(@executable_path
spécifiant l'endroit où se trouve l'exécutable, comme vous l'avez sans
doute compris). J'ai donc bricolé un script Python qu'il suffit
d'appeler sur l'exécutable ainsi que tous les fichiers dylib
et qui
s'occupe du bouzin :
import subprocess
import sys
for exe in sys.argv[1:]:
libs = subprocess.check_output('x86_64-apple-darwin19-otool -L ' + exe, shell=True).decode().split('\n')
for l in libs:
if '/opt/local/lib/' in l:
lname = l.split('/opt/local/lib/')[1].split(' ')[0]
old = '/opt/local/lib/' + lname
new = '@executable_path/../libs/' + lname
subprocess.run('x86_64-apple-darwin19-install_name_tool -change "' + old + '" "' + new + '" ' + exe, shell=True, check=True)
C'est un peu crade, mais bon… ça marche.
Ensuite, tout est prêt pour générer notre fichier DMG, ce qu'on peut
faire très simplement avec la commande genisoimage
:
$ genisoimage -V superfluous-returnz.app -D -R -apple -no-pad -o superfluous-returnz.dmg install
Idéalement, pour faire un véritable installateur « drag'n'drop » comme les mac-users en ont l'habitude, il faudrait ajouter un fond d'écran et placer l'icone du jeu à côté d'un raccourci vers le dossier « Applications » de MacOS en invitant à déposer le premier sur le second. C'est par exemple ce que fait le jeu Supertux :
Malheureusement, je n'ai pas trouvé de façon élégante de faire ça sans
passer par des manipulations « à la main » sur un MacOS en
copiant-collant le fichier .DS_store
du répertoire… du coup j'ai
préféré laisser tomber cette étape. Je suppose que les gens sous MacOS
ont suffisamment l'habitude pour comprendre qu'il faut copier le jeu
dans « Applications » pour réellement l'installer sur la machine.
Test sur machine virtuelle
Contrairement à Microsoft, Apple ne propose pas de machine virtuelle
clef en main de son système d'exploitation : fort heureusement, un
script nommé
macos-virtualbox
est disponible pour générer une VM depuis les fichiers d'installation
d'Apple. C'est un peu plus fastidieux que pour Windows donc, mais rien
d'insurmontable, il suffit de suivre le README du projet.
Une fois notre machine virtuelle configurée, on peut transférer le jeu, le lancer et le débuguer si besoin !
Gros avantage de MacOS sur Windows : les sorties C++ standards
std::cout
et std::cerr
fonctionnent directement dans le terminal
comme sur Gnunux, il n'y a donc pas besoin d'adapter le code. En fait,
j'ai beau avoir défini une macro MAC
comme pour les autre systèmes
d'exploitation :
#if defined(__ANDROID__)
#define SOSAGE_ANDROID
#elif defined(__APPLE__)
#define SOSAGE_MAC
#elif defined(_WIN32)
#define SOSAGE_WINDOWS
#define WINVER 0x0600 // Enable use of locale functions
#elif defined(__linux__)
#define SOSAGE_GNUNUX
#endif
… je dois bien avouer que je n'ai jamais eu besoin de l'utiliser jusqu'à maintenant, ce qui est plutôt chouette ! Moins on a de code spécifique à chaque OS, moins c'est le bazar à maintenir.
Conclusion
La compilation croisée de Gnunux vers MacOS demande définitivement plus d'efforts que celle vers Windows : l'absence de logiciel dédié dans les dépôts des distributions Gnunux usuelles et l'abscence de générateur CPack pour MacOS sur Gnunux sont les deux points noirs.
Malgré tout, avec un peu de patience et pas mal d'espace disque, on peut y arriver ! Un énorme merci aux personnes derrière les logiciels OSXCross et macos-virtualbox sans qui tout cela serait sans doute largement plus compliqué.