7. Python unter iOS verwenden
*****************************

Autoren:
   Russell Keith-Magee (2024-03)

Python unter iOS unterscheidet sich von Python auf Desktop-
Plattformen. Auf einer Desktop-Plattform wird Python in der Regel als
Systemressource installiert, die von jedem Benutzer dieses Computers
verwendet werden kann. Nutzer interagieren dann mit Python, indem sie
eine ausführbare Datei **python** starten und Befehle an einer
interaktiven Eingabeaufforderung eingeben, oder indem sie ein Python-
Skript ausführen.

Unter iOS gibt es kein Konzept der Installation als Systemressource.
Die einzige Verteilungseinheit für Software ist eine "App". Es gibt
auch keine Konsole, in der du eine ausführbare Datei **python**
starten oder mit einem Python-REPL interagieren könntest.

Infolgedessen ist die einzige Möglichkeit, Python unter iOS zu
verwenden, der eingebettete Modus – das heißt, indem du eine native
iOS-Anwendung schreibst, einen Python-Interpreter mithilfe von
"libPython" einbettest und Python-Code über die Python-Einbettungs-API
aufrufst. Der vollständige Python-Interpreter, die Standardbibliothek
und dein gesamter Python-Code werden dann als eigenständiges Paket
gebündelt, das über den iOS App Store vertrieben werden kann.

Wenn du zum ersten Mal damit experimentieren möchtest, eine iOS-App in
Python zu schreiben, bieten dir Projekte wie BeeWare und Kivy eine
deutlich zugänglichere Nutzererfahrung. Diese Projekte übernehmen die
Komplexität, die mit dem Zum-Laufen-Bringen eines iOS-Projekts
verbunden ist, sodass du dich nur um den eigentlichen Python-Code
kümmern musst.


7.1. Python zur Laufzeit unter iOS
==================================


7.1.1. iOS-Versionskompatibilität
---------------------------------

Die minimal unterstützte iOS-Version wird zur Kompilierzeit über die
Option "--host" von "configure" festgelegt. Standardmäßig wird Python
bei der Kompilierung für iOS mit einer minimal unterstützten iOS-
Version von 13.0 kompiliert. Um eine andere minimale iOS-Version zu
verwenden, gib die Versionsnummer als Teil des Arguments "--host" an –
zum Beispiel würde "--host=arm64-apple-ios15.4-simulator" einen ARM64
-Simulator-Build mit einem Deployment-Ziel von 15.4 kompilieren.


7.1.2. Plattform-Identifikation
-------------------------------

Bei der Ausführung unter iOS gibt "sys.platform" den Wert "ios"
zurück. Dieser Wert wird sowohl auf einem iPhone als auch auf einem
iPad zurückgegeben, unabhängig davon, ob die App im Simulator oder auf
einem physischen Gerät läuft.

Informationen zur konkreten Laufzeitumgebung, einschließlich der iOS-
Version, des Gerätemodells und ob es sich um einen Simulator handelt,
können mit "platform.ios_ver()" abgerufen werden. "platform.system()"
gibt je nach Gerät "iOS" oder "iPadOS" zurück.

"os.uname()" gibt Details auf Kernel-Ebene zurück. Es wird der Name
"Darwin" gemeldet.


7.1.3. Verfügbarkeit der Standardbibliothek
-------------------------------------------

Die Python-Standardbibliothek weist unter iOS einige wesentliche
Auslassungen und Einschränkungen auf. Siehe den API-
Verfügbarkeitsleitfaden für iOS für Details.


7.1.4. Binäre Erweiterungsmodule
--------------------------------

Ein wesentlicher Unterschied bei iOS als Plattform ist, dass die
Verteilung über den App Store strenge Anforderungen an die Paketierung
einer Anwendung stellt. Eine dieser Anforderungen regelt, wie binäre
Erweiterungsmodule verteilt werden.

Der iOS App Store verlangt, dass *alle* Binärmodule in einer iOS-App
dynamische Bibliotheken sein müssen, die in einem Framework mit
entsprechenden Metadaten enthalten sind und im Ordner "Frameworks" der
gepackten App gespeichert werden. Pro Framework darf es nur eine
einzige Binärdatei geben, und außerhalb des Ordners "Frameworks" darf
sich kein ausführbares Binärmaterial befinden.

