Journal Retour d'expérience sur l'empaquetage d'une bibliothèque native pour Python

Posté par (page perso) . Licence CC by-sa.
23
26
mai
2019

Jonbour, Nal !

Je vais t'écrire au sujet de l'empaquetage d'une bibliothèque native pour Python, à travers un bref retour d'expérience un tant soit peu motivé, avec des choix pas évidents dedans et un petit vernis technologique pour attirer le chaland. Tu es ferré ? C'est parti.

Le compilateur Pythran est principalement distribué à travers PyPI. La majorité des dépendances sont des dépendances bien empaquetées sous PyPI (par exemple les paquet networkx ou numpy, mais il y a trois dépendances un peu complexes à gérer :

  1. Un compilateur C++ moderne (enfin, si on considère que le support de C++11 défini un compilateur moderne, auquel cas je suis au regret d'annoncer que ICC n'en fait pas partie.
  2. Quelques dépendances sur des fichiers d'en-tête, Boost et xsimd pour ne pas les nommer.
  3. Une implémentation des BLAS.

Le point 1 est à la charge de l'utilisateur, et c'est moins un problème en 2018 que cela ne l'était en 2013. Le point 2. est configurable lors de la construction du paquet : soit on repose sur la version système, soit on embarque une copie (minimale, pas tout Boost hein. L'outil bcp fait d'ailleurs ça très bien) dans le paquet Python, ce qui est le comportement par défaut, toujours pour garder la simplicité de déploiement.

Le point 3. est un vrai problème. Il existe plusieurs implémentations courantes des BLAS (la MKL d'Intel, OpenBLAS, Atlas…) mais je n'ai aucune garantie qu'elle soit présente sur le système. Il n'est pas très raisonnable d'embarquer les sources d'une des implémentations et espérer les recompiler lors de l'installation (c'est techniquement faisable, mais assez gourmand en ressources, une dizaine de minutes sur mon laptop).

Numpy, qui est déjà une dépendance du projet, embarque parfois une version compilée, liée dynamiquement, des OpenBLAS. Ce n'est malheureusement pas une garantie et je n'ai pas trouvé de moyen simple et portable d'obtenir un pointeur vers le chemin de cette bibliothèque depuis une installation de Numpy… (une inspection de /proc/<pid>/maps sous Linux ferait l'affaire, mais quel bazar ! Et puis niveau portabilité c'est pas le Graal.

Je me suis alors dit profitons des wheel Python pour livrer une version liée dynamiquement des OpenBLAS. Ça parait bien et on pourrait même imaginer que Numpy, SciPy etc se mettent à utiliser cette version. C'est d'ailleurs ce que fait le gestionnaire de paquet alternatif conda, pas mal prisé dans le monde du calcul scientifique en Python. Alors pourquoi pas pip ?

Cette solution n'est malheureusement pas satisfaisante dans le cadre de Python, ou alors je suis passé à côté d'un truc. Imaginons ce scenario :

  1. Bob compile sur la machine A le fichier alice.py en alice.so, en liant avec la libopenblas.so fournit par l'hypothétique paquet Python éponyme.
  2. Bob charge sur la machine A son module alice.so. Malheureusement libopenblas.so n'est pas dans un des chemins de recherche de l'application !
  3. Bob connait ses gammes, et utilises l'option -rpath de l'éditeur de lien pour enregistrer dans sa lib la localisation de la libopenblas.so. Malheureusement, il n'y a pas de mécanisme équivalent sous Windows !
  4. Bob s'en bas les steak de Windows et décide de distribuer son application avec un -rpath positionné. Drame, ses utilisateurs n'ont pas installés la libopenblas.so en utilisant la même arborescence de fichier que lui…

Ma conclusion après avoir (un peu !) étudié le sujet, c'est que le monde Python n'est pas fait pour empaqueter des bibliothèques partagées de la sorte. Soit on embarque les dépendances comme le fait Numpy, soit on fait confiance au système. Soit on crée un nouveau gestionnaire de paquet pour régler le problème (et on l'appelle conda). Notons que l'empaquetage d'exécutable pose moins de soucis. J'ai découvert que cmake est disponible via pip.

Comme je suis persévérant (personne ne persévère, car chacun a ses vers à soie), j'ai quand même fournit une version compilée statiquement des OpenBLAS, sous forme de wheel et sous le nom de pythran-openblas ce qui offre la possibilité au code Pythran d'être indépendant de son environnement --- cela reste une option, on peut choisir d'utiliser une lib système fournit par l'utilisateur.

C'est le meilleurs compromis que j'ai pu trouver : embarquer toutes les dépendances par défaut (sauf le compilateur :-) ! ) et laisser la possibilité à l'utilisateur averti de ne pas dupliquer les dépendances…

  • # rpath sous Windows

    Posté par (page perso) . Évalué à 3 (+1/-0). Dernière modification le 27/05/19 à 11:10.

    Bob connait ses gammes, et utilises l'option -rpath de l'éditeur de lien pour enregistrer dans sa lib la localisation de la libopenblas.so. Malheureusement, il n'y a pas de mécanisme équivalent sous Windows !

    Un article sur le sujet qui pourrait t'intéresser:
    http://nibblestew.blogspot.com/2019/05/emulating-rpath-on-windows-via-binary.html

    En gros on peut utiliser rpath sous Windows avec un bon gros hack en patchant le binaire généré. Ça a l'air crade dit comme ça, mais ça a l'air d'être pas mal au niveau compatibilité.

    • [^] # Re: rpath sous Windows

      Posté par (page perso) . Évalué à 3 (+1/-0).

      Dans les commentaires, je viens de voir qu'ils ont ouvert upstream une proposition pour la gestion de rpath:
      https://developercommunity.visualstudio.com/idea/566616/support-rpath-for-binaries-during-development.html

      Vote et soutiens-la si le sujet t'intéresse :)

      • [^] # Re: rpath sous Windows

        Posté par (page perso) . Évalué à 2 (+0/-0).

        Vote et soutiens-la si le sujet t'intéresse :)

        Merci ! J'avoue que mon investissement dans la communauté Windowsienne reste minimaliste hein. Je me trouve même déjà bien altruiste de faire tous ces efforts d'empaquetage !

        En vrai, comme évoqué dans le nourjal, rpath ne résout qu'une partie des problèmes, vu qu'il est positionné sur le code généré par Pythran, dont je ne maîtirse pas l'environnement d'exécution futur. Il est heureusement possible à l'utilisateur de Pythran de positionner le rpath lui même, ou d'aller faire un coup de xxd pour éditer le binaire final (mais beurk hein :-))

  • # Nix?

    Posté par (page perso) . Évalué à 6 (+4/-0).

    En gros, soit ton utilisateur va utiliser les packets de sa distrib pour installer ton projet, et là tu fais confiance aux packageurs de la distribution (cela peut être toi).

    Soit tu demandes à ton utilisateur d'installer des choses lui même, et quitte à ce qu'il install quelque chose à la main, autant que ce soit nix https://nixos.org/nix/ (ou guix) qui ensuite fera marcher ton projet de la manière que tu as décidé, sans erreur possible.

    Mon avis c'est que dés que tu commences à essayer de hacker un truc qui marche un peu près partout, tu complexifies ton projet et au final tu va aussi complexifier l'installation un peu près partout. Fournir une description de paquet avec nix permet de fournir:

    • Quelque chose qui marche partout, les utilisateurs pouvant installer nix partout (modulo windows: dans le subsystem for linux. Ce qui peut être un problème)

    • Un readme d'installation complet et testé. Pour ceux qui choisirons de ne pas installer nix, le fichier de configuration de nix peut être vu comme un readme d'installation exhaustif, tu listes absolument toutes les dépendances avec leur version ainsi que les étapes du build, et à jour, puisque tu t'en sers tous les jours pour travailler. Fini les readmes incomplet et obsolètes.

    • [^] # Re: Nix?

      Posté par (page perso) . Évalué à 3 (+1/-0).

      J'aime bien cette idée de fichier de conf nix qui sert de documentation de dépendances. D'une certaine façon, le .travis.yml et le .appveyor.yml jouent un rôle similaire.

      Je reste néanmoins persuadé qu'il est plus facile pour l'utilisateur de rentrer un simple pip install pythran et que ça juste marche :-/ C'est un peu le problème du standard de fait.

      Même ainsi, je me demande quelle doit être le mode par défaut : lier avec la lib blas système si elle est présente ? Les blas statiques empaquetées par mes soins ?

      • [^] # Re: Nix?

        Posté par (page perso) . Évalué à 2 (+0/-0).

        Même ainsi, je me demande quelle doit être le mode par défaut : lier avec la lib blas système si elle est présente ? Les blas statiques empaquetées par mes soins ?

        C'est là ou je ne veux plus avoir ce type de choix à faire. Au final tu vas complexifier ton système de build pour une heuristique qui risque de ne pas marcher, et quand elle ne marchera plus, tu la changeras en casant les cas où cela marchait déjà.

        De mon coté, je fournis des règles nix que les gens peuvent utiliser avec nix. Et si ils ne veulent pas de nix, ils peuvent suivre le "readme" fourni dans mon nix. Je garde un setup.py, makefile le plus simple du monde, qui peut donc être surchargé pour que l'utilisateur passe ses chemins maison. Si je met mon paquet dans pypi, soit il est full python, et pas de souci, soit je le garde simple et je précise les dépendances "systèmes" que l'utilisateur doit fournir (i.e. en lisant le .nix ;)

        Je reste néanmoins persuadé qu'il est plus facile pour l'utilisateur de rentrer un simple pip install pythran et que ça juste marche :-/ C'est un peu le problème du standard de fait.

        pip est le standard de fait pour la distribution de paquet python, et il fait cela très bien. Mais dés que les paquets sont plus complexes (Python + autre chose), pip ne répond plus du tout à la problématique.

  • # Evite pip

    Posté par (page perso) . Évalué à 6 (+6/-2). Dernière modification le 28/05/19 à 13:24.

    Simplement: Evite pip.

    Comme stipulé plus haut, utilise Nix ou similaire (GUIX, Spack, EasyBuild).

    Les packages managers langages spécifiques (pip, npm, gem, go-get ou cargo) sont des immondices dés que tu essaies d'ajouter du code natif (C,C++, fortran) à l'intérieur de ton package.

    Ils favorisent par ailleurs aussi ce que j'appelle le syndrome des "îles séparées": Tout est re-développé et réinventé 20 fois pour chaque langage car utiliser dans un logiciel développé dans un langage tiers est horriblement pénible.

    • [^] # Re: Evite pip

      Posté par (page perso) . Évalué à 6 (+4/-0).

      J'aurais tendance à donner l'avis exactement contraire et d'utiliser pip malgré ses défauts.

      Tout développeur Python est censé être habitué aux outils Python, alors qu'il y a de fortes chances qu'ils ne connaissent pas Guix ou équivalent.

      • [^] # Re: Evite pip

        Posté par . Évalué à 2 (+0/-0).

        Guix c'est du scheme donc au revoir et niveau portabilité ben voila quoi c'est pas l'outil pour!

    • [^] # Re: Evite pip

      Posté par (page perso) . Évalué à 5 (+3/-0).

      Sauf que l'usage de ces outils de packaging est énormément plus développé que Nix/guix… et que ce sont les trucs vers lesquels les gens vont naturellement (et qui pour 98%¹ des logiciels font bien le boulot).

      ¹ Louche pifométrique personnelle.

      Python 3 - Apprendre à programmer en Python avec PyZo et Jupyter Notebook → https://www.dunod.com/sciences-techniques/python-3

    • [^] # Re: Evite pip

      Posté par . Évalué à 4 (+1/-0). Dernière modification le 29/05/19 à 18:19.

      si tu trouves que pip est une immondice dans ce cas, alors n'essai jamais conda et anaconda tu va avoir une crise cardiaque ! Le moment que je préfère c'est : /chemin_conda = 1,9G, conda remove paquet = /chemin_conda = 2G (et on jette un voile pudique sur le solveur qui n'installe pas toujours les mêmes éléments sur un env strictement identique avec une gestion des prio de dépôts strictement identique et une demande d'installation strictement identique : si, si, c'est possible :p )
      J'essai de convaincre que conda c'est pas production-compliant, même pas assez fiable pour un env de dev, juste une aide pour les poc. Perso c'est "no way".

  • # Comme psycopg2

    Posté par (page perso) . Évalué à 4 (+2/-0).

    Salut,

    Tu touche la limite de la distribution de binaire avec pip et wheel. En effet, pip ne peut pas garantir une dépendance dans un autre système de dépendance. Un paquet debian python-pouet dépose un poue.egg-info pour indiquer à pip sa présence. Ce n'est le cas d'openblas.

    Si tu veux un projet vraiment portable, as-tu penser à détecter et charger à l'exécution la bibliothèque disponible ?

    Ainsi tu n'a ni le problème d'embarquer les dépendances, ni le problème d'avoir une erreur dégueulasse de ld.

    Psycopg2 a eut ce problème et a fait comme toi. Un paquet psycopg2 source. Le paquet binaire embarque libpq et est disponible sous un autre nom : psycopg2-binary.

    • [^] # Re: Comme psycopg2

      Posté par (page perso) . Évalué à 2 (+0/-0).

      Cela a tout de même ses limites : tu ne peux pas dire (comme avec apt) que le projet psycopg2-binary fournit le package psycopg :(

      Ou alors il faut mettre psycopg comme dépendance de psycopg-binary (alors que c'est le contraire d'un point de vue logique).

      Après, comme le packaging Python utilise un fichier setup.py (script Python comme un autre), tu peux déclarer des dépendances dynamiquement : par exemple, setup.py peut tester le système sur lequel le paquet est en cours d'installation et va déclarer des dépendances en fonction de la version de Python, des libs déjà installées, etc.

      Cet avantage vient avec son inconvénient miroir : tu ne peux pas faire de l'analyse statique pour déterminer les dépendances (alors qu'avec un paquet apt, il suffit de lire un fichier de contrôle pour avoir toutes les dépendances).

      • [^] # Re: Comme psycopg2

        Posté par (page perso) . Évalué à 2 (+0/-0).

        Oui, psycopg2 ou psycopg2-binary doit être laissé au choix par l'utilisateur. Sur mes projets, je passe la gestion de la dépendances psycopg2 au runtime ou au paquet de distribution, et je document pip install monprojet psycopg2-binary.

        setup.py n'est valable que pour l'installation depuis les sources et pas pour le wheel. Or on parle ici de déployer des paquets précompilés.

  • # Europython2003

    Posté par (page perso) . Évalué à 2 (+2/-1).

    Europython2003, je fais un lightning talk pour proposer un package manager pour python s'inspirant de debian, Guido me reponds du fin fonds de l'auditoire qu'il y a deja Pypi, je lui reponds que ca ne marche pas pour les librairies C et autres. 15 ans plus tard, le probleme est toujours la. C'est beau le logiciel libre, heureusement qu'il y a des dictateurs benevoles…

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.