Standalone python apps using py2app

Py2app can be used to build python stand alone apps for Mac OS. However it's not as straightforward as the docs seem to suggest:

A Python runtime not could be located.  You may need to install a framework build of Python, or edit the PyRuntimeLocations array in this application's Info.plist file.  

If you've seen this error, you know how frustrating it can get. Turns out the framework isn't actually copied when you build it and specifying PyRuntimeLocations doesn't actually work for a standalone app. If you specify it, that Framework needs to exist at the url specified in your Info.plist on the host that's running the app. So, don't bother with how the error asks us to solve it.

Right way to build it

It takes a bunch of other steps to make sure we have an actual python standalone app that can be used by anyone that has a Mac machine.

Here are some important points before you start:

  • Build this app using an independent python build. So, something like Homebrew (at this point python3 v3.7 was available) or pyenv but with the flags that allow us to download the framework with it.
  • Find the locations for these Python frameworks. If you're going the Homebrew route, you'll find it here. /usr/local/Cellar/python/3.7.0/Frameworks/Python.framework

The first steps are taken from the docs:

  1. Create a setup.py file using py2applet:
    py2applet --make-setup app.py

  2. When you have the setup.py file ready, make sure to create the dist and build directories one step above the setup.py. So in short, you have to refactor your code to be in another sub directory. I've detailed this step here

  3. Now choose and icon file, but don't use py2applet for that. Just pass it to the options dict.
    OPTIONS = dict(iconfile="path/to/my/icon/my_icon_file.icns)"

  4. I would also update the plist file for basic CFBundle* stuff. More information can be found here. I would setup the LSPrefersPPC=True here as well.

Once you're done, just go ahead and build it. Once you see proper output (Done!) you need to navigate within the bdist app, find the .app package and copy the Python.framework director to the Framework directory of the app.

Debugging

This is to debug other issues that arise during the build, once your app has been built. The Mac console isn't all that helpful when you're running into Cocoa errors. The whole point of building it with Python was not dealing with Cocoa in the first place.

Anyway, so try running the app directly from the MacOS dir within the .app

open dist/myapp.app/Contents/MacOS/myapp

This will open a new terminal and try and run your app. If you see any import issues, or other missing packages, you will at least be able to see the actual python error you are having.

Summary

It can be a little confusing even with the steps detailed above so I am pasting an example:

# setup.py file
"""
This is a setup.py script generated by py2applet

Usage:  
    python setup.py py2app
"""

from os import getcwd  
from pathlib import Path

from setuptools import setup

APP = ['MyApp.py']  
DATA_FILES = , 'UI/assets/some_static_aset.jpg']

OPTIONS = dict(  
    iconfile="UI/assets/my_icon_file.icns",
    argv_emulation=True,
    # all these imports are the ones used in your codebase
    includes=['sip',
              'PyQt5',
              'numpy',
              'sys',
              'os',
              'time',
              'xml',
              'UI',
              'threading'],
)

build_dict = {  
    "py2app": {
        "bdist_base": os.path.join(str(Path(os.getcwd()).parent), 'build'),
        "dist_dir": os.path.join(str(Path(os.getcwd()).parent), 'dist'),
    }
}

plist_dict = {  
    'plist': {
        "CFBundleGetInfoString": "MyApp.app built using py2app",
        "CFBundleIdentifier": "org.myorgname.myapp",
        'LSPrefersPPC': True,
    }}

OPTIONS.update(build_dict)  
OPTIONS.update(plist_dict)

setup(  
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

Hit, python setup.py py2app

And then finally copy:

cp -r /usr/local/Cellar/python/3.7.0/Frameworks/Python.framework ../dist/MyApp.app/Contents/Frameworks