Dies steht im Widerspruch zum üblichen Python-Ansatz zur Verteilung
von Binärdateien, der es erlaubt, ein binäres Erweiterungsmodul von
einem beliebigen Ort in "sys.path" zu laden. Um die Einhaltung der
App-Store-Richtlinien sicherzustellen, muss ein iOS-Projekt jedes
Python-Paket nachbearbeiten und dabei ".so"-Binärmodule in einzelne,
eigenständige Frameworks mit entsprechenden Metadaten und Signatur
umwandeln. Details zur Durchführung dieser Nachbearbeitung findest du
im Leitfaden zum Hinzufügen von Python zu deinem Projekt.

Damit Python die Binärdateien an ihrem neuen Speicherort finden kann,
wird die ursprüngliche ".so"-Datei in "sys.path" durch eine
".fwork"-Datei ersetzt. Diese Datei ist eine Textdatei, die den
Speicherort der Framework-Binärdatei relativ zum App-Bundle enthält.
Damit das Framework wieder zum ursprünglichen Speicherort zurückfinden
kann, muss es eine ".origin"-Datei enthalten, die den Speicherort der
".fwork"-Datei relativ zum App-Bundle enthält.

Betrachte zum Beispiel den Fall eines Imports "from foo.bar import
_whiz", wobei "_whiz" mit dem Binärmodul
"sources/foo/bar/_whiz.abi3.so" implementiert ist, wobei "sources" der
auf "sys.path" registrierte Speicherort relativ zum Anwendungsbündel
ist. Dieses Modul *muss* als
"Frameworks/foo.bar._whiz.framework/foo.bar._whiz" verteilt werden
(wobei der Framework-Name aus dem vollständigen Importpfad des Moduls
gebildet wird), mit einer "Info.plist"-Datei im
".framework"-Verzeichnis, die die Binärdatei als Framework
identifiziert. Das Modul "foo.bar._whiz" würde am ursprünglichen
Speicherort durch eine Markierungsdatei
"sources/foo/bar/_whiz.abi3.fwork" repräsentiert, die den Pfad
"Frameworks/foo.bar._whiz/foo.bar._whiz" enthält. Das Framework würde
außerdem "Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin"
enthalten, welches den Pfad zur ".fwork"-Datei enthält.

Beim Ausführen unter iOS installiert der Python-Interpreter einen
"AppleFrameworkLoader", der in der Lage ist, ".fwork"-Dateien zu lesen
und zu importieren. Nach dem Import gibt das Attribut "__file__" des
Binärmoduls den Speicherort der ".fwork"-Datei an. Die "ModuleSpec"
des geladenen Moduls gibt jedoch als "origin" den Speicherort der
Binärdatei im Framework-Ordner an.


7.1.5. Compiler-Stub-Binärdateien
---------------------------------

Xcode stellt für iOS keine expliziten Compiler bereit. Stattdessen
verwendet es ein "xcrun"-Skript, das zu einem vollständigen Compiler-
Pfad aufgelöst wird (z. B. "xcrun --sdk iphoneos clang", um den
"clang" für ein iPhone-Gerät zu erhalten). Die Verwendung dieses
Skripts bringt jedoch zwei Probleme mit sich:

* Die Ausgabe von "xcrun" enthält maschinenspezifische Pfade, was zu
  einem sysconfig-Modul führt, das nicht zwischen Benutzern geteilt
  werden kann, und

* Dies führt zu "CC"/"CPP"/"LD"/"AR"-Definitionen, die Leerzeichen
  enthalten. Es gibt viele Werkzeuge im C-Ökosystem, die davon
  ausgehen, dass man eine Befehlszeile am ersten Leerzeichen aufteilen
  kann, um den Pfad zur ausführbaren Compiler-Datei zu erhalten. Das
  ist bei der Verwendung von "xcrun" nicht der Fall.

