Beaucoup de nos clients utilisent notre librairie de programmation Python pour leurs projets. Ce langage est très facile à prendre en main et est utilisable sur les 3 principaux OS. Cependant, distribuer une application est un peu plus compliqué, car il faut d'une part distribuer le code Python de l'application, mais aussi les dépendances ainsi que la machine virtuelle. Heureusement il existe un utilitaire open source, PyInstaller, qui permet de tout packer dans un exécutable. Cependant, pour l'utiliser avec notre librairie, il faut lui fournir quelques informations en plus.
PyInstaller parcourt un script Python et détermine tous les bibliothèques et modules qui sont nécessaires. Ensuite il regroupe le script de l'application, toutes les dépendances ainsi que l’interpréteur Python dans un même répertoire. Pour finir, un mini exécutable est créé, qui lance l’interpréteur de ce répertoire avec les bons paramètres pour utiliser uniquement le contenu de ce répertoire.
L'option --onefile permet de créer un seul exécutable plus gros, qui contient tout le contenu de ce répertoire. Lors de l’exécution, le contenu du sous-répertoire est dézippé dans un répertoire temporaire et l’application est lancée. Cette option est très pratique, car on n'a qu'un seul fichier à copier qui contient tout ce qui est nécessaire.
Le problème de la librairie Yoctopuce
PyInstaller a cependant une limitation, il n'arrive pas détecter l'utilisation des librairies dynamiques (.dll, .so, etc.) et en conséquence ne les inclut pas dans l’exécutable.
La librairie Yoctopuce utilise justement une librairie dynamique appelée "yapi". En temps normal, les différentes versions (pour les différents OS) de cette librairie se trouvent dans le sous-répertoire cdll et sont chargées lors de l’exécution. Avec PyInstaller, ce sous-répertoire n'est pas inclus et lors de l’exécution et l'application plante avec le message suivant:
C:\tmp\Examples\Doc-Inventory>dist\inventory.exe Traceback (most recent call last): File "inventory.py", line 22, in <module> if YAPI.RegisterHub("usb", errmsg) != YAPI.SUCCESS: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "yoctopuce\yocto_api.py", line 2705, in RegisterHub File "yoctopuce\yocto_api.py", line 2573, in InitAPI File "yoctopuce\yocto_api.py", line 1224, in yloadYapiCDLL ImportError: YAPI shared library is missing (yoctopuce\cdll\yapi64.dll), make sure it is available and accessible. [PYI-38744:ERROR] Failed to execute script 'inventory' due to unhandled exception! </module>
Il y a bien évidemment un moyen de corriger ce problème. Il faut utiliser l'option --add-data de PyInstaller pour forcer a inclure le répertoire cdll dans l’exécutable. L'argument de cette option doit se présenter sous la forme "source:dest_dir", où "source" est le fichier ou le répertoire à inclure, et "dest_dir" est le répertoire de destination par rapport à l'application. Les deux chemins sont séparés par un deux-points.
En fonction de comment vous avez téléchargé ou installé la librairie Yoctopuce, la valeur de ces deux paths est différente.
Si la librairie est installée avec pip
Comme expliqué dans la documentation et dans notre tutoriel, la manière d'inclure la librairie est différente. L'utilisation de notre package PyPi nécessite de préfixer les includes de "yoctopuce.". Il faudra donc que les librairies dynamiques de yapi soient stockées dans un répertoire yoctopuce/cdll
errmsg = YRefParam()
if YAPI.RegisterHub("usb", errmsg) != YAPI.SUCCESS:
sys.exit("init error" + str(errmsg))
print('Device list')
module = YModule.FirstModule()
while module is not None:
print(module.get_serialNumber() + ' (' + module.get_productName() + ')')
module = module.nextModule()
YAPI.FreeAPI()
Lorsque la librairie Yoctopuce est téléchargée avec l'utilitaire pip, on ne sait pas exactement où la librairie est stockée. Il est possible de retrouver ce répertoire avec la commande "show" de l'utilitaire pip.
Voilà les dernières lignes du résultat de la commande "pip show yoctopuce".
C:\tmp\>pip show yoctopuce ... ... distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Location: C:\Users\seb\AppData\Roaming\Python\Python312\site-packages Requires: Required-by:
L'attribut Location correspond au répertoire où tous les packages sont installés. Les libraires dynamiques "yapi" sont donc stockées dans le répertoire :
"C:\Users\seb\AppData\Roaming\Python\Python312\site-packages\yoctopuce\cdll". Le répertoire de destination est yoctopuce/cdll.
Avec toutes ces informations, la commande complète est la suivante:
pyinstaller --onefile --add-data \Users\seb\AppData\Roaming\Python\Python312\site-packages\yoctopuce\cdll:yoctopuce\cdll inventory.py
Cette commande crée un exécutable inventory.exe qui contient tout ce qu'il faut.
Si la librairie est installée manuellement
Si la librairie est téléchargée sur notre site web ou depuis GitHub, il n'est pas nécessaire de préfixer les imports de "yoctopuce.". Il suffit d'inclure yocto_api ou yocto_xxx en fonction des classes utilisées. Il faudra donc que les librairies dynamiques "yapi"" soient stockées dans un répertoire cdll
errmsg = YRefParam()
if YAPI.RegisterHub("usb", errmsg) != YAPI.SUCCESS:
sys.exit("init error" + str(errmsg))
print('Device list')
module = YModule.FirstModule()
while module is not None:
print(module.get_serialNumber() + ' (' + module.get_productName() + ')')
module = module.nextModule()
YAPI.FreeAPI()
Le paramètre --add-data est plus simple à déterminer, car on sait où on a installé la librairie Yoctopuce. Par exemple si on a dézippé la librairie dans le répertoire c:\tmp\yoctlib, le path du répertoire "cdll" est c:\tmp\yoctlib\Sources\cdll. Ce qui nous donnera l'option "--add-data c:\tmp\yoctlib\Sources\cdll/;cdll".
Notez que si vous modifiez dans votre script Python la variable sys.path pour ajouter la librairie Yoctopuce au search path (comme dans nos exemples), il est nécessaire d'utiliser l'option -p avec le path des fichiers sources Yoctopuce. En effet, PyInstaller n'est pas assez malin pour se rendre compte de cette astuce et il faut manuellement lui mettre à jour le search path.
Dans le cas présent, if faut utiliser -p c:\tmp\yoctlib\Sources. Cette option n'est pas nécessaire si les fichiers yocto_xxx.py sont dans search path de Python ou s'ils sont dans le répertoire courant, mais en cas de doute il vaut mieux la mettre systématiquement.
La commande finale est donc:
pyinstaller.exe --onefile -p :\tmp\yoctlib\Sources --add-data "c:\tmp\yoctlib\Sources/cdll/;cdll" inventory.py
Tout comme avec l'utilisation de pip, PyInstaller génère un exécutable inventory.exe qui inclut tout ce qui est nécessaire pour que le programme fonctionne.
Conclusion
PyInstaller facilite la distribution d'applications Python en regroupant le code, les dépendances et l'interpréteur dans un exécutable. Quand il est utilisé avec notre librairie de programmation, il faut ajouter l'option --add-data pour forcer l'inclusion de la librairie dynamique yapi.