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

Python Enhancement Proposals

PEP 517 – Ein Build-System-unabhängiges Format für Quellcode-Verzeichnisse

Autor:
Nathaniel J. Smith <njs at pobox.com>, Thomas Kluyver <thomas at kluyver.me.uk>
BDFL-Delegate:
Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Distutils-SIG Liste
Status:
Final
Typ:
Standards Track
Thema:
Packaging
Erstellt:
30-Sep-2015
Post-History:
01-Oct-2015, 25-Oct-2015, 19-Mai-2017, 11-Sep-2017
Resolution:
Distutils-SIG Nachricht

Inhaltsverzeichnis

Zusammenfassung

Während distutils / setuptools uns weit gebracht haben, leiden sie unter drei ernsten Problemen: (a) ihnen fehlen wichtige Funktionen wie nutzbare Deklaration von Build-Abhängigkeiten, Autokonfiguration und sogar grundlegende ergonomische Annehmlichkeiten wie DRY-konformes Versionsnummernmanagement, und (b) ihre Erweiterung ist schwierig, sodass, obwohl es verschiedene Lösungen für die oben genannten Probleme gibt, diese oft eigenartig, zerbrechlich und teuer im Unterhalt sind, und dennoch (c) ist es sehr schwierig, etwas anderes zu verwenden, da distutils/setuptools die Standard-Schnittstelle für die Installation von Paketen bereitstellen, die sowohl von Benutzern als auch von Installationstools wie pip erwartet wird.

Frühere Bemühungen (z. B. distutils2 oder setuptools selbst) haben versucht, Probleme (a) und/oder (b) zu lösen. Dieser Vorschlag zielt darauf ab, (c) zu lösen.

Das Ziel dieses PEP ist es, distutils-sig aus dem Geschäft der Gatekeeper für Python-Build-Systeme herauszuholen. Wenn Sie distutils verwenden möchten, großartig; wenn Sie etwas anderes verwenden möchten, sollte das auf standardisierten Wegen einfach möglich sein. Die Schwierigkeit der Interaktion mit distutils bedeutet, dass es derzeit nicht viele solche Systeme gibt, aber um eine Vorstellung davon zu geben, was wir uns vorstellen, siehe flit oder bento. Glücklicherweise haben Wheels viele der harten Probleme hier gelöst – z. B. ist es nicht mehr notwendig, dass ein Build-System auch über jede mögliche Installationskonfiguration Bescheid weiß – sodass praktisch alles, was wir wirklich von einem Build-System brauchen, darin besteht, dass es eine Möglichkeit gibt, Standard-konforme Wheels und sdists auszugeben.

Wir schlagen daher eine neue, relativ minimale Schnittstelle für Installationstools wie pip vor, um mit Paketquellcode-Verzeichnissen und Source-Distributionen zu interagieren.

Terminologie und Ziele

Ein Quellcode-Verzeichnis ist so etwas wie ein VCS-Checkout. Wir benötigen eine Standard-Schnittstelle für die Installation aus diesem Format, um Verwendungszwecke wie pip install some-directory/ zu unterstützen.

Eine Source-Distribution ist ein statischer Schnappschuss, der eine bestimmte Version eines Quellcodes darstellt, wie z. B. lxml-3.4.4.tar.gz. Source-Distributionen dienen vielen Zwecken: Sie bilden einen archivierten Nachweis von Releases, sie bieten einen extrem einfachen De-facto-Standard für Tools, die große Codekorpora aufnehmen und verarbeiten wollen, möglicherweise in vielen Sprachen geschrieben (z. B. Code-Suche), sie fungieren als Eingabe für nachgelagerte Verpackungssysteme wie Debian/Fedora/Conda/..., und so weiter. Im Python-Ökosystem spielen sie zusätzlich eine besonders wichtige Rolle, da Verpackungstools wie pip Source-Distributionen verwenden können, um Binärabhängigkeiten zu erfüllen, z. B. wenn es eine Distribution foo.whl gibt, die eine Abhängigkeit von bar deklariert, dann müssen wir den Fall unterstützen, dass pip install bar oder pip install foo automatisch die sdist für bar findet, herunterlädt, baut und das resultierende Paket installiert.

Source-Distributionen werden auch kurz als sdists bezeichnet.

Ein Build-Frontend ist ein Werkzeug, das Benutzer ausführen können, das beliebige Quellcode-Verzeichnisse oder Source-Distributionen entgegennimmt und daraus Wheels baut. Das eigentliche Bauen wird vom Build-Backend jedes Quellcode-Verzeichnisses übernommen. In einem Befehl wie pip wheel some-directory/ agiert pip als Build-Frontend.

Ein Integrations-Frontend ist ein Werkzeug, das Benutzer ausführen können, das eine Reihe von Paket-Anforderungen entgegennimmt (z. B. eine requirements.txt-Datei) und versucht, eine funktionierende Umgebung zur Erfüllung dieser Anforderungen zu aktualisieren. Dies kann das Finden, Bauen und Installieren einer Kombination von Wheels und sdists erfordern. In einem Befehl wie pip install lxml==2.4.0 agiert pip als Integrations-Frontend.

Quellcode-Verzeichnisse

Es gibt ein bestehendes, legacy Quellcode-Verzeichnis-Format, das setup.py beinhaltet. Wir versuchen nicht, es weiter zu spezifizieren; seine De-facto-Spezifikation ist im Quellcode und in der Dokumentation von distutils, setuptools, pip und anderen Tools kodiert. Wir werden es als setup.py-Stil bezeichnen.

Hier definieren wir einen neuen Stil von Quellcode-Verzeichnissen, der auf der Datei pyproject.toml basiert, die in PEP 518 definiert ist, und erweitern die Tabelle [build-system] in dieser Datei um einen zusätzlichen Schlüssel, build-backend. Hier ist ein Beispiel, wie es aussehen würde

[build-system]
# Defined by PEP 518:
requires = ["flit"]
# Defined by this PEP:
build-backend = "flit.api:main"

build-backend ist ein String, der ein Python-Objekt benennt, das zum Ausführen des Builds verwendet wird (siehe unten für Details). Dies wird im gleichen modul:objekt-Format wie ein setuptools Entry Point formatiert. Wenn der String beispielsweise "flit.api:main" lautet, wie im obigen Beispiel, wird dieses Objekt durch Ausführen des Äquivalents von gefunden

import flit.api
backend = flit.api.main

Es ist auch legal, den Teil :objekt wegzulassen, z. B.

build-backend = "flit.api"

was so funktioniert wie

import flit.api
backend = flit.api

Formell sollte der String diese Grammatik erfüllen

identifier = (letter | '_') (letter | '_' | digit)*
module_path = identifier ('.' identifier)*
object_path = identifier ('.' identifier)*
entry_point = module_path (':' object_path)?

