Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 711 – PyBI: ein Standardformat für die Verteilung von Python-Binärdateien

Autor:
Nathaniel J. Smith <njs at pobox.com>
PEP-Delegate:
TODO
Discussions-To:
Discourse thread
Status:
Entwurf
Typ:
Standards Track
Thema:
Packaging
Erstellt:
06-Apr-2023
Post-History:
06-Apr-2023

Inhaltsverzeichnis

Zusammenfassung

„Wie Wheels, aber statt eines vorkompilierten Python-Pakets ist es ein vorkompilierter Python-Interpreter“

Motivation

Endziel: Pypi.org verfügt über vorkompilierte Pakete für alle Python-Versionen auf allen gängigen Plattformen, sodass automatisierte Tools sie leicht abrufen und einrichten können. Es wird schnell und einfach, Python-Vorabversionen auszuprobieren, Python-Versionen in CI festzulegen, eine temporäre Umgebung zu erstellen, um einen Bug-Report zu reproduzieren, der nur bei einer bestimmten Python-Punktversion auftritt, usw.

Erster Schritt (dieser PEP): Definieren eines Standard-Paketdateiformats zum Speichern vorkompilierter Python-Interpreter, das vorhandene Python-Paketstandards so weit wie möglich wiederverwendet.

Beispiele

Beispiel-Pybi-Builds sind unter pybi.vorpus.org verfügbar. Es handelt sich um Zip-Dateien, sodass Sie sie entpacken und im Inneren nachsehen können, wenn Sie sich ein Bild von ihrer Struktur machen möchten.

Sie können auch die Tools, die ich zu ihrer Erstellung verwendet habe, ansehen.

Spezifikation

Dateiname

Dateiname: {distribution}-{version}[-{build tag}]-{platform tag}.pybi

Dies entspricht dem in PEP 427 definierten Wheel-Dateiformat, mit Ausnahme des Weglassens des {python tag} und {abi tag} und der Änderung der Erweiterung von .whl zu .pybi.

Zum Beispiel:

  • cpython-3.9.3-manylinux_2014.pybi
  • cpython-3.10b2-win_amd64.pybi

Genau wie bei Wheels können Sie, wenn ein pybi mehrere Plattformen unterstützt, diese durch Punkte trennen, um eine „komprimierte Tag-Menge“ zu erstellen.

  • cpython-3.9.5-macosx_11_0_x86_64.macosx_11_0_arm64.pybi

(Obwohl dies in der Praxis wahrscheinlich nicht viel verwendet wird, wird der obige Dateiname beispielsweise idiomatischer als cpython-3.9.5-macosx_11_0_universal2.pybi geschrieben.)

Dateiinhalte

Eine .pybi-Datei ist eine Zip-Datei, die direkt an eine beliebige Stelle entpackt und dann als in sich geschlossene Python-Umgebung verwendet werden kann. Es gibt kein .data-Verzeichnis oder Installationsschema-Schlüssel, da die Python-Umgebung weiß, welches Installationsschema sie verwendet, und die Dinge daher von Anfang an an die richtigen Stellen legen kann.

Der Teil „beliebige Stelle“ ist wichtig: Der pybi darf keine fest codierten absoluten Pfade enthalten. Insbesondere dürfen vorinstallierte Skripte in ihren Shebang-Zeilen KEINE absoluten Pfade einbetten.

Ähnlich wie das Verzeichnis <package>-<version>.dist-info von Wheels muss das Pybi-Archiv ein Verzeichnis auf oberster Ebene namens pybi-info/ enthalten. (Begründung: Die Benennung als pybi-info statt dist-info stellt sicher, dass Tools nicht verwirrt werden, welche Art von Metadaten sie betrachten; das Weglassen des Teils {name}-{version} ist in Ordnung, da nur ein pybi in ein bestimmtes Verzeichnis installiert werden kann.) Das Verzeichnis pybi-info/ enthält mindestens die folgenden Dateien:

  • .../PYBI: Metadaten über das Archiv selbst, im selben RFC822-ähnlichen Format wie die Dateien METADATA und WHEEL.
    Pybi-Version: 1.0
    Generator: {name} {version}
    Tag: {platform tag}
    Tag: {another platform tag}
    Tag: {...and so on...}
    Build: 1   # optional
    
  • .../RECORD: wie bei Wheels, außer siehe Hinweis zu Symlinks unten.
  • .../METADATA: Im selben Format wie in der aktuellen Kernmetadatenspezifikation beschrieben, mit der Ausnahme, dass die folgenden Schlüssel verboten sind, da sie keinen Sinn ergeben:
    • Requires-Dist
    • Provides-Extra
    • Requires-Python

    Und außerdem gibt es einige neue, erforderliche Schlüssel, die unten beschrieben werden.

