6. Usando Python no iOS
***********************

Autores:
   Russell Keith-Magee (2024-03)

Python no iOS é diferente do Python em plataformas de desktop. Em uma
plataforma desktop, o Python geralmente é instalado como um recurso do
sistema que pode ser usado por qualquer usuário daquele computador. Os
usuários então interagem com o Python executando um executável
**python** e inserindo comandos em um prompt interativo ou executando
um script Python.

No iOS, não existe o conceito de instalação como recurso do sistema. A
única unidade de distribuição de software é uma aplicação, ou "app".
Também não há console onde você possa executar um executável
**python** ou interagir com um REPL do Python.

Como resultado, a única maneira de usar Python no iOS é no modo
incorporado -- ou seja, escrevendo uma aplicação iOS nativo e
incorporando um interpretador Python usando "libPython" e invocando o
código Python usando API de incorporação do Python. O interpretador
Python completo, a biblioteca padrão e todo o seu código Python são
então empacotados como um pacote independente que pode ser distribuído
pela iOS App Store.

Se você deseja experimentar pela primeira vez escrever uma aplicação
iOS em Python, projetos como BeeWare e Kivy irão fornecer uma
experiência de usuário muito mais acessível. Esses projetos gerenciam
as complexidades associadas à execução de um projeto iOS, portanto,
você só precisa lidar com o próprio código Python.


6.1. Python em tempo de execução no iOS
=======================================


6.1.1. iOS version compatibility
--------------------------------

The minimum supported iOS version is specified at compile time, using
the "--host" option to "configure". By default, when compiled for iOS,
Python will be compiled with a minimum supported iOS version of 13.0.
To use a different miniumum iOS version, provide the version number as
part of the "--host" argument - for example, "--host=arm64-apple-
ios15.4-simulator" would compile an ARM64 simulator build with a
deployment target of 15.4.


6.1.2. Identificação da plataforma
----------------------------------

Ao executar no iOS, "sys.platform" reportará como "ios". Este valor
será retornado em um iPhone ou iPad, independentemente da aplicação
estar execitando no simulador ou em um dispositivo físico.

Informações sobre o ambiente de execução específico, incluindo a
versão do iOS, modelo do dispositivo e se o dispositivo é um
simulador, podem ser obtidas usando "platform.ios_ver()".
"platform.system()" reportará "iOS" ou "iPadOS", dependendo do
dispositivo.

"os.uname()" reporta detalhes em nível de kernel; ele reportará o nome
"Darwin".


6.1.3. Disponibilidade da biblioteca padrão
-------------------------------------------

A biblioteca padrão do Python tem algumas omissões e restrições
notáveis no iOS. Consulte o guia de disponibilidade de API para iOS
para obter detalhes.


6.1.4. Módulos de extensão binária
----------------------------------

Uma diferença notável sobre o iOS como plataforma é que a distribuição
da App Store impõe requisitos rígidos ao empacotamento de uma
aplicação. Um desses requisitos rege como os módulos de extensão
binária são distribuídos.

A iOS App Store exige que *todos* os módulos binários em uma aplicação
iOS sejam bibliotecas dinâmicas, contidas em um framework com
metadados apropriados, armazenados na pasta "Frameworks" da aplicação
empacotada. Pode haver apenas um único binário por framework, e não
pode haver nenhum material binário executável fora da pasta
"Frameworks".

Isto entra em conflito com a abordagem usual do Python para
distribuição de binários, que permite que um módulo de extensão
binária seja carregado de qualquer local em "sys.path". Para garantir
a conformidade com as políticas da App Store, um projeto iOS deve pós-
processar quaisquer pacotes Python, convertendo módulos binários ".so"
em estruturas independentes individuais com metadados e assinatura
apropriados. Para obter detalhes sobre como realizar esse pós-
processamento, consulte o guia para adicionar Python ao seu projeto.

Para ajudar o Python a descobrir binários em seu novo local, o arquivo
".so" original em "sys.path" é substituído por um arquivo ".fwork".
Este arquivo é um arquivo texto que contém a localização do binário do
framework, relativo ao pacote de aplicações. Para permitir que o
framework retorne ao local original, o framework deve conter um
arquivo ".origin" que contém a localização do arquivo ".fwork",
relativo ao pacote da aplicação.

Por exemplo, considere o caso de uma importação "from foo.bar import
_whiz", onde "_whiz" é implementado com o módulo binário
"sources/foo/bar/_whiz.abi3.so", com "sources" sendo o local
registrado em "sys.path", relativo ao pacote da aplicação. Este módulo
*deve* ser distribuído como
"Frameworks/foo.bar._whiz.framework/foo.bar._whiz" (criando o nome do
framework a partir do caminho de importação completo do módulo), com
um arquivo "Info.plist" no diretório ".framework" identificando o
binário como um framework. O módulo "foo.bar._whiz" seria representado
no local original com um arquivo marcador
"sources/foo/bar/_whiz.abi3.fwork", contendo o caminho
"Frameworks/foo.bar._whiz/foo.bar._whiz". O framework também conteria
"Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin", contendo o
caminho para o arquivo ".fwork".

