zipapp — Gestion des archives zip exécutables Python

Nouveau dans la version 3.5.

Code source : Lib/zipapp.py


Ce module fournit des outils pour gérer la création de fichiers zip contenant du code Python, qui peuvent être exécutés directement par l'interpréteur Python. Le module fournit à la fois une interface de ligne de commande Interface en ligne de commande et une interface API Python.

Exemple de base

L'exemple suivant montre comment l'interface de ligne de commande Interface en ligne de commande peut être utilisée pour créer une archive exécutable depuis un répertoire contenant du code Python. Lors de l'exécution, l'archive exécutera la fonction main du module myapp dans l'archive.

$ python -m zipapp myapp -m "myapp:main"
$ python myapp.pyz
<output from myapp>

Interface en ligne de commande

Lorsqu'il est appelé en tant que programme à partir de la ligne de commande, la syntaxe suivante est utilisée :

$ python -m zipapp source [options]

Si source est un répertoire, une archive est créée à partir du contenu de source. Si source est un fichier, ce doit être une archive et il est copié dans l'archive cible (ou le contenu de sa ligne shebang est affiché si l'option --info est indiquée).

Les options suivantes sont disponibles :

-o <output>, --output=<output>

Écrit la sortie dans un fichier nommé output. Si cette option n'est pas spécifiée, le nom du fichier de sortie sera le même que celui de l'entrée source, avec l'extension .pyz. Si un nom de fichier explicite est donné, il est utilisé tel quel (une extension .pyz doit donc être incluse si nécessaire).

Un nom de fichier de sortie doit être spécifié si la source est une archive (et, dans ce cas, la sortie ne doit pas être la même que la source).

-p <interpreter>, --python=<interpreter>

Ajoute une ligne #! à l'archive en spécifiant interpreter comme commande à exécuter. Aussi, sur un système POSIX, cela rend l'archive exécutable. Le comportement par défaut est de ne pas écrire la ligne #! et de ne pas rendre le fichier exécutable.

-m <mainfn>, --main=<mainfn>

Écrit un fichier __main__.py dans l'archive qui exécute mainfn. L'argument mainfn est de la forme « pkg.mod:fn », où « pkg.mod » est un paquet/module dans l'archive, et « fn » est un appelable dans le module donné. Le fichier __main__.py réalise cet appel.

--main ne peut pas être spécifié lors de la copie d'une archive.

-c, --compress

Compresse les fichiers avec la méthode deflate, réduisant ainsi la taille du fichier de sortie. Par défaut, les fichiers sont stockés non compressés dans l'archive.

--compress n'a aucun effet lors de la copie d'une archive.

Nouveau dans la version 3.7.

--info

Affiche l'interpréteur intégré dans l'archive, à des fins de diagnostic. Dans ce cas, toutes les autres options sont ignorées et SOURCE doit être une archive et non un répertoire.

-h, --help

Affiche un court message d'aide et quitte.

API Python

Ce module définit deux fonctions utilitaires :

zipapp.create_archive(source, target=None, interpreter=None, main=None, filter=None, compressed=False)

Crée une archive d'application à partir de source. La source peut être de natures suivantes :

  • Le nom d'un répertoire, ou un path-like object se référant à un répertoire ; dans ce cas, une nouvelle archive d'application sera créée à partir du contenu de ce répertoire.

  • Le nom d'un fichier d'archive d'application existant, ou un path-like object se référant à un tel fichier ; dans ce cas, le fichier est copié sur la cible (en le modifiant pour refléter la valeur donnée à l'argument interpreter). Le nom du fichier doit inclure l'extension .pyz, si nécessaire.

  • Un objet fichier ouvert pour la lecture en mode binaire. Le contenu du fichier doit être une archive d'application et Python suppose que l'objet fichier est positionné au début de l'archive.

L'argument target détermine où l'archive résultante sera écrite :

  • S'il s'agit d'un nom de fichier, ou d'un path-like object, l'archive sera écrite dans ce fichier.

  • S'il s'agit d'un objet fichier ouvert, l'archive sera écrite dans cet objet fichier, qui doit être ouvert pour l'écriture en mode octets.

  • Si la cible est omise (ou None), la source doit être un répertoire et la cible sera un fichier portant le même nom que la source, avec une extension .pyz ajoutée.