Und wir importieren modulpfad und schauen dann nach modulpfad.objektpfad (oder einfach modulpfad, wenn objektpfad fehlt).

Beim Importieren des Modulpfades schauen wir nicht im Verzeichnis, das das Quellcode-Verzeichnis enthält, es sei denn, dies wäre sowieso auf sys.path: (z. B. weil es in PYTHONPATH angegeben ist). Obwohl Python in einigen Situationen automatisch das Arbeitsverzeichnis zu sys.path hinzufügt, sollte der Code zur Auflösung des Backends hiervon nicht betroffen sein.

Wenn die Datei pyproject.toml fehlt oder der Schlüssel build-backend fehlt, verwendet das Quellcode-Verzeichnis diese Spezifikation nicht, und Tools sollten auf das Legacy-Verhalten zurückfallen, setup.py auszuführen (entweder direkt oder durch implizites Aufrufen des Backends setuptools.build_meta:__legacy__).

Wo der Schlüssel build-backend existiert, hat dieser Vorrang und das Quellcode-Verzeichnis folgt dem Format und den Konventionen des angegebenen Backends (somit ist kein setup.py erforderlich, es sei denn, das Backend benötigt es). Projekte möchten möglicherweise weiterhin ein setup.py für die Kompatibilität mit Tools, die diese Spezifikation nicht verwenden, beibehalten.

Dieses PEP definiert auch einen Schlüssel backend-path zur Verwendung in pyproject.toml, siehe den Abschnitt „Build-Backends im Verzeichnis“ unten. Dieser Schlüssel würde wie folgt verwendet

[build-system]
# Defined by PEP 518:
requires = ["flit"]
# Defined by this PEP:
build-backend = "local_backend"
backend-path = ["backend"]

Build-Anforderungen

Dieses PEP legt eine Reihe zusätzlicher Anforderungen an den Abschnitt „Build-Anforderungen“ von pyproject.toml fest. Diese sollen sicherstellen, dass Projekte keine unmöglich zu erfüllenden Bedingungen mit ihren Build-Anforderungen schaffen.

  • Projekt-Build-Anforderungen definieren einen gerichteten Graphen von Anforderungen (Projekt A benötigt B zum Bauen, B benötigt C und D usw.). Dieser Graph darf KEINE Zyklen enthalten. Wenn (aufgrund mangelnder Koordination zwischen Projekten, zum Beispiel) ein Zyklus vorhanden ist, können Frontends den Bau des Projekts ablehnen.
  • Wenn Build-Anforderungen als Wheels verfügbar sind, sollten Frontends diese praktisch nutzen, um tief verschachtelte Builds zu vermeiden. Frontends können jedoch Modi haben, in denen sie keine Wheels bei der Suche nach Build-Anforderungen berücksichtigen, und Projekte dürfen daher nicht davon ausgehen, dass das Veröffentlichen von Wheels ausreicht, um einen Anforderungszyklus zu durchbrechen.
  • Frontends sollten explizit auf Anforderungszyklen prüfen und den Build mit einer aussagekräftigen Meldung beenden, wenn einer gefunden wird.

Beachten Sie insbesondere, dass die Anforderung für keine Anforderungszyklen bedeutet, dass Backends, die sich selbst hosten möchten (d. h. das Bauen eines Wheels für ein Backend nutzt dieses Backend für den Build), besondere Vorkehrungen treffen müssen, um Zyklen zu vermeiden. Typischerweise beinhaltet dies die Angabe als In-Tree-Backend und die Vermeidung externer Build-Abhängigkeiten (normalerweise durch Vendorisierung).

Build-Backend-Interface

Vom Build-Backend-Objekt werden Attribute erwartet, die einige oder alle der folgenden Hooks bereitstellen. Das gemeinsame Argument config_settings wird nach den einzelnen Hooks beschrieben.

Obligatorische Hooks

build_wheel

def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
    ...

Muss eine .whl-Datei bauen und sie im angegebenen wheel_directory platzieren. Es muss den Basisnamen (nicht den vollständigen Pfad) der erstellten .whl-Datei als Unicode-String zurückgeben.

Wenn das Build-Frontend zuvor prepare_metadata_for_build_wheel aufgerufen hat und von dem aus diesem Aufruf resultierenden Wheel erwartet, dass es Metadaten hat, die mit diesem früheren Aufruf übereinstimmen, sollte es den Pfad zum erstellten .dist-info-Verzeichnis als Argument metadata_directory angeben. Wenn dieses Argument bereitgestellt wird, MUSS build_wheel ein Wheel mit identischen Metadaten erstellen. Das vom Build-Frontend übergebene Verzeichnis MUSS mit dem von prepare_metadata_for_build_wheel erstellten Verzeichnis identisch sein, einschließlich aller nicht erkannten Dateien, die es erstellt hat.

Backends, die den prepare_metadata_for_build_wheel Hook nicht bereitstellen, können den Parameter metadata_directory für build_wheel stumm ignorieren oder eine Ausnahme auslösen, wenn dieser auf etwas anderes als None gesetzt ist.

Um sicherzustellen, dass Wheels aus verschiedenen Quellen auf die gleiche Weise gebaut werden, können Frontends zuerst build_sdist aufrufen und dann build_wheel im entpackten sdist aufrufen. Wenn das Backend jedoch angibt, dass ihm einige Anforderungen für die Erstellung eines sdists fehlen (siehe unten), fällt das Frontend auf den Aufruf von build_wheel im Quellverzeichnis zurück.

Das Quellverzeichnis kann schreibgeschützt sein. Backends sollten daher darauf vorbereitet sein, ohne Erstellung oder Änderung von Dateien im Quellverzeichnis zu bauen, können aber entscheiden, diesen Fall nicht zu behandeln, in welchem Fall Fehler für den Benutzer sichtbar sind. Frontends sind nicht für besondere Handhabung von schreibgeschützten Quellverzeichnissen verantwortlich.

Das Backend kann Zwischenartefakte in Cache-Speichern oder temporären Verzeichnissen speichern. Das Vorhandensein oder Fehlen von Caches sollte keinen wesentlichen Unterschied zum Endergebnis des Builds machen.

build_sdist

def build_sdist(sdist_directory, config_settings=None):
    ...

Muss eine .tar.gz Source-Distribution bauen und sie im angegebenen sdist_directory platzieren. Es muss den Basisnamen (nicht den vollständigen Pfad) der erstellten .tar.gz-Datei als Unicode-String zurückgeben.

