2.1- L’hypothèse Nuitka

Quelques mots de description tirés de la documentation de Nuitka:

Nuitka is a Python compiler written in Python. […]You feed it your Python app, it does a lot of clever things, and spits out an executable or extension module. […]It translates the Python into a C program that then is linked against libpython to execute in the same way as CPython does, in a very compatible way. It is somewhat faster than CPython already, but currently it doesn’t make all the optimizations possible, but a 312% factor on pystone is a good start (number is from version 0.6.0 and Python 2.7), and many times this is not generally achieved yet. —https://nuitka.net/pages/overview.html.

Cette alternative à l’interpréteur python peut donc produire des exécutables et ceux-ci pourraient (éventuellement) s’exécuter plus rapidement que dans l’interpréteur standard. Intéressant.

2.1.1- Configuration dédiée

Avertissement:

La configuration windows utilisée pour les builds d’OpenBLAS et de Numpy n’est pas compatible avec Nuitka (erreurs scons, powershell introuvable).

Documentation de Nuitka : https://nuitka.net/doc/user-manual.html

Sur une seconde machine (Conf_win_2) :

Configuration initiale: Windows 7 Edition Familiale Premium 64 bits SP1 (avec windows update complet)

Outil de compilation: Installation de MinGW-w64 avec l’installeur standalone.

Outil de cache: Installation de ccache-standalone depuis le binaire.

Python: installation standard de python 3.7.5 avec l’installeur.

Packages supplémentaires :

  • Packages de base :
    • Setuptools (41.2)
    • Pip (19.2.3)
  • Dépendances du jeu:
    • Pillow (6.2.1)
    • Pygame (1.9.6)
    • Wheel Numpy 1.19 + OpenBLAS

Et enfin Nuitka (0.6.7):

pip install -U nuitka

Variables d’environnement:

On crée une variable utilisateur pointant sur le compilateur gcc (64 bits dans notre cas):

"CC"="C:\\MinGW64\\mingw64\\bin\\gcc.exe"

Structure de fichiers:

C:/Workdir
└── LabPyProject
    ├── …
    ├── labpyproject
    │   ├── apps
    │   ├── core
    │   └── …
    └──standalonePygame.py

On va essayer de créer un exécutable de la version standalone Pygame du jeu avec Nuitka.

2.1.2- Freeze Nuitka « standard »

Dans une invite de commande windows.

On active le cache:

> set NUITKA_CCACHE_BINARY=C:\ccache\ccache.exe

On lance le freeze: voir http://manpages.ubuntu.com/manpages/trusty/man1/nuitka.1.html pour la description des paramètres.

> python -m nuitka --standalone --mingw64 --windows-disable-console --experimental=use_pefile --experimental=use_pefile_recurse --experimental=use_pefile_fullrecurse --recurse-all --recurse-stdlib --python-flag=no_site --enable-plugin=numpy=matplotlib --plugin-enable=qt-plugins --force-dll-dependency-cache-update --output-dir=C:\Workdir\LabPyProject\dist C:\Workdir\LabPyProject\standalonePygame.py

Après un long moment (la machine est ancienne et peu puissante), on obtient le résultat suivant:

C:/Workdir
└── LabPyProject
    ├── dist
    │   ├── standalonePygame.build
    │   └── standalonePygame.dist
    │       ├── …
    │       └── standalonePygame.exe
    └── …

Le dossier standalonePygame.dist constitue le résultat du freeze. Il contient l’exécutable ainsi que des éléments nécessaires à son exécution :

  • Des fichiers *.pyd
  • De nombreuses dll parmi lesquelles python37.dll
  • Quatre dossiers : lib2to3, numpy, PIL, pygame

Pour que l’application soit complète il faut néanmoins recopier les ressources graphiques (images, fonts) du jeu dans le dossier standalonePygame.dist en conservant leur arborescence, soit:

C:/Workdir
└── LabPyProject
    ├── …
    └── dist
        ├── standalonePygame.build
        └── standalonePygame.dist
            ├── …
            └── labpyproject
                └── apps
                    └── labpyrinthe
                        └── gui
                            ├── skinBase
                            │   └── rsc
                            └── skinPygame
                                └── rsc