L'argument interpreter spécifie le nom de l'interpréteur Python avec lequel l'archive sera exécutée. Il est écrit dans une ligne shebang au début de l'archive. Sur un système POSIX, cela est interprété par le système d'exploitation et, sur Windows, il sera géré par le lanceur Python. L'omission de l'interpreter n'entraîne pas l'écriture d'une ligne shebang. Si un interpréteur est spécifié et que la cible est un nom de fichier, le bit exécutable du fichier cible sera mis à 1.

L'argument main spécifie le nom d'un appelable, utilisé comme programme principal pour l'archive. Il ne peut être spécifié que si la source est un répertoire et si la source ne contient pas déjà un fichier __main__.py. L'argument main doit prendre la forme pkg.module:callable et l'archive sera exécutée en important pkg.module et en exécutant l'appelable donné sans argument. Omettre main est une erreur si la source est un répertoire et ne contient pas un fichier __main__.py car, dans ce cas, l'archive résultante ne serait pas exécutable.

L'argument optionnel filter spécifie une fonction de rappel à laquelle on passe un objet Path représentant le chemin du fichier à ajouter (par rapport au répertoire source). Elle doit renvoyer True si le fichier doit effectivement être ajouté.

L'argument optionnel compressed détermine si les fichiers doivent être compressés. S'il vaut True, les fichiers de l'archive sont compressés avec l'algorithme deflate ; sinon, les fichiers sont stockés non compressés. Cet argument n'a aucun effet lors de la copie d'une archive existante.

Si un objet fichier est spécifié pour source ou target, il est de la responsabilité de l'appelant de le fermer après avoir appelé create_archive.

Lors de la copie d'une archive existante, les objets fichier fournis n'ont besoin que des méthodes read et readline ou write. Lors de la création d'une archive à partir d'un répertoire, si la cible est un objet fichier, elle sera passée à la classe zipfile.ZipFile et devra fournir les méthodes nécessaires à cette classe.

Nouveau dans la version 3.7: Ajout des arguments filter et compressed.

zipapp.get_interpreter(archive)

Renvoie l'interpréteur spécifié dans la ligne #! au début de l'archive. S'il n'y a pas de ligne #!, renvoie None. L'argument archive peut être un nom de fichier ou un objet de type fichier ouvert à la lecture en mode binaire. Python suppose qu'il est au début de l'archive.

Exemples

Regroupe le contenu d'un répertoire dans une archive, puis l'exécute.

$ python -m zipapp myapp
$ python myapp.pyz
<output from myapp>

La même chose peut être faite en utilisant la fonction create_archive() :

>>> import zipapp
>>> zipapp.create_archive('myapp.pyz', 'myapp')

Pour rendre l'application directement exécutable sur un système POSIX, spécifiez un interpréteur à utiliser.

$ python -m zipapp myapp -p "/usr/bin/env python"
$ ./myapp.pyz
<output from myapp>

Pour remplacer la ligne shebang sur une archive existante, créez une archive modifiée en utilisant la fonction create_archive() :

>>> import zipapp
>>> zipapp.create_archive('old_archive.pyz', 'new_archive.pyz', '/usr/bin/python3')

Pour mettre à jour le fichier sans créer de copie locale, effectuez le remplacement en mémoire à l'aide d'un objet BytesIO, puis écrasez la source par la suite. Notez qu'il y a un risque lors de l'écrasement d'un fichier local qu'une erreur entraîne la perte du fichier original. Ce code ne protège pas contre de telles erreurs, assurez-vous de prendre les mesures nécessaires en production. De plus, cette méthode ne fonctionnera que si l'archive tient en mémoire :

>>> import zipapp
>>> import io
>>> temp = io.BytesIO()
>>> zipapp.create_archive('myapp.pyz', temp, '/usr/bin/python2')
>>> with open('myapp.pyz', 'wb') as f:
>>>     f.write(temp.getvalue())

Spécification de l'interprète