Eine .tar.gz Source-Distribution (sdist) enthält ein einzelnes Top-Level-Verzeichnis namens {name}-{version} (z. B. foo-1.0), das die Quellcodedateien des Pakets enthält. Dieses Verzeichnis muss auch die pyproject.toml aus dem Build-Verzeichnis und eine PKG-INFO-Datei mit Metadaten im Format, das in PEP 345 beschrieben ist, enthalten. Obwohl historisch auch ZIP-Dateien als sdists verwendet wurden, sollte dieser Hook einen komprimierten Tarball erzeugen. Dies ist bereits das gebräuchlichere Format für sdists, und ein konsistentes Format erleichtert die Tooling.

Der generierte Tarball sollte das moderne POSIX.1-2001 pax-Tar-Format verwenden, das UTF-8-basierte Dateinamen spezifiziert. Dies ist für das mit Python 3.6 ausgelieferte tarfile-Modul noch nicht der Standard, daher müssen Backends, die das tarfile-Modul verwenden, explizit format=tarfile.PAX_FORMAT übergeben.

Einige Backends haben möglicherweise zusätzliche Anforderungen für die Erstellung von sdists, wie z. B. Versionskontrolltools. Einige Frontends ziehen es jedoch möglicherweise vor, Zwischen-sdists bei der Erstellung von Wheels zu erstellen, um die Konsistenz zu gewährleisten. Wenn das Backend eine sdist nicht erstellen kann, weil eine Abhängigkeit fehlt oder aus einem anderen gut verstandenen Grund, sollte es eine Ausnahme eines spezifischen Typs auslösen, den es als UnsupportedOperation auf dem Backend-Objekt verfügbar macht. Wenn das Frontend diese Ausnahme beim Bauen einer sdist als Zwischenschritt für ein Wheel erhält, sollte es auf das Bauen eines Wheels direkt zurückfallen. Das Backend muss diesen Ausnahmetyp nicht definieren, wenn es ihn niemals auslösen würde.

Optionale Hooks

get_requires_for_build_wheel

def get_requires_for_build_wheel(config_settings=None):
    ...

Dieser Hook MUSS eine zusätzliche Liste von Strings zurückgeben, die PEP 508-Abhängigkeitsspezifikationen enthält, zusätzlich zu denen, die in der Datei pyproject.toml angegeben sind, die bei Aufruf der Hooks build_wheel oder prepare_metadata_for_build_wheel installiert werden müssen.

Beispiel

def get_requires_for_build_wheel(config_settings):
    return ["wheel >= 0.25", "setuptools"]

Wenn nicht definiert, entspricht die Standardimplementierung return [].

prepare_metadata_for_build_wheel

def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
    ...

Muss ein .dist-info-Verzeichnis mit Wheel-Metadaten innerhalb des angegebenen metadata_directory erstellen (d. h., es erstellt ein Verzeichnis wie {metadata_directory}/{package}-{version}.dist-info/). Dieses Verzeichnis MUSS ein gültiges .dist-info-Verzeichnis sein, wie in der Wheel-Spezifikation definiert, außer dass es RECORD oder Signaturen nicht enthalten muss. Der Hook darf auch andere Dateien in diesem Verzeichnis erstellen, und ein Build-Frontend MUSS solche Dateien erhalten, aber ansonsten ignorieren; die Absicht hier ist, dass in Fällen, in denen die Metadaten von Build-Zeit-Entscheidungen abhängen, das Build-Backend diese Entscheidungen möglicherweise in einem bequemen Format aufzeichnen muss, um vom eigentlichen Wheel-Build-Schritt wiederverwendet zu werden.

Dies muss den Basisnamen (nicht den vollständigen Pfad) des von ihm erstellten .dist-info-Verzeichnisses als Unicode-String zurückgeben.

Wenn ein Build-Frontend diese Informationen benötigt und die Methode nicht definiert ist, sollte es build_wheel aufrufen und die resultierenden Metadaten direkt betrachten.

get_requires_for_build_sdist

def get_requires_for_build_sdist(config_settings=None):
    ...

Dieser Hook MUSS eine zusätzliche Liste von Strings zurückgeben, die PEP 508-Abhängigkeitsspezifikationen enthält, zusätzlich zu denen, die in der Datei pyproject.toml angegeben sind. Diese Abhängigkeiten werden bei Aufruf des Hooks build_sdist installiert.

Wenn nicht definiert, entspricht die Standardimplementierung return [].

Hinweis

Editable Installs

Dieses PEP spezifizierte ursprünglich einen weiteren Hook, install_editable, um eine editable Installation durchzuführen (wie mit pip install -e). Er wurde aufgrund der Komplexität des Themas entfernt, könnte aber in einem späteren PEP spezifiziert werden.

Kurz gesagt, die zu beantwortenden Fragen sind: welche vernünftigen Wege gibt es, eine „editable Install“ zu implementieren? Sollte das Backend oder das Frontend entscheiden, wie eine editable Install erstellt wird? Und wenn das Frontend das tut, was benötigt es vom Backend, um dies zu tun?

Konfigurationseinstellungen

config_settings

Dieses Argument, das an alle Hooks übergeben wird, ist ein beliebiges Wörterbuch, das als „Escape Hatch“ bereitgestellt wird, damit Benutzer Ad-hoc-Konfigurationen in einzelne Paket-Builds übergeben können. Build-Backends dürfen dieser Wörterbuch beliebige Semantik zuweisen. Build-Frontends sollten einen Mechanismus bereitstellen, damit Benutzer beliebige String-Schlüssel/String-Wert-Paare angeben können, die in dieses Wörterbuch aufgenommen werden. Sie könnten zum Beispiel eine Syntax wie --package-config CC=gcc unterstützen. Wenn ein Benutzer doppelte String-Schlüssel angibt, sollten Build-Frontends die entsprechenden String-Werte zu einer Liste von Strings kombinieren. Build-Frontends können auch beliebige andere Mechanismen bereitstellen, damit Benutzer Einträge in dieses Wörterbuch aufnehmen können. Zum Beispiel könnte pip wählen, eine Mischung aus modernen und Legacy-Kommandozeilenargumenten wie

pip install                                           \
  --package-config CC=gcc                             \
  --global-option="--some-global-option"              \
  --build-option="--build-option1"                    \
  --build-option="--build-option2"

in ein config_settings-Wörterbuch wie

{
 "CC": "gcc",
 "--global-option": ["--some-global-option"],
 "--build-option": ["--build-option1", "--build-option2"],
}

Natürlich liegt es an den Benutzern, sicherzustellen, dass sie Optionen übergeben, die für das jeweilige Build-Backend und Paket sinnvoll sind, das sie bauen.

Die Hooks können mit Positions- oder Schlüsselwortargumenten aufgerufen werden, daher sollten Backend-Implementierer darauf achten, dass ihre Signaturen sowohl der Reihenfolge als auch den Namen der obigen Argumente entsprechen.