Notre application est à présent complète. Les tests d’exécution donnent les résultats suivants :

  • L’exécutable produit par Nuitka est moins performant que le script lancé dans l’interpréteur python
  • L’application est portable, elle s’exécute sur la première machine windows (Conf_win_1) utilisée pour le build Numpy + OpenBLAS

Quelques remarques:

  • le dossier standalonePygame.dist pèse 169 Mo, les ressources graphiques représentant moins de 7 Mo
  • standalonePygame.exe pèse 59.8 Mo
  • la dll associée à OpenBLAS est présente sous le nom de ibopenblas.dll et libopenblas.dll les deux pesant chacune 41.3 Mo. Cette duplication semble liée au plugin Numpy de Nuitka.

2.1.3- Freeze Nuitka « hinted »

Nuitka offre une seconde approche optimisée pour la production d’exécutables. Via une démarche en deux passes, on obtient plus rapidement un résultat plus compact : https://github.com/Nuitka/NUITKA-Utilities/tree/master/hinted-compilation.

On copie les trois scripts du répertoire hinted_compilation dans notre projet :

Structure de fichiers:

C:/Workdir
└── LabPyProject
    ├── …
    ├── labpyproject
    │   ├── apps
    │   ├── core
    │   └── …
    ├── get-hints.py
    ├── hinted-mods.py
    ├── nuitka-hints.py
    └── standalonePygame.py

Première passe:

Dans une invite de commande windows:

> cd c:\Workdir\LabPyProject
> python get-hints.py standalonePygame.py

Cette première étape crée le fichier standalonePygame-37-win32-64.json dans le dossier LabPyProject.

Deuxième passe:

> cd c:\Workdir\LabPyProject
> python nuitka-hints.py --windows-disable-console –outputdir= C:\Workdir\LabPyProject\dist2 standalonePygame.py

Après 18 mn seulement de traitements on obtient :

C:/Workdir
└── LabPyProject
    ├── dist2
    │   ├── standalonePygame.build
    │   └── standalonePygame.dist2
    │       ├── …
    │       └── standalonePygame.exe
    └── …

Comme précédemment, on complète standalonePygame.dist2 avec les ressources graphiques nécessaires.

Les tests d’exécution sont similaires à ceux de la version standard (moins performants que dans l’interpréteur python).

Outre la durée de compilation bien plus brève, les différences résident dans une légère diminution du poids :

  • à présent standalonePygame.dist2 pèse 150 Mo
  • standalonePygame.exe pèse lui 35 Mo

2.1.4- Conclusion à propos de Nuitka

Les concepteurs de Nuitka indiquent que leur outil peut optimiser certaines constructions python lors de la compilation. A l’évidence, le code de labpyproject ne fait pas partie des formes les plus favorables.