Notez que si vous spécifiez un interpréteur et que vous distribuez ensuite votre archive d'application, vous devez vous assurer que l'interpréteur utilisé est portable. Le lanceur Python pour Windows gère la plupart des formes courantes de la ligne POSIX #!, mais il y a d'autres problèmes à considérer :

  • Si vous utilisez /usr/bin/env python (ou d'autres formes de la commande python, comme /usr/bin/python), vous devez considérer que vos utilisateurs peuvent avoir Python 2 ou Python 3 par défaut, et écrire votre code pour fonctionner dans les deux versions.

  • Si vous utilisez une version explicite, par exemple /usr/bin/env python3 votre application ne fonctionnera pas pour les utilisateurs qui n'ont pas cette version. (C'est peut-être ce que vous voulez si vous n'avez pas rendu votre code compatible Python 2).

  • Il n'y a aucun moyen de dire « python X.Y ou supérieur » donc faites attention si vous utilisez une version exacte comme /usr/bin/env python3.4 car vous devrez changer votre ligne shebang pour les utilisateurs de Python 3.5, par exemple.

Normalement, vous devriez utiliser un /usr/bin/env python2 ou /usr/bin/env python3, selon que votre code soit écrit pour Python 2 ou 3.

Création d'applications autonomes avec zipapp

En utilisant le module zipapp, il est possible de créer des programmes Python qui peuvent être distribués à des utilisateurs finaux dont le seul pré-requis est d'avoir la bonne version de Python installée sur leur ordinateur. Pour y arriver, la clé est de regrouper toutes les dépendances de l'application dans l'archive avec le code source de l'application.

Les étapes pour créer une archive autonome sont les suivantes :

  1. Créez votre application dans un répertoire comme d'habitude, de manière à avoir un répertoire myapp contenant un fichier __main__.py et tout le code de l'application correspondante.

  2. Installez toutes les dépendances de votre application dans le répertoire myapp en utilisant pip :

    $ python -m pip install -r requirements.txt --target myapp
    

    (ceci suppose que vous ayez vos dépendances de projet dans un fichier requirements.txt — sinon vous pouvez simplement lister les dépendances manuellement sur la ligne de commande pip).

  3. Si nécessaire, supprimez les répertoires .dist-info créés par pip dans le répertoire myapp. Ceux-ci contiennent des métadonnées pour pip afin de gérer les paquets et, comme vous n'utiliserez plus pip, ils ne sont pas nécessaires (c'est sans conséquence si vous les laissez).

  4. Regroupez le tout à l'aide de :

    $ python -m zipapp -p "interpreter" myapp
    

Cela produira un exécutable autonome qui peut être exécuté sur n'importe quelle machine avec l'interpréteur approprié disponible. Voir Spécification de l'interprète pour plus de détails. Il peut être envoyé aux utilisateurs sous la forme d'un seul fichier.

Sous Unix, le fichier myapp.pyz est exécutable tel quel. Vous pouvez renommer le fichier pour supprimer l'extension .pyz si vous préférez un nom de commande « simple ». Sous Windows, le fichier myapp.pyz[w] est exécutable en vertu du fait que l'interpréteur Python est associé aux extensions de fichier .pyz et .pyzw une fois installé.

Création d'un exécutable Windows

Sous Windows, l'association de Python à l'extension .pyz est facultative et, de plus, il y a certains mécanismes qui ne reconnaissent pas les extensions enregistrées de manière « transparente » (l'exemple le plus simple est que subprocess.run(['myapp']) ne trouvera pas votre application — vous devez explicitement spécifier l'extension).

Sous Windows, il est donc souvent préférable de créer un exécutable à partir du zipapp. C'est relativement facile bien que cela nécessite un compilateur C. L'astuce repose sur le fait que les fichiers zip peuvent avoir des données arbitraires au début et les fichiers exe de Windows peuvent avoir des données arbitraires à la fin. Ainsi, en créant un lanceur approprié et en rajoutant le fichier .pyz à sa fin, vous obtenez un fichier unique qui exécute votre application.

Un lanceur approprié peut être aussi simple que ce qui suit :

#define Py_LIMITED_API 1
#include "Python.h"

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef WINDOWS
int WINAPI wWinMain(
    HINSTANCE hInstance,      /* handle to current instance */
    HINSTANCE hPrevInstance,  /* handle to previous instance */
    LPWSTR lpCmdLine,         /* pointer to command line */
    int nCmdShow              /* show state of window */
)
#else
int wmain()
#endif
{
    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    myargv[0] = __wargv[0];
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
    return Py_Main(__argc+1, myargv);
}

Si vous définissez le symbole du préprocesseur WINDOWS cela va générer un exécutable IUG, et sans lui, un exécutable console.

Pour compiler l'exécutable, vous pouvez soit simplement utiliser les outils standards en ligne de commande MSVC, soit profiter du fait que distutils sait comment compiler les sources Python:

>>> from distutils.ccompiler import new_compiler
>>> import distutils.sysconfig
>>> import sys
>>> import os
>>> from pathlib import Path