Alle Hooks werden mit dem Arbeitsverzeichnis auf die Wurzel des Quellcode-Verzeichnisses gesetzt ausgeführt und DÜRFEN beliebige informative Texte auf stdout und stderr ausgeben. Sie DÜRFEN nicht von stdin lesen, und das Build-Frontend kann stdin schließen, bevor die Hooks aufgerufen werden.

Das Build-Frontend kann stdout und/oder stderr vom Backend aufzeichnen. Wenn das Backend erkennt, dass ein Ausgabestrom kein Terminal/Konsole ist (z. B. not sys.stdout.isatty()), SOLLTE es sicherstellen, dass alle Ausgaben, die es in diesen Strom schreibt, UTF-8-kodiert sind. Das Build-Frontend darf NICHT fehlschlagen, wenn die aufgezeichnete Ausgabe kein gültiges UTF-8 ist, aber es darf nicht alle Informationen in diesem Fall erhalten (z. B. kann es mit dem replace-Fehlerhandler in Python dekodieren). Wenn der Ausgabestrom ein Terminal ist, ist das Build-Backend dafür verantwortlich, seine Ausgabe korrekt darzustellen, wie bei jedem Programm, das in einem Terminal läuft.

Wenn ein Hook eine Ausnahme auslöst oder den Prozess zum Abbruch bringt, deutet dies auf einen Fehler hin.

Build-Umgebung

Eine der Aufgaben eines Build-Frontends ist die Einrichtung der Python-Umgebung, in der das Build-Backend ausgeführt wird.

Wir verlangen keine Verwendung eines bestimmten „virtuellen Umgebungs“-Mechanismus; ein Build-Frontend könnte virtualenv oder venv verwenden oder gar keinen speziellen Mechanismus. Aber welcher Mechanismus auch immer verwendet wird, MUSS die folgenden Kriterien erfüllen

  • Alle vom Projekt geforderten Build-Anforderungen müssen importierbar sein aus Python. Insbesondere
    • Die Hooks get_requires_for_build_wheel und get_requires_for_build_sdist werden in einer Umgebung ausgeführt, die die Bootstrap-Anforderungen enthält, die in der Datei pyproject.toml angegeben sind.
    • Die Hooks prepare_metadata_for_build_wheel und build_wheel werden in einer Umgebung ausgeführt, die die Bootstrap-Anforderungen aus pyproject.toml und die vom Hook get_requires_for_build_wheel spezifizierten Anforderungen enthält.
    • Der Hook build_sdist wird in einer Umgebung ausgeführt, die die Bootstrap-Anforderungen aus pyproject.toml und die vom Hook get_requires_for_build_sdist spezifizierten Anforderungen enthält.
  • Dies muss auch für neue Python-Subprozesse, die von der Build-Umgebung gestartet werden, wahr bleiben, z. B. Code wie
    import sys, subprocess
    subprocess.check_call([sys.executable, ...])
    

    muss einen Python-Prozess starten, der Zugriff auf alle Build-Anforderungen des Projekts hat. Dies ist z. B. notwendig für Build-Backends, die Legacy setup.py-Skripte in einem Subprozess ausführen möchten.

  • Alle Kommandozeilen-Skripte, die von den Build-erforderten Paketen bereitgestellt werden, müssen im PATH der Build-Umgebung vorhanden sein. Wenn ein Projekt beispielsweise eine Build-Anforderung für flit deklariert, dann muss Folgendes als Mechanismus zum Ausführen des flit Kommandozeilen-Tools funktionieren
    import subprocess
    import shutil
    subprocess.check_call([shutil.which("flit"), ...])
    

Ein Build-Backend MUSS bereit sein, in jeder Umgebung zu funktionieren, die die oben genannten Kriterien erfüllt. Insbesondere DARF es NICHT davon ausgehen, dass es Zugriff auf Pakete hat, die nicht in der stdlib enthalten sind oder die nicht explizit als Build-Anforderungen deklariert sind.

Frontends sollten jeden Hook in einem frischen Subprozess aufrufen, damit Backends frei sind, prozessweite globale Zustände zu ändern (wie Umgebungsvariablen oder das Arbeitsverzeichnis). Eine Python-Bibliothek wird bereitgestellt, die Frontends zum einfachen Aufrufen von Hooks auf diese Weise verwenden können.

Empfehlungen für Build-Frontends (nicht-normativ)

Ein Build-Frontend kann jeden Mechanismus zur Einrichtung einer Build-Umgebung verwenden, der die oben genannten Kriterien erfüllt. Zum Beispiel wäre die bloße Installation aller Build-Anforderungen in die globale Umgebung ausreichend, um jedes konforme Paket zu bauen – dies wäre jedoch aus verschiedenen Gründen suboptimal. Dieser Abschnitt enthält nicht-normative Ratschläge für Frontend-Implementierer.

Ein Build-Frontend SOLLTE standardmäßig eine isolierte Umgebung für jeden Build erstellen, die nur die Standardbibliothek und alle explizit angeforderten Build-Abhängigkeiten enthält. Dies hat zwei Vorteile

  • Es ermöglicht, dass ein einzelner Installationslauf mehrere Pakete baut, die widersprüchliche Build-Anforderungen haben. Z. B. wenn package1 pbr==1.8.1 als Build-Anforderung hat und package2 pbr==1.7.2 als Build-Anforderung hat, dann können diese nicht gleichzeitig in der globalen Umgebung installiert werden – was ein Problem ist, wenn der Benutzer pip install package1 package2 anfordert. Oder wenn der Benutzer bereits pbr==1.8.1 in seiner globalen Umgebung installiert hat und ein Paket pbr==1.7.2 als Build-Anforderung hat, dann wäre das Herunterstufen der Version des Benutzers eher unhöflich.
  • Es fungiert als eine Art öffentliche Gesundheitsmaßnahme, um die Anzahl der Pakete zu maximieren, die tatsächlich genaue Build-Abhängigkeiten deklarieren. Wir können so viele nachdrückliche Ermahnungen an Paketautoren schreiben, wie wir wollen, aber wenn Build-Frontends standardmäßig keine Isolierung erzwingen, werden wir unweigerlich viele Pakete auf PyPI haben, die auf der Maschine des ursprünglichen Autors einwandfrei funktionieren und nirgendwo anders, was eine Kopfschmerzquelle ist, die niemand braucht.