Si on ne retient pas cet outil pour la distribution de ce projet, Nuitka n’en demeure pas moins un projet prometteur qui n’est pas arrivé encore au terme de ses développements (milestone 2 sur 6 cf https://nuitka.net/pages/overview.html).

2.2- La solution cx_Freeze

2.2.1- Installation

Sur la machine Conf_win_1 (ayant servi à compiler Numpy+OpenBLAS), dans notre installation standard de python 3.7.5 disposant des packages :

  • Packages de base :
    • Setuptools (42.0.2)
    • Pip (19.3.1)
    • Wheel (0.33.6)
    • Cython (0.29.14)
  • Dépendances du jeu:
    • Pillow (6.2.1)
    • Pygame (1.9.6)
    • Wheel Numpy 1.19 + OpenBLAS

On ajoute cx_Freeze via la commande (dans la console python):

>>> pip install cx-Freeze

2.2.2- Freeze de plusieurs exécutables avec cx_Freeze

Cx_Freeze permet de générer plusieurs exécutables très légers en une seule opération. Nous allons créer des exécutables pour la version standalone du jeu (pyGame), la version client (pyGame) et la version serveur (console).

La structure de fichiers est la suivante:

C:/Workdir
└── LabPyProject
    ├── …
    ├── labpyproject
    │   ├── apps
    │   ├── core
    │   └── …
    ├── dist
    │   └── pc
    │       └── cxf 
    ├── clientPygame.py
    ├── labpy_ico0001.ico
    ├── serverConsole.py
    ├── setup_cxf_pc.py
    ├── standalonePygame.py
    └── tcp_config.txt

La configuration du freeze se fait dans le fichier setup_cxf_pc.py (pour setup cx_Freeze sur pc), dont le contenu est le suivant:

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Python 3
"""
Setup CX_freeze pour windows
"""
import sys, os, glob
from cx_Freeze import setup, Executable
# 1- Ressources à inclure
root = ""
root_gui = root + "labpyproject\\apps\\labpyrinthe\\gui\\"
datas_dir_list = list()
# dirs de png :
for x in glob.glob(root_gui + "**\\*.png", recursive=True):
    relpath = os.path.relpath(x, start=root)
    reldir = os.path.dirname(relpath)
    if reldir not in datas_dir_list:
        datas_dir_list.append(reldir)
# dirs de fonts :
ttf_root = root_gui + "skinPygame\\rsc\\fonts\\"
for x in glob.glob(ttf_root + "**\\*.ttf", recursive=True):
    relpath = os.path.relpath(x, start=root)
    reldir = os.path.dirname(relpath)
    if reldir not in datas_dir_list:
        datas_dir_list.append(reldir)
# création de la liste de tupples décrivant les dirs de ressources à inclure
datas_files = [(r, r) for r in datas_dir_list]
# ajout du .txt de config reseau
datas_files.append(("tcp_config.txt", "tcp_config.txt"))
# 2- options
options = dict(
		zip_include_packages=["*"],
		zip_exclude_packages=[],
		include_files = datas_files,
		include_msvcr = True,
		optimize = 1,
		excludes = ["atomicwrites",
		            "curses",
		            "email",
		            "html",
		            "http",
		            "json",
		            "multiprocessing",
		            "pluggy",
		            "pydoc_data",
		            "tkinter.tk.demos",
		            "tkinter.tk.images",
		            ],
		        )
# 3- exe à générer
ex_std = Executable(
    script="standalonePygame.py",
    base='Win32GUI',
    icon="labpy_ico0001.ico"
    )
 ex_clt = Executable(
    script="clientPygame.py",
    base='Win32GUI',
    icon="labpy_ico0001.ico"
    )
ex_svr = Executable(
    script="serverConsole.py",
    base=None,
    icon="labpy_ico0001.ico"
    )
# 4- setup
setup(name='LabPyrinthe',
      version = '1.0',
      description = 'Python arcade maze game',
      author="Gabriel Leroy",
      options={"build_exe": options},
      executables=[ex_std, ex_clt, ex_svr])

Ce fichier de configuration comprend 4 parties :

  1. Recherche des ressources à inclure (png, ttf, txt). On utilise le package glob pour reconstituer l’arborescence des ressources (graphiques ou de configuration). Etape que l’on faisait manuellement avec Nuitka (mais qui aurait pu être automatisée).
  2. Options du freeze
    • zip_include_packages=["*"] permet de diviser par 2 le poids du dossier généré.
    • include_files = datas_files: inclusion des ressources recherchées au point 1
    • excludes = […]: ne génère qu’un gain faible (entre 1 et 2 Mo)
  3. Exécutables à créer. Création de trois objets Executable pour nos 3 applications à freezer. Les applications pygame ont pour base Win32GUI, l’application serveur exécutée en console a elle pour base None.
  4. Finalisation de la configuration (fonction setup) ajoutant quelques métadonnées.

Il ne reste plus qu’à lancer la compilation dans une console windows:

> cd C:\Workdir\LabPyProject
> python setup_cxf_pc.py build -b dist/pc/cxf

L’option –b permet d’indiquer le répertoire de destination du freeze, dont le résultat est:

C:/Workdir
└── LabPyProject
    ├── …
    └── dist
        └── pc
            └── cxf
                └── exe.win-amd64-3.7
                    ├── labpyproject
                    ├── lib
                    ├── clientPygame.exe
                    ├── python37.dll
                    ├── serverConsole.exe
                    ├── standalonePygame.exe
                    └── tcp_config.txt

Le dossier exe.win-amd64-3.7 constitue le contenu que nous souhaitons diffuser, avec:

  • labpyproject : dossier contenant l’arborescence de ressources graphiques
  • lib : dépendances constituées d’un dossier tkinter, de fichiers *.pyd, d’une archive library.zip et de dlls dont libopenblas.dll.
  • les trois exe pèsent de 28 à 31 ko, ils n’embarquent pas la dll python ni aucune autre dépendance.

Le dossier pèse 93.6 Mo.

Les exécutables fonctionnent de manière plus fluide que dans l’interpréteur python. Ils sont portables sur une autre machine de même architecture (windows 64 bits) et n’alarment pas l’antivirus. Nous avons trouvé notre solution de freeze.

2.3- Création d’un installeur pour Windows avec Inno Setup

Pour que notre application soit très simple à installer et désinstaller, nous allons utiliser le logiciel libre Inno Setup.

Installation :

Installer la dernière version stable proposée dans https://jrsoftware.org/isdl.php.

Structure de fichiers:

C:/Workdir
└── LabPyProject
    ├── …
    ├── labpyrinthe_innosetup_win64.iss
    ├── labpy_ico0001.ico
    └── dist
        └── pc
            ├── cxf
            │   └── exe.win-amd64-3.7
            │       ├── labpyproject
            │       ├── lib
            │       ├── clientPygame.exe
            │       ├── python37.dll
            │       ├── serverConsole.exe
            │       ├── standalonePygame.exe
            │       └── tcp_config.txt
            └── innosetup

Configuration:

A l’aide du wizard d’Inno Setup on crée le script labpyrinthe_innosetup_win64.iss que l’on particularise:

; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={7BA5F406-11D0-46ED-8D1D-54BA57871248}
AppName=LabPyrinthe
AppVersion=1.0
;AppVerName=LabPyrinthe 1.0
AppPublisher=Gabriel Leroy
AppPublisherURL=https://github.com/gab-python/labpyproject
AppSupportURL=https://github.com/gab-python/labpyproject
AppUpdatesURL=https://github.com/gab-python/labpyproject
DefaultDirName={autopf}\LabPyrinthe
DisableDirPage=yes
DefaultGroupName=LabPyrinthe
DisableProgramGroupPage=yes
; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check.
UsedUserAreasWarning=no
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
OutputDir=C:\Workdir\LabPyProject\dist\pc\innosetup
OutputBaseFilename=LabPyrinthe_win64_setup
SetupIconFile=C:\Workdir\LabPyProject\labpy_ico0001.ico
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
[Files]
Source: "C:\Workdir\LabPyProject\dist\pc\cxf\exe.win-amd64-3.7\standalonePygame.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Workdir\LabPyProject\dist\pc\cxf\exe.win-amd64-3.7\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\LabPyrinthe"; Filename: "{app}\standalonePygame.exe"
Name: "{group}\reseau\LabPyrinthe client"; Filename: "{app}\clientPygame.exe"
Name: "{group}\reseau\LabPyrinthe serveur"; Filename: "{app}\serverConsole.exe"
Name: "{group}\reseau\Config"; Filename: "{app}\tcp_config.txt"
Name: "{group}\{cm:UninstallProgram,LabPyrinthe}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\LabPyrinthe"; Filename: "{app}\standalonePygame.exe"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\LabPyrinthe"; Filename: "{app}\standalonePygame.exe"; Tasks: quicklaunchicon
[Run]
Filename: "{app}\standalonePygame.exe"; Description: "{cm:LaunchProgram,LabPyrinthe}"; Flags: nowait postinstall skipifsilent

On exécute ce script dans Inno Setup et on obtient le résultat suivant:

C:/Workdir
└── LabPyProject
    ├── …
    └── dist
        └── pc
           ├── cxf
           └── innosetup
               └── LabPyrinthe_win64_setup.exe

LabPyrinthe_win64_setup.exe est notre installeur du projet LabPyrinthe pour windows 64 bits. Il pèse 27.2 Mo (contre 93.6 Mo pour le dossier source généré par cx_Freeze).

Une fois exécuté, il crée l’entrée suivante dans la liste des programmes (sous Windows 7):

programmes

Remarque:

Sous Windows 10, le sous dossier reseau n’apparait pas, l’ensemble des raccourcis sont créés à la racine du dossier Labpyrinthe.

Nous disposons à présent d’un seul fichier à diffuser qui permet d’installer, exécuter et désinstaller très facilement notre application sur Windows 64 bits.

Installeur du jeu Labpyrinthe pour windows 64: Télécharger