Pybi-spezifische Kernmetadaten

Hier ist ein Beispiel für die neuen METADATA-Felder, bevor wir die vollständigen Details angeben:

Pybi-Environment-Marker-Variables: {"implementation_name": "cpython", "implementation_version": "3.10.8", "os_name": "posix", "platform_machine": "x86_64", "platform_system": "Linux", "python_full_version": "3.10.8", "platform_python_implementation": "CPython", "python_version": "3.10", "sys_platform": "linux"}
Pybi-Paths: {"stdlib": "lib/python3.10", "platstdlib": "lib/python3.10", "purelib": "lib/python3.10/site-packages", "platlib": "lib/python3.10/site-packages", "include": "include/python3.10", "platinclude": "include/python3.10", "scripts": "bin", "data": "."}
Pybi-Wheel-Tag: cp310-cp310-PLATFORM
Pybi-Wheel-Tag: cp310-abi3-PLATFORM
Pybi-Wheel-Tag: cp310-none-PLATFORM
Pybi-Wheel-Tag: cp39-abi3-PLATFORM
Pybi-Wheel-Tag: cp38-abi3-PLATFORM
Pybi-Wheel-Tag: cp37-abi3-PLATFORM
Pybi-Wheel-Tag: cp36-abi3-PLATFORM
Pybi-Wheel-Tag: cp35-abi3-PLATFORM
Pybi-Wheel-Tag: cp34-abi3-PLATFORM
Pybi-Wheel-Tag: cp33-abi3-PLATFORM
Pybi-Wheel-Tag: cp32-abi3-PLATFORM
Pybi-Wheel-Tag: py310-none-PLATFORM
Pybi-Wheel-Tag: py3-none-PLATFORM
Pybi-Wheel-Tag: py39-none-PLATFORM
Pybi-Wheel-Tag: py38-none-PLATFORM
Pybi-Wheel-Tag: py37-none-PLATFORM
Pybi-Wheel-Tag: py36-none-PLATFORM
Pybi-Wheel-Tag: py35-none-PLATFORM
Pybi-Wheel-Tag: py34-none-PLATFORM
Pybi-Wheel-Tag: py33-none-PLATFORM
Pybi-Wheel-Tag: py32-none-PLATFORM
Pybi-Wheel-Tag: py31-none-PLATFORM
Pybi-Wheel-Tag: py30-none-PLATFORM
Pybi-Wheel-Tag: py310-none-any
Pybi-Wheel-Tag: py3-none-any
Pybi-Wheel-Tag: py39-none-any
Pybi-Wheel-Tag: py38-none-any
Pybi-Wheel-Tag: py37-none-any
Pybi-Wheel-Tag: py36-none-any
Pybi-Wheel-Tag: py35-none-any
Pybi-Wheel-Tag: py34-none-any
Pybi-Wheel-Tag: py33-none-any
Pybi-Wheel-Tag: py32-none-any
Pybi-Wheel-Tag: py31-none-any
Pybi-Wheel-Tag: py30-none-any