Es wird jedoch auch Situationen geben, in denen Build-Anforderungen auf verschiedene Weise problematisch sind. Zum Beispiel könnte ein Paketautor versehentlich eine wichtige Anforderung weglassen, trotz unserer besten Bemühungen; oder ein Paket könnte foo >= 1.0 als Build-Anforderung deklarieren, was großartig funktionierte, als 1.0 die neueste Version war, aber jetzt, da 1.1 draußen ist und einen Showstopper-Bug hat; oder der Benutzer entscheidet sich, ein Paket gegen numpy==1.7 zu bauen – anstatt des bevorzugten numpy==1.8 des Pakets – um sicherzustellen, dass der resultierende Build auf der C ABI-Ebene mit einer älteren Version von numpy kompatibel ist (auch wenn dies bedeutet, dass der resultierende Build upstream nicht unterstützt wird). Daher SOLLTEN Build-Frontends einen Mechanismus bereitstellen, damit Benutzer die oben genannten Standardeinstellungen überschreiben können. Zum Beispiel könnte ein Build-Frontend eine Option --build-with-system-site-packages haben, die die Option --system-site-packages an virtualenv oder Äquivalentes weitergibt, wenn Build-Umgebungen erstellt werden, oder eine Option --build-requirements-override=my-requirements.txt, die die normalen Build-Anforderungen des Projekts überschreibt.

Das allgemeine Prinzip hier ist, dass wir die Hygiene bei Paket-*Autoren* erzwingen wollen, während wir den *Endbenutzern* immer noch erlauben, die Haube zu öffnen und bei Bedarf mit Klebeband zu reparieren.

Build-Backends im Verzeichnis

Unter bestimmten Umständen möchten Projekte den Quellcode für das Build-Backend direkt im Quellcode-Verzeichnis einschließen, anstatt das Backend über den Schlüssel requires zu referenzieren. Zwei spezifische Situationen, in denen dies erwartet würde, sind

  • Backends selbst, die ihre eigenen Funktionen zum Bauen von sich selbst verwenden möchten („Self-Hosting-Backends“)
  • Projektspezifische Backends, die typischerweise aus einem benutzerdefinierten Wrapper um ein Standard-Backend bestehen, wobei der Wrapper zu projektspezifisch ist, um ihn unabhängig zu verteilen („In-Tree-Backends“)

Projekte können angeben, dass ihr Backend-Code im Verzeichnis gehostet wird, indem sie den Schlüssel backend-path in pyproject.toml aufnehmen. Dieser Schlüssel enthält eine Liste von Verzeichnissen, die das Frontend am Anfang von sys.path hinzufügen wird, wenn das Backend geladen und die Backend-Hooks ausgeführt werden.

Es gibt zwei Einschränkungen für den Inhalt des Schlüssels backend-path

  • Verzeichnisse in backend-path werden relativ zum Projekt-Root interpretiert und MÜSSEN auf einen Ort innerhalb des Quellcode-Verzeichnisses verweisen (nachdem relative Pfade und symbolische Links aufgelöst wurden).
  • Der Backend-Code MUSS aus einem der Verzeichnisse geladen werden, die in backend-path angegeben sind (d. h. es ist nicht erlaubt, backend-path anzugeben und keinen In-Tree-Backend-Code zu haben).

Die erste Einschränkung dient dazu, sicherzustellen, dass Quellcode-Verzeichnisse in sich geschlossen bleiben und nicht auf Orte außerhalb des Quellcode-Verzeichnisses verweisen können. Frontends SOLLTEN diese Bedingung prüfen (typischerweise durch Auflösung des Speicherorts zu einem absoluten Pfad und Auflösung symbolischer Links, und dann Überprüfung gegen das Projekt-Root) und mit einer Fehlermeldung abbrechen, wenn sie verletzt wird.

Die Funktion backend-path ist zur Unterstützung der Implementierung von In-Tree-Backends gedacht und nicht zur Konfiguration bestehender Backends. Die zweite Einschränkung oben dient speziell dazu, sicherzustellen, dass dies die Art und Weise ist, wie die Funktion verwendet wird. Frontends können diese Prüfung durchsetzen, sind aber nicht dazu verpflichtet. Dies würde typischerweise das Überprüfen des __file__-Attributs des Backends gegen die Orte in backend-path beinhalten.

Source-Distributionen

Wir setzen das Legacy-Sdist-Format fort und fügen einige neue Einschränkungen hinzu. Dieses Format ist weitgehend undefiniert, kommt aber im Grunde darauf hinaus: eine Datei namens {NAME}-{VERSION}.{EXT}, die in ein buildbares Quellcode-Verzeichnis namens {NAME}-{VERSION}/ entpackt wird. Traditionell enthielten diese immer setup.py-Stil Quellcode-Verzeichnisse; wir erlauben jetzt, dass sie auch pyproject.toml-Stil Quellcode-Verzeichnisse enthalten.

Integrations-Frontends erfordern, dass eine sdist namens {NAME}-{VERSION}.{EXT} eine wheel namens {NAME}-{VERSION}-{COMPAT-INFO}.whl generiert.

Die neuen Einschränkungen für sdists, die von PEP 517-Backends gebaut werden, sind

  • Sie werden komprimierte Tar-Archive sein, mit der Erweiterung .tar.gz. Zip-Archive oder andere Kompressionsformate für Tarballs sind derzeit nicht erlaubt.
  • Tar-Archive müssen im modernen POSIX.1-2001 pax-Tar-Format erstellt werden, das UTF-8 für Dateinamen verwendet.
  • Das in einer sdist enthaltene Quellcode-Verzeichnis wird voraussichtlich die Datei pyproject.toml enthalten.

Evolutionäre Hinweise

Ein Ziel hier ist es, die Konvertierung von Alt-Style-sdists in Neu-Style-sdists so einfach wie möglich zu gestalten. (Dies ist z. B. eine Motivation für die Unterstützung dynamischer Build-Anforderungen.) Das Ideal wäre, dass es eine einzelne statische pyproject.toml gäbe, die in jeden „Version 0“-VCS-Checkout eingefügt werden könnte, um ihn in den neuen Glanz zu konvertieren. Dies ist wahrscheinlich nicht zu 100 % möglich, aber wir können ihm nahe kommen, und es ist wichtig zu verfolgen, wie nahe wir kommen… daher dieser Abschnitt.

Ein grober Plan wäre: Ein Build-System-Paket erstellen (setuptools_pypackage oder wie auch immer) das weiß, wie man mit der von uns entwickelten Hook-Sprache spricht, und diese in Aufrufe von setup.py umwandelt. Dies wird wahrscheinlich eine Art von Haken oder Monkeypatching für setuptools erfordern, um eine Möglichkeit bereitzustellen, das setup_requires= Argument bei Bedarf zu extrahieren, und eine neue Version des sdist-Befehls bereitzustellen, die das neue Format generiert. Dies scheint alles machbar und für einen großen Teil der Pakete ausreichend zu sein (obwohl wir ein solches System natürlich prototypisieren wollen, bevor wir hier etwas abschließen). (Alternativ könnten diese Änderungen an setuptools selbst vorgenommen werden, anstatt in ein separates Paket zu gelangen.)