Um diese Probleme zu vermeiden, stellt Python Stubs für diese
Werkzeuge bereit. Diese Stubs sind Shell-Skript-Wrapper um die
zugrunde liegenden "xcrun"-Werkzeuge, die in einem "bin"-Ordner
zusammen mit dem kompilierten iOS-Framework verteilt werden. Diese
Skripte sind verschiebbar und lösen sich stets zu den passenden
lokalen Systempfaden auf. Indem diese Skripte in den bin-Ordner
aufgenommen werden, der ein Framework begleitet, wird der Inhalt des
"sysconfig"-Moduls für Endnutzer nützlich, um ihre eigenen Module zu
kompilieren. Beim Kompilieren von Python-Modulen von Drittanbietern
für iOS solltest du sicherstellen, dass sich diese Stub-Binärdateien
in deinem Pfad befinden.


7.2. Python unter iOS installieren
==================================


7.2.1. Werkzeuge zum Erstellen von iOS-Apps
-------------------------------------------

Der Build-Prozess für iOS erfordert die Verwendung von Apples Xcode-
Werkzeugen. Es wird dringend empfohlen, die aktuellste stabile Xcode-
Version zu verwenden. Dies erfordert in der Regel die Verwendung der
neuesten (oder zweit-neuesten) veröffentlichten macOS-Version, da
Apple Xcode nicht für ältere macOS-Versionen pflegt. Die Xcode Command
Line Tools reichen für die iOS-Entwicklung nicht aus. Du benötigst
eine *vollständige* Xcode-Installation.

Wenn du deinen Code im iOS-Simulator ausführen möchtest, musst du
außerdem eine iOS Simulator Platform installieren. Beim ersten Start
von Xcode solltest du aufgefordert werden, eine iOS Simulator Platform
auszuwählen. Alternativ kannst du eine iOS Simulator Platform über den
Reiter "Platforms" im Xcode-Einstellungsfenster hinzufügen.


7.2.2. Python zu einem iOS-Projekt hinzufügen
---------------------------------------------

Python kann jedem iOS-Projekt hinzugefügt werden, sowohl mit Swift als
auch mit Objective C. Die folgenden Beispiele verwenden Objective C.
Wenn du Swift verwendest, könnte dir eine Bibliothek wie PythonKit
hilfreich sein.

Um Python zu einem iOS-Xcode-Projekt hinzuzufügen:

1. Baue oder beziehe ein Python-"XCFramework". Wie du ein
   Python-"XCFramework" baust, steht in der Anleitung unter
   Platforms/Apple/iOS/README.md (in der CPython-Quelldistribution).
   Mindestens brauchst du einen Build, der "arm64-apple-ios"
   unterstützt, dazu entweder "arm64-apple-ios-simulator" oder "x86_64
   -apple-ios-simulator".

2. Zieh das "XCframework" in dein iOS-Projekt. In der folgenden
   Anleitung nehmen wir an, dass du das "XCframework" in die Wurzel
   deines Projekts gelegt hast. Du kannst aber jeden anderen Ort
   verwenden und die Pfade entsprechend anpassen.

3. Füge deinen Anwendungscode als Ordner in dein Xcode-Projekt ein. In
   der folgenden Anleitung nehmen wir an, dass dein eigener Code in
   einem Ordner namens "app" in der Wurzel deines Projekts liegt. Du
   kannst jeden anderen Ort verwenden und die Pfade entsprechend
   anpassen. Stelle sicher, dass dieser Ordner dem Ziel deiner App
   zugeordnet ist.

4. Wähle das App-Ziel aus, indem du den Wurzelknoten deines Xcode-
   Projekts anklickst und dann den Zielnamen in der erscheinenden
   Seitenleiste.

5. Füge in den Einstellungen unter „General“ bei „Frameworks,
   Libraries and Embedded Content“ das "Python.xcframework" hinzu, mit
   „Embed & Sign“ ausgewählt.

6. Ändere im Reiter „Build Settings“ Folgendes:

   * Build Options

     * User Script Sandboxing: No

     * Enable Testability: Yes

   * Pfade suchen

     * Framework Search Paths: "$(PROJECT_DIR)"

     * Header Search Paths:
       ""$(BUILT_PRODUCTS_DIR)/Python.framework/Headers""

   * Apple Clang - Warnings - All languages

     * Quoted Include In Framework Header: No