>>> def compile(src):
>>>     src = Path(src)
>>>     cc = new_compiler()
>>>     exe = src.stem
>>>     cc.add_include_dir(distutils.sysconfig.get_python_inc())
>>>     cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
>>>     # First the CLI executable
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe)
>>>     # Now the GUI executable
>>>     cc.define_macro('WINDOWS')
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe + 'w')

>>> if __name__ == "__main__":
>>>     compile("zastub.c")

Le lanceur résultant utilise le « Limited ABI » donc il fonctionnera sans changement avec n'importe quelle version de Python 3.x. Tout ce dont il a besoin est que Python (python3.dll) soit sur le PATH de l'utilisateur.

Pour une distribution entièrement autonome vous pouvez distribuer le lanceur avec votre application en fin de fichier, empaqueté avec la distribution embedded Python. Ceci fonctionnera sur n'importe quel ordinateur avec l'architecture appropriée (32 bits ou 64 bits).

Mises en garde

Il y a certaines limites à l'empaquetage de votre application dans un seul fichier. Dans la plupart des cas, si ce n'est tous, elles peuvent être traitées sans qu'il soit nécessaire d'apporter de modifications majeures à votre application.

  1. Si votre application dépend d'un paquet qui inclut une extension C, ce paquet ne peut pas être exécuté à partir d'un fichier zip (c'est une limitation du système d'exploitation, car le code exécutable doit être présent dans le système de fichiers pour que le lanceur de l'OS puisse le charger). Dans ce cas, vous pouvez exclure cette dépendance du fichier zip et, soit demander à vos utilisateurs de l'installer, soit la fournir avec votre fichier zip et ajouter du code à votre fichier __main__.py pour inclure le répertoire contenant le module décompressé dans sys.path. Dans ce cas, vous devrez vous assurer d'envoyer les binaires appropriés pour votre ou vos architecture(s) cible(s) (et éventuellement choisir la bonne version à ajouter à sys.path au moment de l'exécution, basée sur la machine de l'utilisateur).

  2. Si vous livrez un exécutable Windows comme décrit ci-dessus, vous devez vous assurer que vos utilisateurs ont python3.dll sur leur PATH (ce qui n'est pas le comportement par défaut de l'installateur) ou vous devez inclure la distribution intégrée dans votre application.

  3. Le lanceur suggéré ci-dessus utilise l'API d'intégration Python. Cela signifie que dans votre application sys.executable sera votre application et pas un interpréteur Python classique. Votre code et ses dépendances doivent être préparés à cette possibilité. Par exemple, si votre application utilise le module multiprocessing, elle devra appeler multiprocessing.set_executable() pour que le module sache où trouver l'interpréteur Python standard.

Le format d'archive d'application Zip Python

Python est capable d'exécuter des fichiers zip qui contiennent un fichier __main__.py depuis la version 2.6. Pour être exécutée par Python, une archive d'application doit simplement être un fichier zip standard contenant un fichier __main__.py qui sera exécuté comme point d'entrée de l'application. Comme d'habitude pour tout script Python, le parent du script (dans ce cas le fichier zip) sera placé sur sys.path et ainsi d'autres modules pourront être importés depuis le fichier zip.

Le format de fichier zip permet d'ajouter des données arbitraires à un fichier zip. Le format de l'application zip utilise cette possibilité pour préfixer une ligne shebang POSIX standard dans le fichier (#!/path/to/interpreter).

Formellement, le format d'application zip de Python est donc :

  1. Une ligne shebang facultative, contenant les caractères b'#! suivis d'un nom d’interpréteur, puis un caractère fin de ligne (b'\n'). Le nom de l'interpréteur peut être n'importe quoi acceptable pour le traitement shebang de l'OS, ou le lanceur Python sous Windows. L'interpréteur doit être encodé en UTF-8 sous Windows, et en sys.getfilesystemencoding() sur POSIX.

  2. Des données zipfile standards, telles que générées par le module zipfile. Le contenu du fichier zip doit inclure un fichier appelé __main__.py (qui doit se trouver à la racine du fichier zip — c'est-à-dire qu'il ne peut se trouver dans un sous-répertoire). Les données du fichier zip peuvent être compressées ou non.

Si une archive d'application a une ligne shebang, elle peut avoir le bit exécutable activé sur les systèmes POSIX, pour lui permettre d'être exécutée directement.

Vous pouvez créer des archives d'applications sans utiliser les outils de ce module — le module existe pour faciliter les choses, mais les archives, créées par n'importe quel moyen tout en respectant le format ci-dessus, sont valides pour Python.