Es bleiben jedoch zwei Hindernisse, die bedeuten, dass wir Pakete wahrscheinlich nicht automatisch in das neue Format aktualisieren können

  1. Derzeit gibt es Pakete, die darauf bestehen, dass bestimmte Pakete in ihrer Umgebung verfügbar sind, bevor setup.py ausgeführt wird. Das bedeutet, wenn wir uns entscheiden, Build-Skripte in einer isolierten virtuellen Umgebung auszuführen, müssen Projekte prüfen, ob sie dies tun, und wenn ja, müssen sie beim Upgrade auf das neue System explizit diese Abhängigkeiten deklarieren (entweder über setup_requires= oder über statische Deklaration in pyproject.toml).
  2. Derzeit gibt es Pakete, die keine konsistenten Metadaten deklarieren (z. B. egg_info und bdist_wheel könnten unterschiedliche install_requires= erhalten). Beim Upgrade auf das neue System müssen Projekte prüfen, ob dies auf sie zutrifft, und wenn ja, müssen sie damit aufhören.

Abgelehnte Optionen

  • Wir diskutierten die Erstellung von Wheel- und sdist-Hooks, die entpackte Verzeichnisse mit denselben Inhalten wie ihre jeweiligen Archive erstellen. In einigen Fällen könnte dies die Notwendigkeit vermeiden, ein Archiv zu packen und zu entpacken, aber das scheint eine voreilige Optimierung zu sein. Es ist vorteilhaft für Tools, mit Archiven als kanonischen Austauschformaten zu arbeiten (insbesondere für Wheels, wo das Archivformat bereits standardisiert ist). Eine genaue Kontrolle der Archivgenerierung ist für reproduzierbare Builds wichtig. Und es ist nicht klar, dass Aufgaben, die eine entpackte Distribution erfordern, häufiger vorkommen als solche, die ein Archiv erfordern.
  • Wir erwogen einen zusätzlichen Hook, um Dateien vor dem Aufruf von build_wheel in ein Build-Verzeichnis zu kopieren. Bei der Betrachtung bestehender Build-Systeme stellten wir fest, dass die Übergabe eines Build-Verzeichnisses an build_wheel für viele Tools sinnvoller war als das präventive Kopieren von Dateien in ein Build-Verzeichnis.
  • Die Idee, build_wheel ein Build-Verzeichnis zu übergeben, wurde dann ebenfalls als unnötige Komplikation angesehen. Build-Tools können ein temporäres Verzeichnis oder ein Cache-Verzeichnis verwenden, um Zwischendateien während des Builds zu speichern. Wenn Bedarf besteht, könnte in Zukunft ein vom Frontend gesteuertes Cache-Verzeichnis hinzugefügt werden.
  • Für build_sdist, um einen Fehler aus einem erwarteten Grund zu signalisieren, wurden verschiedene Optionen sehr ausführlich diskutiert, einschließlich des Auslösens von NotImplementedError und der Rückgabe von entweder NotImplemented oder None. Bitte versuchen Sie nicht, diese Diskussion erneut zu eröffnen, es sei denn, es gibt einen *äußerst* guten Grund, da wir sie ziemlich leid sind.
  • Das Importieren des Backends aus Dateien im Quellcode wäre konsistenter mit der Art und Weise, wie Python-Importe oft funktionieren. Das Zulassen davon verhindert jedoch verwirrende Fehler durch Namenskollisionen von Modulen. Die erste Version dieses PEP enthielt keine Möglichkeit, Backends aus Dateien im Quellcode zu importieren, aber der Schlüssel backend-path wurde in der nächsten Revision hinzugefügt, um Projekten die Wahl dieses Verhaltens zu ermöglichen, falls erforderlich.

Zusammenfassung der Änderungen an PEP 517

Die folgenden Änderungen wurden an diesem PEP vorgenommen, nachdem die erste Referenzimplementierung in pip 19.0 veröffentlicht wurde.

  • Zyklen in den Build-Anforderungen wurden explizit verboten.
  • Unterstützung für In-Tree-Backends und Self-Hosting von Backends wurde durch die Einführung des Schlüssels backend-path in der Tabelle [build-system] hinzugefügt.
  • Es wurde klargestellt, dass das Backend setuptools.build_meta:__legacy__ PEP 517 eine akzeptable Alternative zur direkten Ausführung von setup.py für Quellcodebäume ist, die build-backend nicht explizit angeben.

Anhang A: Vergleich mit PEP 516

PEP 516 ist ein konkurrierender Vorschlag zur Spezifikation einer Build-System-Schnittstelle, der nun zugunsten dieses PEP abgelehnt wurde. Der Hauptunterschied besteht darin, dass unser Build-Backend über eine Python-Hook-basierte Schnittstelle definiert wird und nicht über eine Befehlszeilenschnittstelle.

Dieser Anhang dokumentiert die Argumente, die für dieses PEP gegenüber PEP 516 vorgebracht wurden.

Wir erwarten *nicht*, dass die Angabe von Python-Hooks anstelle von Befehlszeilenschnittstellen allein die Komplexität des Aufrufs des Backends reduziert, da Build-Frontends Hooks ohnehin in einem Kindprozess ausführen wollen – dies ist wichtig, um das Build-Frontend selbst vom Backend-Code zu isolieren und die Ausführungsumgebung der Build-Backends besser zu kontrollieren. Unter beiden Vorschlägen muss es also Code in pip geben, um einen Subprozess zu starten und mit einer Art Befehlszeilen-/IPC-Schnittstelle zu kommunizieren, und es muss Code im Subprozess geben, der diese Befehlszeilenargumente parsen und die eigentliche Build-Backend-Implementierung aufrufen kann. Dieses Diagramm gilt also für alle Vorschläge gleichermaßen.

+-----------+          +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|   (pip)   |          |   interface   |           | implementation |
+-----------+          +---------------+           +----------------+

Der Hauptunterschied zwischen den beiden Ansätzen ist, wie diese Schnittstellengrenzen auf die Projektstruktur abgebildet werden.

.-= This PEP =-.

+-----------+          +---------------+    |      +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|   (pip)   |          |   interface   |    |      | implementation |
+-----------+          +---------------+    |      +----------------+
                                            |
|______________________________________|    |
   Owned by pip, updated in lockstep        |
                                            |
                                            |
                                 PEP-defined interface boundary
                               Changes here require distutils-sig


.-= Alternative =-.

+-----------+    |     +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python-> |    backend     |
|   (pip)   |    |     |   interface   |           | implementation |
+-----------+    |     +---------------+           +----------------+
                 |
                 |     |____________________________________________|
                 |      Owned by build backend, updated in lockstep
                 |
    PEP-defined interface boundary
  Changes here require distutils-sig

Durch die Verlagerung der PEP-definierten Schnittstellengrenze in Python-Code gewinnen wir drei wichtige Vorteile.