Ao executar no iOS, o interpretador Python instalará um
"AppleFrameworkLoader" que é capaz de ler e importar arquivos
".fwork". Uma vez importado, o atributo "__file__" do módulo binário
reportará como a localização do arquivo ".fwork". Entretanto, o
"ModuleSpec" para o módulo carregado reportará a "origin" como a
localização do binário na pasta do framework.


6.1.5. Binários stub do compilador
----------------------------------

O Xcode não expõe compiladores explícitos para iOS; em vez disso, ele
usa um script "xcrun" que resolve um caminho  do compilador (por
exemplo, "xcrun --sdk iphoneos clang" para obter o "clang" para um
dispositivo iPhone). No entanto, usar este script apresenta dois
problemas:

* A saída de "xcrun" inclui caminhos que são específicos da máquina,
  resultando em um módulo sysconfig que não pode ser compartilhado
  entre usuários; e

* Isso resulta em definições "CC"/"CPP"/"LD"/"AR" que incluem espaços.
  Existem muitas ferramentas do ecossistema C que pressupõem que você
  pode dividir uma linha de comando no primeiro espaço para obter o
  caminho para o executável do compilador; este não é o caso ao usar
  "xcrun".

Para evitar esses problemas, o Python forneceu stubs para essas
ferramentas. Esses stubs são wrappers de script de shell em torno das
ferramentas subjacentes "xcrun", distribuídos em uma pasta "bin"
distribuída junto com a estrutura iOS compilada. Esses scripts são
relocáveis ​​e sempre serão resolvidos para os caminhos apropriados do
sistema local. Ao incluir esses scripts na pasta bin que acompanha um
framework, o conteúdo do módulo "sysconfig" se torna útil para
usuários finais compilarem seus próprios módulos. Ao compilar módulos
Python de terceiros para iOS, você deve garantir que esses binários
stub estejam no seu caminho.


6.2. Instalando Python no iOS
=============================


6.2.1. Ferramentas para construir aplicações de iOS
---------------------------------------------------

A construção para iOS requer o uso das ferramentas Xcode da Apple. É
altamente recomendável que você use a versão estável mais recente do
Xcode. Isso exigirá o uso da versão mais recente (ou segunda) do macOS
lançada recentemente, já que a Apple não mantém o Xcode para versões
mais antigas do macOS. As ferramentas de linha de comando do Xcode não
são suficientes para o desenvolvimento iOS; você precisa de uma
instalação *completa* do Xcode.

Se quiser executar seu código no simulador iOS, você também precisará
instalar um iOS Simulator Platform. Você deverá ser solicitado a
selecionar um iOS Simulator Platform ao executar o Xcode pela primeira
vez. Alternativamente, você pode adicionar um iOS Simulator Platform
selecionando na guia Platforms do painel Settings do Xcode.


6.2.2. Adicionando Python a um projeto iOS
------------------------------------------

Python pode ser adicionado a qualquer projeto iOS, usando Swift ou
Objective C. Os exemplos a seguir usarão Objective C; se você estiver
usando Swift, poderá achar uma biblioteca como PythonKit útil.

Para adicionar Python a um projeto Xcode de iOS:

1. Construa ou obtenha um "XCFramework" em Python. Veja as instruções
   em iOS/README.rst (na distribuição fonte do CPython) para detalhes
   sobre como construir um "XCFramework" em Python. No mínimo, você
   precisará de uma construção que tenha suporte "arm64-apple-ios",
   além de "arm64-apple-ios-simulator" ou "x86_64-apple-ios-
   simulator".

2. Arraste o "XCframework" para o seu projeto iOS. Nas instruções a
   seguir, presumiremos que você colocou o "XCframework" na raiz do
   seu projeto; no entanto, você pode usar qualquer outro local
   desejado ajustando os caminhos conforme necessário.

3. Arraste o arquivo "iOS/Resources/dylib-Info-template.plist" para o
   seu projeto e certifique-se de que ele esteja associado ao destino
   da aplicação.

4. Adicione o código da sua aplicação como uma pasta no seu projeto
   Xcode. Nas instruções a seguir, presumiremos que seu código de
   usuário está em uma pasta chamada "app" na raiz do seu projeto;
   você pode usar qualquer outro local ajustando os caminhos conforme
   necessário. Certifique-se de que esta pasta esteja associada ao
   destino da sua aplicação.

5. Selecione o destino da aplicação selecionando o nó raiz do seu
   projeto Xcode e, em seguida, o nome do destino na barra lateral que
   aparece.

