DEV Community

Cover image for Embedding Python interpreter inside a MacOS app (and iOS app), and publish to the App Store successfully.
Eldar E.
Eldar E.

Posted on • Updated on

Embedding Python interpreter inside a MacOS app (and iOS app), and publish to the App Store successfully.

The struggle was real

When I started looking into this topic, I quickly realized most of the guides, tutorials and StackOverflow answers have only partial information or straight out give bad advice, that would prevent you from publishing your app on the Apple's App Store.

I had to scramble though all that and was finally able to embed a Python interpreter in a MacOS app, fully signed and with the ability to be published to the App Store.

So here it is, a quick and simple guide on how-to embed a Python interpreter into your app. It's really simple when you have clear steps.

Why would we even need this?

As a native Apple eco system developer (I mostly do iOS these days), I believe Swift is the only way to go when developing for iOS and MacOS.
But, there are times your app requires something big you don't want to reinvent, and it's only available in Python. Then why not use it?

Python has so many tools and Open Source code you can use straight out of the box, that can empower your application with amazing functionality, and you can easily call Python code from Swift.

But be careful, by embedding a Python interpreter in your binary, you enlarge it by around ~100MB. So I wouldn't recommend this for simple stuff you can either do yourself in Swift or find a well maintained SPM.

Can you publish these kind of MacOS apps on the App Store?

Yes. You can embed Python and publish to the MacOS App Store.

While MacOS already comes with Python, you don't want to use it.

  1. You can't rely on the Python version installed on the system.
  2. This will require you to delete your MacOS app's Sandbox and Disable Library Validation. And once you do it, you can't submit your app to the App Store, and you even might have issues with the Notarization outside the App Store: https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution

On the other hand, by embedding the Python interpreter you allow the Python part of your app to be signed as well.
This allows the Hardened Runtime to remain intact:

  • NO NEED to delete the Sandbox - you need it to be able to submit your MacOS App to the App Store.
  • NO NEED to Disable Library Validation.

What about iOS?

Yes. You can embed Python and publish to the iOS App Store. Just look at Pyto: https://pyto.app
There is no limitation from iOS App Store on doing so. It's all signed as a single app.
The process of embedding Python on iOS is very similar to MacOS, but it might require a few more steps I will cover in a future article.

Step-by-step to embed Python interpreter in a MacOS app

  1. Add PythonKit SPM:
    https://github.com/pvieito/PythonKit.

  2. Download Released framework for the desired Python version (for MacOS platform):
    https://github.com/beeware/Python-Apple-support

  3. Extract the python-stdlib and Python.xcframework from the tag.gz archive.

  4. Copy them to the root of the MacOS App, preferably via Xcode.

  5. Xcode General -> Frameworks:
    5.1. Should already be there:
    - Python.xcframework is set as Do Not Embed
    - PythonKit
    5.2. Add additional required framework:
    - SystemConfiguration.framework set as Do Not Embed

  6. Xcode Build Phases:
    6.1. Verify Copy Bundle Resources contains python-stdlib.
    6.2. Add bash script to Sign .so binaries in python-stdlib/lib-dynload/:
    IMPORTANT NOTE: .so binaries must be signed with your TeamID, if you need to use Sign and Run Locally it will be signed as ad-hoc, and you will need to Disable Library Validation.

set -e
echo "Signing as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)"
find "$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload" -name "*.so" -exec /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der {} \;
Enter fullscreen mode Exit fullscreen mode
  1. Create a file called module.modulemap with the following code:
module Python {
    umbrella header "Python.h"
    export *
    link "Python"
}
Enter fullscreen mode Exit fullscreen mode
  1. Place the module.modulemap file inside the Python.xcframework/macos-arm64_x86_64/Headers/.
    This will allow us to do import Python

  2. Init Python at runtime, as early as possible:

import Python

guard let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil) else { return }
guard let libDynloadPath = Bundle.main.path(forResource: "python-stdlib/lib-dynload", ofType: nil) else { return }
setenv("PYTHONHOME", stdLibPath, 1)
setenv("PYTHONPATH", "\(stdLibPath):\(libDynloadPath)", 1)
Py_Initialize()
// we now have a Python interpreter ready to be used
Enter fullscreen mode Exit fullscreen mode
  1. Run test code:
import PythonKit

let sys = Python.import("sys")
print("Python Version: \(sys.version_info.major).\(sys.version_info.minor)")
print("Python Encoding: \(sys.getdefaultencoding().upper())")
print("Python Path: \(sys.path)")

_ = Python.import("math") // verifies `lib-dynload` is found and signed successfully
Enter fullscreen mode Exit fullscreen mode
  1. We're in business. Now we can add whatever python code we want. To integrate 3rd party python code and dependencies, you will need to make sure PYTHONPATH contains their paths; And then you can just do Python.import(" <SOME LIB> "). Sometimes, the python code might be too complicated to call with PythonKit from Swift, so my recommendation is to write a small Python script and call that script's method from Swift instead.

Good luck.

^(;,;)^

UPDATE:
I've submitted the above steps to be the official usage guide for BeeWare's Python-Apple-support. If you're having trouble with the above steps, the usage guide might contain additional info: https://github.com/beeware/Python-Apple-support/blob/main/USAGE.md.

Latest comments (1)

Collapse
 
curzelit profile image
Federico Curzel

This was super interesting and super helpful, thanks a lot!
I've been migrating parts of my macOS app to Python, and might finally ship it to windows soon...
Export and app store upload worked fine, BUT - there's always a but :)
If I export the .app and try to run it, i get this:

PythonKit/PythonLibrary.swift:59: Fatal error: 'try!' expression unexpectedly raised an error: Python library not found. Set the PYTHON_LIBRARY environment variable with the path to a Python library.

Any ideas? :D
Thanks in advance!