Erstens, da es wahrscheinlich nur eine kleine Anzahl von Build-Frontends geben wird (pip und... vielleicht ein paar andere?), während es wahrscheinlich einen langen Schwanz von benutzerdefinierten Build-Backends geben wird (da diese von jedem Paket separat gewählt werden, um ihren spezifischen Build-Anforderungen zu entsprechen), sehen die eigentlichen Diagramme wahrscheinlich eher so aus:

.-= This PEP =-.

+-----------+          +---------------+           +----------------+
| frontend  | -spawn-> | child cmdline | -Python+> |    backend     |
|   (pip)   |          |   interface   |        |  | implementation |
+-----------+          +---------------+        |  +----------------+
                                                |
                                                |  +----------------+
                                                +> |    backend     |
                                                |  | implementation |
                                                |  +----------------+
                                                :
                                                :

.-= Alternative =-.

+-----------+          +---------------+           +----------------+
| frontend  | -spawn+> | child cmdline | -Python-> |    backend     |
|   (pip)   |       |  |   interface   |           | implementation |
+-----------+       |  +---------------+           +----------------+
                    |
                    |  +---------------+           +----------------+
                    +> | child cmdline | -Python-> |    backend     |
                    |  |   interface   |           | implementation |
                    |  +---------------+           +----------------+
                    :
                    :

Das heißt, dieses PEP führt zu weniger Gesamtcode im gesamten Ökosystem. Und insbesondere reduziert es die Eintrittsbarriere für die Erstellung eines neuen Build-Systems. Dies ist zum Beispiel ein vollständiges, funktionierendes Build-Backend:

# mypackage_custom_build_backend.py
import os.path
import pathlib
import shutil
import tarfile

SDIST_NAME = "mypackage-0.1"
SDIST_FILENAME = SDIST_NAME + ".tar.gz"
WHEEL_FILENAME = "mypackage-0.1-py2.py3-none-any.whl"

#################
# sdist creation
#################

def _exclude_hidden_and_special_files(archive_entry):
    """Tarfile filter to exclude hidden and special files from the archive"""
    if archive_entry.isfile() or archive_entry.isdir():
        if not os.path.basename(archive_entry.name).startswith("."):
            return archive_entry

def _make_sdist(sdist_dir):
    """Make an sdist and return both the Python object and its filename"""
    sdist_path = pathlib.Path(sdist_dir) / SDIST_FILENAME
    sdist = tarfile.open(sdist_path, "w:gz", format=tarfile.PAX_FORMAT)
    # Tar up the whole directory, minus hidden and special files
    sdist.add(os.getcwd(), arcname=SDIST_NAME,
              filter=_exclude_hidden_and_special_files)
    return sdist, SDIST_FILENAME

def build_sdist(sdist_dir, config_settings):
    """PEP 517 sdist creation hook"""
    sdist, sdist_filename = _make_sdist(sdist_dir)
    return sdist_filename

#################
# wheel creation
#################

def get_requires_for_build_wheel(config_settings):
    """PEP 517 wheel building dependency definition hook"""
    # As a simple static requirement, this could also just be
    # listed in the project's build system dependencies instead
    return ["wheel"]

def build_wheel(wheel_directory,
                metadata_directory=None, config_settings=None):
    """PEP 517 wheel creation hook"""
    from wheel.archive import archive_wheelfile
    path = os.path.join(wheel_directory, WHEEL_FILENAME)
    archive_wheelfile(path, "src/")
    return WHEEL_FILENAME

Natürlich ist dies ein *schreckliches* Build-Backend: Es erfordert, dass der Benutzer die Wheel-Metadaten manuell in src/mypackage-0.1.dist-info/ eingerichtet hat; wenn sich die Versionsnummer ändert, muss sie an mehreren Stellen manuell aktualisiert werden... aber es funktioniert, und weitere Funktionen könnten inkrementell hinzugefügt werden. Viel Erfahrung zeigt, dass große erfolgreiche Projekte oft als schnelle Hacks entstehen (z. B. Linux – „nur ein Hobby, wird nicht groß und professionell“; IPython/Jupyterdie $PYTHONSTARTUP-Datei eines Doktoranden), daher ist es wichtig, die Eintrittsbarriere zu minimieren, wenn unser Ziel darin besteht, das Wachstum eines lebendigen Ökosystems guter Build-Tools zu fördern.

Zweitens, da Python eine einfachere und gleichzeitig reichhaltigere Struktur zur Beschreibung von Schnittstellen bietet, entfernen wir unnötige Komplexität aus der Spezifikation – und Spezifikationen sind der schlimmste Ort für Komplexität, da die Änderung von Spezifikationen mühsame Konsensbildung über viele Stakeholder hinweg erfordert. Im Ansatz mit der Befehlszeilenschnittstelle müssen wir ad-hoc-Wege finden, um verschiedene Arten von Eingaben auf eine einzige lineare Befehlszeile abzubilden (z. B. wie vermeiden wir Kollisionen zwischen benutzerdefinierten Konfigurationsargumenten und PEP-definierten Argumenten? Wie geben wir optionale Argumente an? Bei der Arbeit mit einer Python-Schnittstelle haben diese Fragen einfache, offensichtliche Antworten). Beim Starten und Verwalten von Subprozessen gibt es viele knifflige Details, die richtig gemacht werden müssen, subtile plattformübergreifende Unterschiede, und einige der offensichtlichsten Ansätze – z. B. die Verwendung von stdout zur Rückgabe von Daten für die build_requires-Operation – können unerwartete Fallstricke erzeugen (z. B. was passiert, wenn die Berechnung der Build-Anforderungen das Starten einiger Kindprozesse erfordert und diese Kinder gelegentlich eine Fehlermeldung auf stdout ausgeben? Offensichtlich kann ein sorgfältiger Autor von Build-Backends dieses Problem vermeiden, aber der offensichtlichste Weg zur Definition einer Python-Schnittstelle eliminiert diese Möglichkeit vollständig, da der Hook-Rückgabewert klar abgegrenzt ist).

Im Allgemeinen bedeutet die Notwendigkeit, Build-Backends in ihren eigenen Prozess zu isolieren, dass wir die IPC-Komplexität nicht vollständig entfernen können – aber indem wir beide Seiten des IPC-Kanals unter die Kontrolle eines einzigen Projekts stellen, machen wir es viel billiger, Fehler in der IPC-Schnittstelle zu beheben, als wenn die Behebung von Fehlern koordinierte Zustimmung und koordinierte Änderungen im gesamten Ökosystem erfordert.

Drittens und am wichtigsten ist, dass der Python-Hook-Ansatz uns viel mächtigere Optionen für die Weiterentwicklung dieser Spezifikation in der Zukunft bietet.