7. Füge einen Build-Schritt hinzu, der die Python-Standardbibliothek
   und deine eigenen binären Python-Abhängigkeiten verarbeitet. Füge
   im Reiter „Build Phases“ einen neuen Build-Schritt „Run Script“
   ein, und zwar *vor* dem Schritt „Embed Frameworks“, aber *nach* dem
   Schritt „Copy Bundle Resources“. Nenne den Schritt „Process Python
   libraries“, schalte das Kontrollkästchen „Based on dependency
   analysis“ ab und setze den Inhalt des Skripts auf:

      set -e
      source $PROJECT_DIR/Python.xcframework/build/build_utils.sh
      install_python Python.xcframework app

   Wenn du dein XCframework an einem anderen Ort als der Wurzel deines
   Projekts abgelegt hast, passe den Pfad im ersten Argument an.

8. Füge Objective-C-Code hinzu, um einen Python-Interpreter im
   eingebetteten Modus zu starten und zu verwenden. Du solltest
   sicherstellen, dass:

   * der UTF-8-Modus ("PyPreConfig.utf8_mode") *eingeschaltet* ist;

   * die gepufferte Standard-Ein-/Ausgabe ("PyConfig.buffered_stdio")
     *abgeschaltet* ist;

   * das Schreiben von Bytecode ("PyConfig.write_bytecode")
     *abgeschaltet* ist;

   * die Signalbehandlungen ("PyConfig.install_signal_handlers")
     *eingeschaltet* sind;

   * die Systemprotokollierung ("PyConfig.use_system_logger")
     *eingeschaltet* ist (optional, aber dringend empfohlen,
     standardmäßig eingeschaltet);

   * "PYTHONHOME" für den Interpreter auf den Unterordner "python" des
     Bündels deiner App zeigt, und

   * der "PYTHONPATH" für den Interpreter Folgendes enthält:

     * den Unterordner "python/lib/python3.X" des Bündels deiner App,

     * den Unterordner "python/lib/python3.X/lib-dynload" des Bündels
       deiner App, und

     * den Unterordner "app" des Bündels deiner App

   Den Ort des Bündels deiner App kannst du über "[[NSBundle
   mainBundle] resourcePath]" ermitteln.

Die Schritte 7 und 8 dieser Anleitung setzen voraus, dass du einen
einzigen Ordner mit reinem Python-Anwendungscode namens "app" hast.
Wenn deine App binäre Module von Dritten enthält, sind zusätzliche
Schritte nötig:

* Du musst sicherstellen, dass alle Ordner mit Binärdateien von
  Dritten entweder dem App-Ziel zugeordnet oder in Schritt 7
  ausdrücklich mitkopiert werden. Schritt 7 sollte außerdem alle
  Binärdateien entfernen, die für die Zielplattform des jeweiligen
  Builds nicht passen (also etwa Gerätebinärdateien löschen, wenn du
  eine App für den Simulator baust).

* Wenn du einen eigenen Ordner für Pakete von Dritten verwendest,
  stelle sicher, dass dieser Ordner am Ende des Aufrufs von
  "install_python" in Schritt 7 und in der "PYTHONPATH"-Konfiguration
  in Schritt 8 ergänzt wird.

* Wenn einer der Ordner mit Paketen von Dritten ".pth"-Dateien
  enthält, solltest du diesen Ordner als *Site-Verzeichnis* hinzufügen
  (mit "site.addsitedir()"), statt ihn direkt zu "PYTHONPATH" oder
  "sys.path" hinzuzufügen.


7.2.3. Ein Python-Paket testen
------------------------------

Der CPython-Quellbaum enthält ein Testbed-Projekt, mit dem die
CPython-Testsammlung im iOS-Simulator ausgeführt wird. Dieses Testbed
lässt sich auch als Testbed-Projekt verwenden, um die Testsammlung
deiner eigenen Python-Bibliothek unter iOS auszuführen.

Nachdem du ein iOS-XCFramework gebaut oder bezogen hast (Einzelheiten
unter Platforms/Apple/iOS/README.md), erstelle einen Klon des Python-
Testbed-Projekts für iOS. Wenn du das Build-Skript "Platforms/Apple"
verwendet hast, um das XCframework zu bauen, kannst du Folgendes
ausführen:

   $ python cross-build/iOS/testbed clone --app <path/to/module1> --app <path/to/module2> app-testbed

Oder, wenn du dein eigenes XCframework beschafft hast, mit:

   $ python Platforms/Apple/testbed clone --platform iOS --framework <path/to/Python.xcframework> --app <path/to/module1> --app <path/to/module2> app-testbed