Spezifikation

  • Pybi-Environment-Marker-Variables: Der Wert aller PEP 508-Umgebungsmarker-Variablen, die über Installationen dieses Pybi hinweg statisch sind, als JSON-Dictionary. Zum Beispiel:
    • python_version wird immer vorhanden sein, da ein Python-3.10-Paket immer python_version == "3.10" hat.
    • platform_version wird im Allgemeinen nicht vorhanden sein, da es detaillierte Informationen über das Betriebssystem liefert, auf dem Python ausgeführt wird, zum Beispiel:
      #60-Ubuntu SMP Thu May 6 07:46:32 UTC 2021
      

      platform_release hat ähnliche Probleme.

    • platform_machine wird *meistens* vorhanden sein, mit Ausnahme von macOS Universal2-Pybis: Diese können potenziell im x86-64- oder im ARM64-Modus ausgeführt werden, und wir wissen nicht, welcher Modus bis zur tatsächlichen Ausführung des Interpreters gilt, daher können wir ihn nicht in statischen Metadaten aufzeichnen.

    Begründung: In vielen Fällen sollte dies einem Resolver, der unter Linux läuft, ermöglichen, Paket-Pins für eine Python-Umgebung unter Windows oder umgekehrt zu berechnen, solange der Resolver Zugriff auf die .pybi-Datei der Zielplattform hat. (Beachten Sie, dass Requires-Python-Beschränkungen durch Verwendung des python_full_version-Werts überprüft werden können.) Obwohl wir einige Schlüssel manchmal weglassen müssen, sind sie entweder ziemlich nutzlos (platform_version, platform_release) oder können vom Resolver rekonstruiert werden (platform_machine).

    Die Marker sind auch allgemein nützliche Informationen, auf die zugegriffen werden kann. Wenn Sie beispielsweise ein pypy3-7.3.2-Pybi haben und wissen möchten, welche Python-Sprachversion dies unterstützt, dann ist dies im python_version-Marker aufgezeichnet.

    (Hinweis: Möglicherweise möchten wir platform_version und platform_release deprecaten/entfernen? Sie sind problematisch und ich kann keine Fälle finden, in denen sie nützlich sind. Aber das liegt außerhalb des Umfangs dieser speziellen PEP.)

  • Pybi-Paths: Die für die Installation von Wheels benötigten Installationspfade (gleiche Schlüssel wie sysconfig.get_paths()), als relative Pfade ab dem Stammverzeichnis der Zip-Datei, als JSON-Dictionary.

    Diese Pfade MÜSSEN im Unix-Format mit Schrägstrichen als Trennzeichen und nicht mit Backslashes geschrieben werden.

    Es muss möglich sein, den Python-Interpreter aufzurufen, indem {paths["scripts"]}/python ausgeführt wird. Wenn alternative Interpreter-Einstiegspunkte vorhanden sind (z. B. pythonw für Windows-GUI-Anwendungen), sollten diese ebenfalls in diesem Verzeichnis unter ihren üblichen Namen ohne Versionsnummer aufgeführt sein. (Sie können *auch* einen python3.11-Symlink haben, wenn Sie möchten; es gibt keine Regel dagegen. Es ist nur so, dass python vorhanden und funktionsfähig sein muss.)

    Begründung: Pybi-Paths und Pybi-Wheel-Tags (siehe unten) reichen zusammen aus, damit ein Installer Wheels auswählen und sie in eine entpackte Pybi-Umgebung installieren kann, ohne Python aufrufen zu müssen. Außerdem müssen wir den Speicherort des Interpreters irgendwo festhalten, also ist das ein zweischneidiges Schwert.

  • Pybi-Wheel-Tag: Die von diesem Interpreter unterstützten Wheel-Tags in bevorzugter Reihenfolge (am meisten bevorzugt zuerst, am wenigsten bevorzugt zuletzt), mit der Ausnahme, dass der spezielle Plattform-Tag PLATFORM alle Plattform-Tags ersetzen sollte, die vom endgültigen Installationssystem abhängen.

    Diskussion: Es wäre schön™, wenn Installer die entsprechenden Wheel-Tags eines Pybis im Voraus berechnen könnten, damit sie Wheels in das entpackte Pybi installieren könnten, ohne den Python-Interpreter tatsächlich aufrufen zu müssen, um seine Tags abzufragen – sowohl aus Effizienzgründen als auch um exotischere Anwendungsfälle wie die Einrichtung einer Windows-Umgebung von einem Linux-Host aus zu ermöglichen.

    Aber leider ist es unmöglich, die vollständige Menge der von einer Python-Installation unterstützten Plattform-Tags im Voraus zu berechnen, da sie vom endgültigen System abhängen können.

    • Ein Pybi mit dem Tag manylinux_2_12_x86_64 kann immer Wheels verwenden, die als manylinux_2_12_x86_64 getaggt sind. Es *kann* auch Wheels verwenden, die mit manylinux_2_17_x86_64 getaggt sind, aber nur, wenn das endgültige Installationssystem glibc 2.17+ hat.
    • Ein Pybi mit dem Tag macosx_11_0_universal2 (= x86-64 + ARM64-Unterstützung im selben Binärpaket) kann möglicherweise Wheels verwenden, die als macosx_11_0_arm64 getaggt sind, aber nur, wenn es auf einem „Apple Silicon“-Computer installiert ist und im ARM64-Modus ausgeführt wird.

    In diesen beiden Fällen kann ein Installationstool immer noch den geeigneten Satz von Wheel-Tags ermitteln, indem es die lokalen Plattform-Tags berechnet, die Wheel-Tag-Vorlagen aus Pybi-Wheel-Tag entnimmt und die tatsächlichen unterstützten Plattformen anstelle des magischen PLATFORM-Strings einsetzt.

    Es gibt jedoch andere, noch kompliziertere Fälle.

    • Sie können (normalerweise) sowohl 32-Bit- als auch 64-Bit-Anwendungen auf 64-Bit-Windows ausführen. Ein Pybi
      Installer könnte den Satz zulässiger Pybi-Tags auf der aktuellen Plattform als [win32, win_amd64] berechnen. Aber Sie können diesen Satz nicht einfach nehmen und ihn in die Wheel-Tag-Vorlage des Pybis einsetzen, sonst erhalten Sie Unsinn.
      [
        "cp39-cp39-win32",
        "cp39-cp39-win_amd64",
        "cp39-abi3-win32",
        "cp39-abi3-win_amd64",
        ...
      ]
      

      Um dies zu handhaben, muss der Installer irgendwie verstehen, dass ein manylinux_2_12_x86_64-Pybi ein manylinux_2_17_x86_64-Wheel verwenden kann, solange beides gültige Tags auf der aktuellen Maschine sind, aber ein win32-Pybi *kann kein* win_amd64-Wheel verwenden, auch wenn beides gültige Tags auf der aktuellen Maschine sind.

    • Ein Pybi mit dem Tag macosx_11_0_universal2 kann möglicherweise Wheels verwenden, die als macosx_11_0_x86_64 getaggt sind, aber nur, wenn es auf einer x86-64-Maschine installiert ist *oder* es auf einer ARM-Maschine installiert ist *und* der Interpreter mit dem magischen Zauberspruch aufgerufen wird, der macOS anweist, ein Binärpaket im x86-64-Modus auszuführen. Wie der Installer den Pybi aufrufen möchte, spielt also auch eine Rolle!

    Die tatsächliche Verwendung von Pybi-Wheel-Tag-Werten ist also weniger trivial, als es scheinen mag, und sie sind wahrscheinlich nur mit ziemlich ausgefeilten Tools nützlich. Aber intelligente Pybi-Installer müssen bereits viele dieser Plattformkompatibilitätsprobleme verstehen, um ein funktionierendes Pybi auswählen zu können, und für den Fall des plattformübergreifenden Pinning/Umgebungsaufbaus können Benutzer potenziell alle Informationen bereitstellen, die erforderlich sind, um genau zu disambiguieren, welche Plattform sie anvisieren. Daher ist es immer noch nützlich genug, um in die PyBI-Metadaten aufgenommen zu werden – Tools, die es nicht nützlich finden, können es einfach ignorieren.

