pythonmacosbundlepy2app

How to create Mac application bundle for Python script via Python


I want to create a simple Mac application bundle which calls a simple Python script. I want to do that in Python.

Is there an easy way?

I tried to use py2app but that fails somehow, e.g.:

from setuptools import setup
setup(app=["foo.py"], setup_requires=["py2app"])

gives:

---------------------------------------------------------------------------
SystemExit                                Traceback (most recent call last)
/Users/az/<ipython console> in <module>()

/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/distutils/core.pyc in setup(**attrs)
    138         ok = dist.parse_command_line()
    139     except DistutilsArgError, msg:
--> 140         raise SystemExit, gen_usage(dist.script_name) + "\nerror: %s" % msg
    141 
    142     if DEBUG:

SystemExit: usage: ipython [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: ipython --help [cmd1 cmd2 ...]
   or: ipython --help-commands
   or: ipython cmd --help

error: no commands supplied
Type %exit or %quit to exit IPython (%Exit or %Quit do so unconditionally).

I also tried:

import py2app.build_app
py2app.build_app.py2app("foo.py")

which also doesn't work (TypeError: dist must be a Distribution instance) (I'm not really sure how to use py2app.build_app.py2app and also haven't really found much examples / documentation about it).

Maybe setuptools/py2app or so are anyway overkill for my use case. I just want to create a simple empty app bundle, copy a Python script into it and configure its Info.plist in such a way that it calls the Python script.


Solution

  • This is exactly what I wanted and works just fine:

    #!/usr/bin/python
    
    import sys
    assert len(sys.argv) > 1
    
    apppath = sys.argv[1]
    
    import os, os.path
    assert os.path.splitext(apppath)[1] == ".app"
    
    os.makedirs(apppath + "/Contents/MacOS")
    
    version = "1.0.0"
    bundleName = "Test"
    bundleIdentifier = "org.test.test"
    
    f = open(apppath + "/Contents/Info.plist", "w")
    f.write("""<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>CFBundleDevelopmentRegion</key>
        <string>English</string>
        <key>CFBundleExecutable</key>
        <string>main.py</string>
        <key>CFBundleGetInfoString</key>
        <string>%s</string>
        <key>CFBundleIconFile</key>
        <string>app.icns</string>
        <key>CFBundleIdentifier</key>
        <string>%s</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
        <string>%s</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
        <string>%s</string>
        <key>CFBundleSignature</key>
        <string>????</string>
        <key>CFBundleVersion</key>
        <string>%s</string>
        <key>NSAppleScriptEnabled</key>
        <string>YES</string>
        <key>NSMainNibFile</key>
        <string>MainMenu</string>
        <key>NSPrincipalClass</key>
        <string>NSApplication</string>
    </dict>
    </plist>
    """ % (bundleName + " " + version, bundleIdentifier, bundleName, bundleName + " " + version, version))
    f.close()
    
    f = open(apppath + "/Contents/PkgInfo", "w")
    f.write("APPL????")
    f.close()
    
    f = open(apppath + "/Contents/MacOS/main.py", "w")
    f.write("""#!/usr/bin/python
    print "Hi there"
    """)
    f.close()
    
    import stat
    oldmode = os.stat(apppath + "/Contents/MacOS/main.py").st_mode
    os.chmod(apppath + "/Contents/MacOS/main.py", oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)