Alle mit dem Schalter "--app" angegebenen Ordner werden in das
geklonte Testbed-Projekt kopiert. Das entstehende Testbed liegt im
Ordner "app-testbed". In diesem Beispiel wären "module1" und "module2"
zur Laufzeit importierbare Module. Hat dein Projekt weitere
Abhängigkeiten, lassen sich diese in den Ordner "app-
testbed/Testbed/app_packages" installieren (mit "pip install --target
app-testbed/Testbed/app_packages" oder Ähnlichem).

Anschließend kannst du den Ordner "app-testbed" verwenden, um die
Testsammlung deiner App auszuführen. Wenn zum Beispiel "module1.tests"
der Einsprungpunkt deiner Testsammlung war, könntest du Folgendes
ausführen:

   $ python app-testbed run -- module1.tests

Das entspricht dem Aufruf von "python -m module1.tests" auf einem
Desktop-Python-Build. Alle Argumente nach dem "--" werden an das
Testbed weitergegeben, als wären sie Argumente von "python -m" auf
einem Desktop-Rechner.

Du kannst das Testbed-Projekt auch in Xcode öffnen:

   $ open app-testbed/iOSTestbed.xcodeproj

Damit stehen dir die vollständigen Xcode-Werkzeuge zur Fehlersuche zur
Verfügung.

Die Argumente, mit denen die Testsammlung ausgeführt wird, sind Teil
des Testplans. Um den Testplan zu ändern, wähle den Testplan-Knoten im
Projektbaum (er sollte das erste Kind des Wurzelknotens sein) und dann
den Reiter „Configurations“. Ändere den Wert „Arguments Passed On
Launch“, um die Testargumente anzupassen.

Der Testplan schaltet außerdem paralleles Testen ab und gibt die
Verwendung der Datei "Testbed.lldbinit" für die Konfiguration des
Debuggers vor. Die Standardkonfiguration des Debuggers schaltet
automatische Haltepunkte bei den Signalen "SIGINT", "SIGUSR1",
"SIGUSR2" und "SIGXFSZ" ab.


7.3. Vorgaben des App Store erfüllen
====================================

Der einzige Weg, Apps an fremde iOS-Geräte zu verteilen, ist die
Einreichung im iOS App Store. Zur Verteilung eingereichte Apps müssen
Apples App-Prüfverfahren durchlaufen. Dieses Verfahren umfasst eine
Reihe automatischer Prüfregeln, die das eingereichte Anwendungsbündel
auf bedenklichen Code untersuchen. Es gibt einige Schritte, die nötig
sind, damit deine App diese Prüfungen besteht.


7.3.1. Unverträglicher Code in der Standardbibliothek
-----------------------------------------------------

Die Python-Standardbibliothek enthält Code, von dem bekannt ist, dass
er gegen diese automatischen Regeln verstößt. Diese Verstöße scheinen
zwar Fehlalarme zu sein, Apples Prüfregeln lassen sich aber nicht
anfechten. Deshalb ist es nötig, die Python-Standardbibliothek zu
verändern, damit eine App die Prüfung des App Store besteht.

Der Python-Quellbaum enthält eine Patch-Datei, die allen Code
entfernt, von dem bekannt ist, dass er im Prüfverfahren des App Store
Schwierigkeiten macht. Dieser Patch wird beim Bauen für iOS
automatisch angewandt.


7.3.2. Datenschutzmanifeste
---------------------------

Im April 2025 hat Apple die Anforderung eingeführt, dass bestimmte
Bibliotheken von Dritten ein Privacy Manifest bereitstellen müssen.
Wenn du also ein binäres Modul hast, das eine der betroffenen
Bibliotheken verwendet, musst du für diese Bibliothek eine Datei
".xcprivacy" bereitstellen. OpenSSL ist eine der betroffenen
Bibliotheken, es gibt aber weitere.

Wenn du ein binäres Modul namens "mymodule.so" erzeugst und das in
Schritt 7 beschriebene Xcode-Build-Skript verwendest, kannst du eine
Datei "mymodule.xcprivacy" neben "mymodule.so" legen — das
Datenschutzmanifest wird dann beim Umwandeln des binären Moduls in ein
Framework an den erforderlichen Ort installiert.