6. Nas configurações de "General", em "Frameworks, Libraries and
   Embedded Content", adicione "Python.xcframework", com "Embed &
   Sign" selecionado.

7. Na guia "Build Settings", modifique o seguinte:

   * Build Options

     * User Script Sandboxing: No

     * Enable Testability: Yes

   * Search Paths

     * 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

8. Adicione uma etapa de construção que copie a biblioteca padrão do
   Python em sua aplicação. Na aba "Build Phases", adicione uma nova
   etapa de construção "Run Script" *antes* da etapa "Embed
   Frameworks", mas *depois* da etapa "Copy Bundle Resources". Nomeie
   a etapa como "Install Target Specific Python Standard Library",
   desative a caixa de seleção "Based on dependency analysis" e defina
   o conteúdo do script como:

      set -e

      mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib"
      if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then
          echo "Installing Python modules for iOS Simulator"
          rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
      else
          echo "Installing Python modules for iOS Device"
          rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/"
      fi

   Note que o nome da "fatia" do simulador no XCframework pode ser
   diferente, dependendo das arquiteturas de CPU que seu "XCFramework"
   provê suporte.

9. Adicione uma segunda etapa de construção que processe os módulos de
   extensão binária na biblioteca padrão no formato "Framework".
   Adicione uma etapa de construção "Run Script" *diretamente após*
   aquela que você adicionou na etapa 8, chamada "Prepare Python
   Binary Modules". Também deve ter "Based on dependency analysis"
   desmarcado, com o seguinte conteúdo de script:

      set -e

      install_dylib () {
          INSTALL_BASE=$1
          FULL_EXT=$2

          # The name of the extension file
          EXT=$(basename "$FULL_EXT")
          # The location of the extension file, relative to the bundle
          RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/}
          # The path to the extension file, relative to the install base
          PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/}
          # The full dotted name of the extension module, constructed from the file path.
          FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" ".");
          # A bundle identifier; not actually used, but required by Xcode framework packaging
          FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-")
          # The name of the framework folder.
          FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework"

          # If the framework folder doesn't exist, create it.
          if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then
              echo "Creating framework for $RELATIVE_EXT"
              mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER"
              cp "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
              plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
              plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist"
          fi

          echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
          mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME"
          # Create a placeholder .fwork file where the .so was
          echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork
          # Create a back reference to the .so file location in the framework
          echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin"
       }

       PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib")
       echo "Install Python $PYTHON_VER standard library extension modules..."
       find "$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload" -name "*.so" | while read FULL_EXT; do
          install_dylib python/lib/$PYTHON_VER/lib-dynload/ "$FULL_EXT"
       done

       # Clean up dylib template
       rm -f "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist"

       echo "Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..."
       find "$CODESIGNING_FOLDER_PATH/Frameworks" -name "*.framework" -exec /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "{}" \;

10. Adicione código Objective C para inicializar e usar um
    interpretador Python em modo embarcado. Você deve garantir que:

   * "Modo UTF-8" esteja *enabled*;

   * "Buffered stdio" esteja *disabled*;

   * "Escrita de bytecode" esteja *disabled*;

   * "Manipuladores de sinais" estejam *enabled*;

   * "PYTHONHOME" para o interpretador esteja configurado para apontar
     para a subpasta "python" do pacote da sua aplicação; e

   * O "PYTHONPATH" para o interpretador inclui:

     * a subpasta "python/lib/python3.X" do pacote da sua aplicação,

     * a subpasta "python/lib/python3.X/lib-dynload" do pacote da sua
       aplicação, e

     * a subpasta "app" do pacote da sua aplicação

   O local do pacote da sua aplicação pode ser determinada usando
   "[[NSBundle mainBundle] resourcePath]".

Os passos 8, 9 e 10 destas instruções presumem que você tem uma única
pasta de código de aplicação Python puro, chamada "app". Se você tiver
módulos binários de terceiros em sua aplicação, serão necessárias
algumas etapas adicionais:

* Você precisa garantir que todas as pastas que contenham binários de
  terceiros sejam associadas ao destino da aplicação ou copiadas como
  parte da etapa 8. A etapa 8 também deve limpar quaisquer binários
  que não sejam apropriados para a plataforma que uma construção
  específica está direcionando (ou seja, exclua todos os binários do
  dispositivo se estiver criando uma aplicação direcionado ao
  simulador).

* Quaisquer pastas que contenham binários de terceiros devem ser
  processadas no formato framework no passo 9. A invocação de
  "install_dylib" que processa a pasta "lib-dynload" pode ser copiada
  e adaptada para este propósito.

* Se você estiver usando uma pasta separada para pacotes de terceiros,
  certifique-se de que essa pasta esteja incluída como parte da
  configuração "PYTHONPATH" no passo 10.