Um konkret zu werden, stellen Sie sich vor, wir fügen nächstes Jahr einen neuen Hook build_sdist_from_vcs hinzu, der eine Alternative zum aktuellen Hook build_sdist bietet, bei der das Frontend für die Übergabe von Versionskontroll-Tracking-Metadaten an Backends verantwortlich ist (einschließlich der Angabe, wann alle auf der Festplatte befindlichen Dateien verfolgt werden), anstatt dass einzelne Backends diese Informationen selbst abfragen müssen. Um den Übergang zu bewältigen, möchten wir, dass Build-Frontends build_sdist_from_vcs transparent verwenden können, wenn es verfügbar ist, und andernfalls auf build_sdist zurückfallen; und wir möchten, dass Build-Backends beide Methoden definieren können, um mit alten und neuen Build-Frontends kompatibel zu sein.

Darüber hinaus sollte unser Mechanismus zwei weitere Ziele erfüllen: (a) Wenn neue Versionen von z. B. pip und flit beide aktualisiert werden, um die neue Schnittstelle zu unterstützen, dann sollte dies ausreichen, damit sie verwendet werden kann; insbesondere sollte es *nicht* notwendig sein, dass jedes Projekt, das flit *verwendet*, seine individuelle pyproject.toml-Datei aktualisiert. (b) Wir wollen keine zusätzlichen Prozesse starten, nur um diese Verhandlung durchzuführen, da Prozessstarts bei der Bereitstellung großer Multi-Package-Stacks auf einigen Plattformen (Windows) leicht zu einem Engpass werden können.

In der hier beschriebenen Schnittstelle sind all diese Ziele leicht zu erreichen. Da pip den Code steuert, der im Kindprozess ausgeführt wird, kann es diesen leicht so schreiben, dass er etwas wie folgt tut:

command, backend, args = parse_command_line_args(...)
if command == "build_sdist":
   if hasattr(backend, "build_sdist_from_vcs"):
       backend.build_sdist_from_vcs(...)
   elif hasattr(backend, "build_sdist"):
       backend.build_sdist(...)
   else:
       # error handling

In der Alternative, bei der die öffentliche Schnittstellengrenze am Subprozessaufruf platziert wird, ist dies nicht möglich – entweder müssen wir einen zusätzlichen Prozess starten, nur um abzufragen, welche Schnittstellen unterstützt werden (wie in einer früheren Version von PEP 516 enthalten, einer Alternative dazu), oder wir verzichten ganz auf die automatische Aushandlung (wie in der aktuellen Version dieses PEP), was bedeutet, dass jede Änderung an der Schnittstelle N einzelne Pakete erfordert, ihre pyproject.toml-Dateien aktualisieren müssen, bevor eine Änderung in Kraft treten kann, und dass Änderungen notwendigerweise auf neue Releases beschränkt sein werden.

Eine spezifische Konsequenz davon ist, dass wir in diesem PEP den Befehl prepare_metadata_for_build_wheel optional machen können. In unserem Design kann dies leicht von Build-Frontends gehandhabt werden, die Code in ihrem Subprozess-Runner platzieren können, wie z. B.:

def dump_wheel_metadata(backend, working_dir):
    """Dumps wheel metadata to working directory.

       Returns absolute path to resulting metadata directory
    """
    if hasattr(backend, "prepare_metadata_for_build_wheel"):
        subdir = backend.prepare_metadata_for_build_wheel(working_dir)
    else:
        wheel_fname = backend.build_wheel(working_dir)
        already_built = os.path.join(working_dir, "ALREADY_BUILT_WHEEL")
        with open(already_built, "w") as f:
            f.write(wheel_fname)
        subdir = unzip_metadata(os.path.join(working_dir, wheel_fname))
    return os.path.join(working_dir, subdir)

def ensure_wheel_is_built(backend, output_dir, working_dir, metadata_dir):
    """Ensures built wheel is available in output directory

       Returns absolute path to resulting wheel file
    """
    already_built = os.path.join(working_dir, "ALREADY_BUILT_WHEEL")
    if os.path.exists(already_built):
        with open(already_built, "r") as f:
            wheel_fname = f.read().strip()
        working_path = os.path.join(working_dir, wheel_fname)
        final_path = os.path.join(output_dir, wheel_fname)
        os.rename(working_path, final_path)
        os.remove(already_built)
    else:
        wheel_fname = backend.build_wheel(output_dir, metadata_dir=metadata_dir)
    return os.path.join(output_dir, wheel_fname)

und somit eine völlig einheitliche Schnittstelle für den Rest des Frontends bereitstellen, ohne zusätzliche Subprozessaufrufe, keine doppelten Builds usw. Aber offensichtlich ist dies die Art von Code, die man nur als Teil einer privaten, inner-projektbezogenen Schnittstelle schreiben möchte (z. B. erfordert das gegebene Beispiel, dass das Arbeitsverzeichnis zwischen den beiden Aufrufen geteilt wird, aber nicht mit anderen Wheel-Builds, und dass der Rückgabewert der Metadaten-Hilfsfunktion an den Wheel-Bau-Aufruf zurückgegeben wird).

(Und natürlich ist das optionale Machen des metadata-Befehls ein Teil der Senkung der Eintrittsbarriere für die Entwicklung neuer Backends, wie oben erwähnt.)

Andere Unterschiede

Neben dem oben beschriebenen Hauptunterschied zwischen Kommandozeile und Python-Hook gibt es einige weitere Unterschiede in diesem Vorschlag.

  • Der Metadaten-Befehl ist optional (wie oben beschrieben).
  • Wir geben Metadaten als Verzeichnis zurück, anstatt als einzelne METADATA-Datei. Dies entspricht besser der Art und Weise, wie Wheel-Metadaten in der Praxis über mehrere Dateien verteilt sind (z. B. Einstiegspunkte) und gibt uns zukünftig mehr Möglichkeiten. (Zum Beispiel könnten wir, anstatt dem PEP 426-Vorschlag zu folgen, das Format von METADATA auf JSON umzustellen, entscheiden, das bestehende METADATA für Abwärtskompatibilität beizubehalten und gleichzeitig neue Erweiterungen als JSON-„Sidecar“-Dateien im selben Verzeichnis hinzuzufügen. Oder auch nicht; der Punkt ist, dass es unsere Optionen offen hält.)
  • Wir stellen einen Mechanismus zur Übergabe von Informationen zwischen dem Metadaten-Schritt und dem Wheel-Bau-Schritt bereit. Ich schätze, jeder wird zustimmen, dass dies eine gute Idee ist?
  • Wir geben detailliertere Empfehlungen zum Build-Umfeld ab, diese sind jedoch ohnehin nicht normativ.

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

Zuletzt geändert: 2025-02-01 08:59:27 GMT