Vous avez votre code écrit en Python. Vous en êtes fier et souhaitez le distribuer à d'autre personnes sans qu'elles aient besoin d'installer Python sur leur machine.

PyInstaller va nous permettre de faire ça, qu'il s'agisse d'un simple script, un module complexe ou une application entière.
Le résultat final sera un exécutable autonome prêt à être transmis. Bien entendu, l'exécutable ne sera pas multi-plateforme ; il sera fonctionnel pour les OS du même type que celui utilisé pour sa création.


Un simple script

Admettons que le script se nomme foo.py, la commande est on ne peut plus simple :

python -m PyInstaller foo.py

PyInstaller va alors faire sa tambouille et créer un sous dossier foo dans dist, càd dist/foo. Ce dossier contient tout le nécessaire pour fonctionner tout seul, sans avoir besoin d'installer quoi que ce soit d'autre. Dans ce sous dossier se trouve l'éxécutable généré : dist/foo/foo (ou dist\foo\foo.exe sous Windows).

Il ne reste plus qu'à envoyer ce dossier aux personnes désirant utiliser votre script.

Application complète

Admettons que vous ayez une application complexe qui utilise Qt et QML, multi-plateforme, j'ai nommé Nuxeo Drive.
Dans ce cas, il est judicieux (et même nécessaire, je dirai) d'avoir un fichier qui spécifie les paramètres de compilation pour PyInstaller. Il s'agit de fichier .spec.

Voci le contenu d'un tel fichier (ndrive.spec) :

# -*- mode: python -*-
# coding: utf-8

import io
import os
import os.path
import re
import sys


def get_version(init_file):
    """ Find the current version. """

    with io.open(init_file, encoding="utf-8") as handler:
        for line in handler.readlines():
            if line.startswith("__version__"):
                return re.findall(r"\"(.+)\"", line)[0]


cwd = os.getcwd()
tools = os.path.join(cwd, "tools")
nxdrive = os.path.join(cwd, "nxdrive")
data = os.path.join(nxdrive, "data")
icon = {
    "darwin": os.path.join(tools, "osx", "app_icon.icns"),
    "linux": os.path.join(tools, "linux", "app_icon.png"),
    "win32": os.path.join(tools, "windows", "app_icon.ico"),
}[sys.platform]

hiddenimports = [
    "sentry_sdk.integrations.argv",
    "sentry_sdk.integrations.atexit",
    "sentry_sdk.integrations.dedupe",
    "sentry_sdk.integrations.excepthook",
    "sentry_sdk.integrations.logging",
    "sentry_sdk.integrations.modules",
    "sentry_sdk.integrations.stdlib",
    "sentry_sdk.integrations.threading",
]
excludes = [
    # https://github.com/pyinstaller/pyinstaller/wiki/Recipe-remove-tkinter-tcl
    "FixTk",
    "tcl",
    "tk",
    "_tkinter",
    "tkinter",
    "Tkinter",
    # Misc
    "PIL",
    "ipdb",
    "lib2to3",
    "numpy",
    "pydev",
    "scipy",
    "yappi",
]

data = [(data, "data")]
version = get_version(os.path.join(nxdrive, "__init__.py"))
properties_rc = None

if sys.platform == "win32":
    # Set executable properties
    properties_tpl = tools + "\\windows\\properties_tpl.rc"
    properties_rc = tools + "\\windows\\properties.rc"
    if os.path.isfile(properties_rc):
        os.remove(properties_rc)

    version_tuple = tuple(map(int, version.split(".") + [0] * (3 - version.count("."))))

    with open(properties_tpl) as tpl, open(properties_rc, "w") as out:
        content = tpl.read().format(version=version, version_tuple=version_tuple)
        print(content)
        out.write(content)

    # Missing modules when packaged
    hiddenimports.append("win32timezone")

a = Analysis(
    [os.path.join(nxdrive, "__main__.py")],
    datas=data,
    excludes=excludes,
    hiddenimports=hiddenimports,
)

pyz = PYZ(a.pure, a.zipped_data)

exe = EXE(
    pyz,
    a.scripts,
    exclude_binaries=True,
    name="ndrive",
    console=False,
    debug=False,
    strip=False,
    upx=False,
    icon=icon,
    version=properties_rc,
)

coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, name="ndrive")

info_plist = {
    "CFBundleName": "NuxeoDrive",
    "CFBundleShortVersionString": version,
    "CFBundleURLTypes": {
        "CFBundleURLName": "org.nuxeo.nxdrive.direct-edit",
        "CFBundleTypeRole": "Editor",
        "CFBundleURLSchemes": ["nxdrive"],
    },
    "LSUIElement": True,  # Implies LSBackgroundOnly, no icon in the Dock
    "NSHighResolutionCapable": True,
}

app = BUNDLE(
    coll,
    name="Nuxeo Drive.app",
    icon=icon,
    info_plist=info_plist,
    bundle_identifier="org.nuxeo.drive",
)

Il fait plein de choses, comme par exemple :

  • inclure les modules que PyInstaller n'arrive pas à détecter lors de son analyse des sources ;
  • exclure certains modules qui ne sont pas nécessaire et grossirait le poids final de l'exécutable ;
  • définir une icône personnalisée suivant l'OS ;
  • définir les propriétés de l'exécutable sous Windows ;
  • généré un fichier Info.plist sur macOS, pratique pour spécifier le comportement de l'application ou ajouter la prise en charge d'URL personnalisée ;
  • et enfin, générer le .app sur macOS.

C'est peut-être un peu lourd et inapproprié pour votre cas particulier, mais cela donne une idée et des astuces si vous êtes dans le besoin.

Ensuite, la commande pour lancer la génération de l'exécutable automnome (+ création du .app sur macOS) :

python -m PyInstaller ndrive.spec --clean --noconfirm

--clean et --noconfirm permettent de supprimer les anciennes versions avant de commencer.

PyInstaller va alors faire sa tambouille et créer un sous dossier ndrive dans dist, càd dist/ndrive. Ce dossier contient tout le nécessaire pour fonctionner tout seul, sans avoir besoin d'installer quoi que ce soit d'autre. Dans ce sous dossier se trouve l'éxécutable généré : dist/ndrive/ndrive (ou dist\ndrive\ndrive.exe sous Windows). Ainsi que l'application sur macOS : dist/Nuxeo Drive.app.


Étapes suivantes :