Sie können diese Metadatenwerte wahrscheinlich generieren, indem Sie dieses Skript auf dem kompilierten Interpreter ausführen.

import packaging.markers
import packaging.tags
import sysconfig
import os.path
import json
import sys

marker_vars = packaging.markers.default_environment()
# Delete any keys that depend on the final installation
del marker_vars["platform_release"]
del marker_vars["platform_version"]
# Darwin binaries are often multi-arch, so play it safe and
# delete the architecture marker. (Better would be to only
# do this if the pybi actually is multi-arch.)
if marker_vars["sys_platform"] == "darwin":
    del marker_vars["platform_machine"]

# Copied and tweaked version of packaging.tags.sys_tags
tags = []
interp_name = packaging.tags.interpreter_name()
if interp_name == "cp":
    tags += list(packaging.tags.cpython_tags(platforms=["xyzzy"]))
else:
    tags += list(packaging.tags.generic_tags(platforms=["xyzzy"]))

tags += list(packaging.tags.compatible_tags(platforms=["xyzzy"]))

# Gross hack: packaging.tags normalizes platforms by lowercasing them,
# so we generate the tags with a unique string and then replace it
# with our special uppercase placeholder.
str_tags = [str(t).replace("xyzzy", "PLATFORM") for t in tags]

(base_path,) = sysconfig.get_config_vars("installed_base")
# For some reason, macOS framework builds report their
# installed_base as a directory deep inside the framework.
while "Python.framework" in base_path:
    base_path = os.path.dirname(base_path)
