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.
- You can't rely on the Python version installed on the system.
- 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
Add PythonKit SPM:
https://github.com/pvieito/PythonKit.Download Released framework for the desired Python version (for MacOS platform):
https://github.com/beeware/Python-Apple-supportExtract the
python-stdlib
andPython.xcframework
from thetag.gz
archive.Copy them to the root of the MacOS App, preferably via Xcode.
Xcode General -> Frameworks:
5.1. Should already be there:
-Python.xcframework
is set asDo Not Embed
-PythonKit
5.2. Add additional required framework:
-SystemConfiguration.framework
set asDo Not Embed
Xcode
Build Phases
:
6.1. VerifyCopy Bundle Resources
containspython-stdlib
.
6.2. Add bash script to Sign.so
binaries inpython-stdlib/lib-dynload/
:
IMPORTANT NOTE:.so
binaries must be signed with your TeamID, if you need to useSign and Run Locally
it will be signed as ad-hoc, and you will need toDisable 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 {} \;
- Create a file called
module.modulemap
with the following code:
module Python {
umbrella header "Python.h"
export *
link "Python"
}
Place the
module.modulemap
file inside thePython.xcframework/macos-arm64_x86_64/Headers/
.
This will allow us to doimport Python
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
- 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
- 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 doPython.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.
Top comments (1)
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:
Any ideas? :D
Thanks in advance!