6. Использование Python на iOS

Авторы:

Рассел Кит-Мейджи (2024-03)

Python на iOS отличается от Python на настольных платформах. На настольных платформах Python обычно устанавливается как системный ресурс, который может использовать любой пользователь компьютера. Пользователи взаимодействуют с Python, запуская исполняемый файл python и вводя команды в интерактивной подсказке, или запуская сценарий Python.

В iOS нет понятия установки как системного ресурса. Единственной единицей распространения программного обеспечения является «приложение». Также нет консоли, где можно было бы запустить исполняемый файл python или взаимодействовать с Python REPL.

В результате единственный способ использовать Python на iOS - это встроенный режим, то есть написать родное приложение для iOS, встроить в него интерпретатор Python, используя libPython, и вызывать код Python, используя Python embedding API. Полный интерпретатор Python, стандартная библиотека и весь ваш Python-код затем упаковываются в отдельный пакет, который можно распространять через iOS App Store.

Если вы хотите впервые попробовать написать iOS-приложение на Python, такие проекты, как BeeWare и Kivy, обеспечат вам гораздо более доступный пользовательский опыт. Эти проекты справляются со всеми сложностями, связанными с запуском iOS-проекта, так что вам нужно будет разобраться только с самим кодом на Python.

6.1. Python во время выполнения на iOS

6.1.1. Идентификация платформы

При выполнении на iOS значение sys.platform будет отображаться как ios. Это значение будет возвращено на iPhone или iPad, независимо от того, запущено ли приложение на симуляторе или на физическом устройстве.

Информацию о конкретной среде выполнения, включая версию iOS, модель устройства и наличие симулятора, можно получить с помощью platform.ios_ver(). platform.system() сообщит iOS или iPadOS, в зависимости от устройства.

os.uname() сообщает подробности на уровне ядра; он сообщит имя Darwin.

6.1.2. Наличие стандартной библиотеки

Стандартная библиотека Python имеет некоторые заметные упущения и ограничения для iOS. Подробности см. в разделе API availability guide for iOS.

6.1.3. Двоичные модули расширения

Одно из заметных отличий iOS как платформы заключается в том, что распространение App Store накладывает жесткие требования на упаковку приложения. Одно из этих требований определяет, как распространяются бинарные модули расширения.

iOS App Store требует, чтобы все двоичные модули в приложении для iOS были динамическими библиотеками, содержащимися во фреймворке с соответствующими метаданными и хранящимися в папке Frameworks упакованного приложения. На каждый фреймворк может приходиться только один двоичный файл, а за пределами папки Frameworks не может быть исполняемого двоичного материала.

Это противоречит обычному подходу Python к распространению двоичных файлов, который позволяет загружать двоичный модуль расширения из любого места на sys.path. Чтобы обеспечить соответствие политикам App Store, проект iOS должен постобработать все пакеты Python, преобразовав бинарные модули .so в отдельные автономные фреймворки с соответствующими метаданными и подписью. Подробнее о том, как выполнить эту постобработку, см. в руководстве для adding Python to your project.

Чтобы помочь Python обнаружить двоичные файлы в новом месте, оригинальный файл .so на sys.path заменяется файлом .fwork. Этот файл представляет собой текстовый файл, содержащий расположение двоичного файла фреймворка относительно пакета приложений. Чтобы фреймворк мог вернуться в исходное расположение, он должен содержать файл .origin, содержащий расположение файла .fwork относительно пакета приложений.

Например, рассмотрим случай импорта from foo.bar import _whiz, где _whiz реализован с помощью бинарного модуля sources/foo/bar/_whiz.abi3.so, причем sources - это место, зарегистрированное в sys.path, относительно пакета приложения. Этот модуль должен распространяться как Frameworks/foo.bar._whiz.framework/foo.bar._whiz (создавая имя фреймворка из полного пути импорта модуля), с файлом Info.plist в каталоге .framework, идентифицирующим бинарник как фреймворк. Модуль foo.bar._whiz будет представлен в исходном расположении с помощью файла-маркера sources/foo/bar/_whiz.abi3.fwork, содержащего путь Frameworks/foo.bar._whiz/foo.bar._whiz. Фреймворк также будет содержать Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin, содержащий путь к файлу .fwork.

При работе на iOS интерпретатор Python установит AppleFrameworkLoader, способный читать и импортировать .fwork файлы. После импорта атрибут __file__ бинарного модуля будет сообщать о местоположении .fwork файла. Однако атрибут ModuleSpec загруженного модуля будет указывать на origin как на местоположение бинарного файла в папке framework.

6.1.4. Исполняемые файлы-заглушки компилятора

Xcode не предоставляет явных компиляторов для iOS; вместо этого он использует скрипт xcrun, который разрешает полный путь к компилятору (например, xcrun --sdk iphoneos clang для получения clang для устройства iPhone). Однако использование этого скрипта создает две проблемы:

  • Вывод xcrun включает пути, специфичные для конкретной машины, что приводит к созданию модуля sysconfig, который нельзя использовать совместно с другими пользователями; и

  • Это приводит к тому, что определения CC/CPP/LD/AR включают пробелы. Многие инструменты экосистемы Си предполагают, что вы можете разделить командную строку на первый пробел, чтобы получить путь к исполняемому файлу компилятора; это не так, если использовать xcrun.

Чтобы избежать этих проблем, Python предоставил заглушки для этих инструментов. Эти заглушки представляют собой обертки сценариев оболочки вокруг инструментов xcrun, распространяемых в папке bin, находящейся вместе со скомпилированным фреймворком iOS. Эти скрипты можно перемещать, и они всегда будут преобразовываться в соответствующие локальные системные пути. Благодаря включению этих скриптов в папку bin, сопровождающую фреймворк, содержимое модуля sysconfig становится полезным для конечных пользователей при компиляции их собственных модулей. При компиляции сторонних модулей Python для iOS необходимо убедиться, что эти двоичные файлы-заглушки находятся на вашем пути.

