Embedding Python interpreter inside a MacOS app (and iOS app), and publish to the App Store successfully.
Tags: swift, pythonkit, macos, ios
Published: 2022-12-03 13:20:37


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`.
```bash
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 {} \;
```

7. Create a file called `module.modulemap` with the following code:
```
module Python {
    umbrella header "Python.h"
    export *
    link "Python"
}
```

8. Place the `module.modulemap` file inside the `Python.xcframework/macos-arm64_x86_64/Headers/`.
This will allow us to do `import Python`

9. Init Python at runtime, as early as possible:
```swift
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
```

10. Run test code:
```swift
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
```

11. 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.