paths = {key: os.path.relpath(path, base_path).replace("\\", "/") for (key, path) in sysconfig.get_paths().items()}

json.dump({"marker_vars": marker_vars, "tags": str_tags, "paths": paths}, sys.stdout)

Dies gibt ein JSON-Dictionary auf stdout aus, mit separaten Einträgen für jede Gruppe von Pybi-spezifischen Tags.

Nicht-normative Kommentare

Warum nicht einfach conda verwenden?

Dies liegt nicht wirklich im Umfang dieser PEP, aber da Conda ein beliebter Weg ist, binäre Python-Interpreter zu verteilen, ist dies eine natürliche Frage.

Die einfache Antwort ist: Conda ist großartig! Aber es gibt viele Python-Benutzer, die keine Conda-Benutzer sind, und sie verdienen auch nette Dinge. Diese PEP gibt ihnen einfach eine weitere Option.

Die tiefere Antwort ist: Die Maintainer, die Pakete auf PyPI hochladen, sind das Rückgrat des Python-Ökosystems. Sie sind die erste Zielgruppe für Python-Paketierungs-Tools. Und eine Sache, die sie wollen, ist, ein Paket einmal hochzuladen und es über alle verschiedenen Arten, wie Python bereitgestellt wird, zugänglich zu machen: in Debian und Fedora und Homebrew und FreeBSD, in Conda-Umgebungen, in Monorepos großer Unternehmen, in Nix, in Blender-Plugins, in RenPy-Spielen, ... Sie verstehen schon.

All diese Umgebungen haben ihre eigenen Werkzeuge und Strategien zur Verwaltung von Paketen und Abhängigkeiten. Was PyPI und Wheels also besonders macht, ist, dass sie darauf ausgelegt sind, Abhängigkeiten auf eine *standardisierte, abstrakte Weise* zu beschreiben, die all diese nachgelagerten Systeme verbrauchen und in ihre lokalen Konventionen umwandeln können. Deshalb verwenden Paketmaintainer Python-spezifische Metadaten und laden auf PyPI hoch: weil es ihnen ermöglicht, all diese Systeme gleichzeitig anzusprechen. Jedes Mal, wenn Sie ein Python-Paket für Conda erstellen, wird ein Zwischen-Wheel generiert, da Wheels die gemeinsame Sprache sind, mit der Python-Paket-Build-Systeme und Conda miteinander sprechen können.

Aber wenn Sie dann ein Maintainer sind, der ein sdist+wheels veröffentlicht, möchten Sie natürlich testen, was Sie veröffentlichen, was von beliebigen PyPI-Paketen und -Versionen abhängen kann. Sie benötigen also Tools, die Python-Umgebungen direkt aus PyPI erstellen, und Conda ist dafür grundsätzlich nicht ausgelegt. Daher sind Conda und Pip für verschiedene Fälle notwendig, und dieser Vorschlag zielt zufällig auf die Pip-Seite dieser Gleichung ab.

Sdists (oder nicht)

Es könnte cool sein, ein „sdist“-Äquivalent für Pybis zu haben, d.h. eine Art Format für eine Python-Quellveröffentlichung, die strukturiert genug ist, um Tools automatisch abrufen und in ein Pybi umwandeln zu lassen, für Plattformen, auf denen keine vorkompilierten Pybis verfügbar sind. Aber das ist für die MVP nicht notwendig und öffnet eine Büchse der Pandora, also kümmern wir uns später darum.