6.2. Установка Python на iOS

6.2.1. Инструменты для создания приложений для iOS

Сборка под iOS требует использования инструментария Apple Xcode. Настоятельно рекомендуется использовать самый последний стабильный выпуск Xcode. Это потребует использования самой (или второй по счету) последней версии macOS, так как Apple не поддерживает Xcode для старых версий macOS. Инструментов командной строки Xcode недостаточно для разработки iOS; вам нужна полная установка Xcode.

Если вы хотите запустить свой код на симуляторе iOS, вам также потребуется установить iOS Simulator Platform. При первом запуске Xcode вам должно быть предложено выбрать платформу для симулятора iOS. Кроме того, вы можете добавить платформу iOS Simulator Platform, выбрав ее на вкладке Platforms панели настроек Xcode.

6.2.2. Добавление Python в проект iOS

Python можно добавить в любой iOS-проект, используя либо Swift, либо Objective C. В следующих примерах используется Objective C; если вы используете Swift, вам может пригодиться библиотека вроде PythonKit.

Чтобы добавить Python в проект iOS Xcode:

  1. Соберите или получите Python XCFramework. Смотрите инструкции в iOS/README.rst (в исходном дистрибутиве CPython) для получения подробной информации о том, как собрать Python XCFramework. Как минимум, вам понадобится сборка, поддерживающая arm64-apple-ios, плюс один из arm64-apple-ios-simulator или x86_64-apple-ios-simulator.

  2. Перетащите XCframework в свой проект iOS. В следующих инструкциях мы будем считать, что вы перетащили XCframework в корень вашего проекта; однако вы можете использовать любое другое место, корректируя пути по мере необходимости.

  3. Перетащите файл iOS/Resources/dylib-Info-template.plist в свой проект и убедитесь, что он связан с целевым приложением.

  4. Добавьте код приложения в качестве папки в проект Xcode. В следующих инструкциях мы предположим, что ваш пользовательский код находится в папке с именем app в корне вашего проекта; вы можете использовать любое другое место, изменяя пути по мере необходимости. Убедитесь, что эта папка связана с целью вашего приложения.

  5. Выберите цель приложения, выбрав корневой узел вашего проекта Xcode, а затем имя цели в появившейся боковой панели.

  6. В настройках «Общие», в разделе «Фреймворки, библиотеки и встроенный контент», добавьте Python.xcframework, выбрав «Embed & Sign».

  7. На вкладке «Параметры сборки» измените следующие параметры:

    • Варианты сборки

      • Песочница пользовательских сценариев: Нет

      • Включить тестируемость: Да

    • Пути поиска

      • Пути поиска фреймворка: $(PROJECT_DIR)

      • Пути поиска заголовков: "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"

    • Apple Clang - Предупреждения - Все языки

      • Котировки включаются в заголовок фреймворка: Нет

  8. Добавьте шаг сборки, который копирует стандартную библиотеку Python в ваше приложение. На вкладке «Build Phases» добавьте новый шаг сборки «Run Script» до шага «Embed Frameworks», но после шага «Copy Bundle Resources». Назовите шаг «Install Target Specific Python Standard Library», отключите флажок «Based on dependency analysis» и задайте содержание сценария:

    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
    

    Обратите внимание, что название «кусочка» симулятора в XCframework может быть другим, в зависимости от архитектуры процессора, которую поддерживает ваш XCFramework.

  9. Добавьте второй шаг сборки, который обрабатывает бинарные модули расширения из стандартной библиотеки в формат «Framework». Добавьте шаг сборки «Run Script» прямо после шага, который вы добавили в шаге 8, под названием «Prepare Python Binary Modules». На нем также должен быть снят флажок «Based on dependency analysis», а содержание сценария должно быть следующим:

    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. Добавьте код Objective C для инициализации и использования интерпретатора Python во встроенном режиме. Вы должны убедиться, что:

  • UTF-8 mode является включенным;

  • Buffered stdio является отключенным;

  • Writing bytecode является отключенным;

  • Signal handlers являются включенными;

  • PYTHONHOME для интерпретатора настроен так, чтобы указывать на подпапку python в папке вашего приложения; и

  • В состав PYTHONPATH для интерпретатора входят:

    • в подпапке python/lib/python3.X пакета вашего приложения,

    • в подпапку python/lib/python3.X/lib-dynload пакета вашего приложения, и

    • вложенная папка app в пакете вашего приложения

Местоположение связки вашего приложения можно определить с помощью [[NSBundle mainBundle] resourcePath].

Шаги 8, 9 и 10 этой инструкции предполагают, что у вас есть одна папка с чистым кодом приложения на Python, названная app. Если в вашем приложении есть бинарные модули сторонних разработчиков, потребуются дополнительные шаги:

  • Необходимо убедиться, что все папки, содержащие сторонние исполняемые файлы, либо связаны с целевым приложением, либо скопированы в них в рамках шага 8. На шаге 8 также следует очистить все двоичные файлы, которые не подходят для платформы, на которую нацелена конкретная сборка (например, удалите все двоичные файлы устройств, если вы собираете приложение для симулятора).

  • Все папки, содержащие двоичные файлы сторонних разработчиков, должны быть обработаны в виде фреймворка на шаге 9. Вызов install_dylib, который обрабатывает папку lib-dynload, можно скопировать и адаптировать для этой цели.

  • Если вы используете отдельную папку для пакетов сторонних разработчиков, убедитесь, что эта папка включена в конфигурацию PYTHONPATH на шаге 10.