Welche Pakete sollten in einem pybi gebündelt werden?

Pybi-Builder haben die Möglichkeit, genau auszuwählen, was in sie hineinkommt. Sie könnten beispielsweise einige vorinstallierte Pakete in das site-packages-Verzeichnis des Pybis aufnehmen oder Teile der Standardbibliothek entfernen, die Sie nicht möchten. Wir können Sie nicht aufhalten! Wenn Sie jedoch Pakete vorinstallieren, wird dringend empfohlen, auch die entsprechenden Metadaten (.dist-info usw.) einzuschließen, damit Pip oder andere Tools verstehen können, was vor sich geht.

Für meine Prototyp-„Allzweck“-Pybis habe ich Folgendes gewählt:

  • Stellen Sie sicher, dass site-packages *leer* ist.

    Begründung: Bei herkömmlichen eigenständigen Python-Installern, die sich an Endbenutzer richten, möchten Sie wahrscheinlich mindestens pip einbeziehen, um Bootstrapping-Probleme zu vermeiden (PEP 453). Aber Pybis sind anders: Sie sind dafür konzipiert, von „intelligenten“ Tools installiert zu werden, die das Pybi als Teil eines größeren automatisierten Bereitstellungsprozesses verwenden. Für diese Installer ist es einfacher, von einem leeren Blatt zu starten und dann hinzuzufügen, was sie benötigen, als mit einigen vorinstallierten Paketen zu beginnen, die sie vielleicht wollen oder nicht wollen. (Und außerdem kann man immer noch python -m ensurepip ausführen.)

  • Vollständige Standardbibliothek einbeziehen, *außer* test.

    Begründung: Das Top-Level-Modul test enthält die Testsuite von CPython selbst. Es ist riesig (CPython ohne test ist ca. 37 MB, dann fügt test weitere ca. 25 MB hinzu!), und wird von regulärem Benutzercode praktisch nie verwendet. Außerdem lassen die offiziellen Nuget-Pakete, die offiziellen Manylinux-Images und mehrere Linux-Distributionen es aus, und das hat keine größeren Probleme verursacht.

    Dies scheint also der beste Weg zu sein, um breite Kompatibilität mit vernünftigen Download-/Installationsgrößen auszugleichen.

  • Ich versende keine .pyc-Dateien. Sie beanspruchen Speicherplatz beim Download, können auf dem endgültigen System zu minimalen Kosten generiert werden und entfernen durch ihr Weglassen eine Quelle für standortabhängige Informationen. (.pyc-Dateien speichern den absoluten Pfad der entsprechenden .py-Datei und fügen ihn in Tracebacks ein; Pybis sind jedoch verschiebbar, sodass der korrekte Pfad erst nach der Installation bekannt ist.)

Abwärtskompatibilität

Keine Abwärtskompatibilitätsprobleme.

Sicherheitsimplikationen

Keine Sicherheitsimplikationen, abgesehen von der Tatsache, dass jeder, der sich verpflichtet, Binärdateien zu verteilen, einen Plan für die Verwaltung ihrer Sicherheit erstellen muss (z. B. ob er einen neuen Build nach einem OpenSSL CVE veröffentlicht). Aber gemeinsam pflegen wir Kern-Python-Leute bereits Binärpakete für alle wichtigen Plattformen (macOS + Windows über python.org und Linux-Builds über das offizielle manylinux-Image), sodass selbst wenn wir offizielle CPython-Builds auf PyPI veröffentlichen, dies keine neuen Sicherheitsprobleme aufwirft.

Wie man das lehrt

Dies richtet sich nicht an Endbenutzer; ihre Erfahrung wird einfach sein, dass z. B. ihre pyenv- oder tox-Aufrufe magisch schneller und zuverlässiger werden (wenn die Maintainer dieser Projekte beschließen, diese PEP zu nutzen).


Quelle: https://github.com/python/peps/blob/main/peps/pep-0711.rst

Zuletzt geändert: 2025-02-01 08:55:40 GMT