cloudmatrix/esky

View on GitHub
esky/bdist_esky/f_py2app.py

Summary

Maintainability
D
2 days
Test Coverage
#  Copyright (c) 2009-2010, Cloud Matrix Pty. Ltd.
#  All rights reserved; available under the terms of the BSD License.
"""

  esky.bdist_esky.f_py2app:  bdist_esky support for py2app

"""

from __future__ import with_statement


import os
import sys
import imp
import zipfile
import shutil
import inspect
import struct
import tempfile


from py2app.build_app import py2app, get_zipfile, Target

import esky
from esky.util import create_zipfile


def freeze(dist):
    """Freeze the given distribution data using py2app."""
    includes = dist.includes
    excludes = dist.excludes
    options = dist.freezer_options
    #  Merge in any includes/excludes given in freezer_options
    includes.append("esky")
    for inc in options.pop("includes",()):
        includes.append(inc)
    for exc in options.pop("excludes",()):
        excludes.append(exc)
    if "pypy" not in includes and "pypy" not in excludes:
        excludes.append("pypy")
    options["includes"] = includes
    options["excludes"] = excludes
    # The control info (name, icon, etc) for the app will be taken from
    # the first script in the list.  Subsequent scripts will be passed
    # as the extra_scripts argument.
    exes = list(dist.get_executables())
    if not exes:
        raise RuntimeError("no scripts specified")
    cmd = _make_py2app_cmd(dist.freeze_dir,dist.distribution,options,exes)
    cmd.run()
    #  Remove any .pyc files with a corresponding .py file.
    #  This helps avoid timestamp changes that might interfere with
    #  the generation of useful patches between versions.
    appnm = dist.distribution.get_name()+".app"
    app_dir = os.path.join(dist.freeze_dir,appnm)
    resdir = os.path.join(app_dir,"Contents/Resources")
    for (dirnm,_,filenms) in os.walk(resdir):
        for nm in filenms:
            if nm.endswith(".pyc"):
                pyfile = os.path.join(dirnm,nm[:-1])
                if os.path.exists(pyfile):
                    os.unlink(pyfile+"c")
            if nm.endswith(".pyo"):
                pyfile = os.path.join(dirnm,nm[:-1])
                if os.path.exists(pyfile):
                    os.unlink(pyfile+"o")
    #  Copy data files into the freeze dir
    for (src,dst) in dist.get_data_files():
        dst = os.path.join(app_dir,"Contents","Resources",dst)
        dstdir = os.path.dirname(dst)
        if not os.path.isdir(dstdir):
            dist.mkpath(dstdir)
        dist.copy_file(src,dst)
    #  Copy package data into site-packages.zip
    zfpath = os.path.join(cmd.lib_dir,get_zipfile(dist.distribution))
    lib = zipfile.ZipFile(zfpath,"a")
    for (src,arcnm) in dist.get_package_data():
        lib.write(src,arcnm)
    lib.close()
    #  Create the bootstraping code, using custom code if specified.
    esky_name = dist.distribution.get_name()
    code_source = ["__esky_name__ = %r" % (esky_name,)]
    code_source.append(inspect.getsource(esky.bootstrap))
    if not dist.compile_bootstrap_exes:
        code_source.append(_FAKE_ESKY_BOOTSTRAP_MODULE)
        code_source.append(_EXTRA_BOOTSTRAP_CODE)
    code_source.append(dist.get_bootstrap_code())
    code_source.append("if not __rpython__:")
    code_source.append("    bootstrap()")
    code_source = "\n".join(code_source)
    def copy_to_bootstrap_env(src,dst=None):
        if dst is None:
            dst = src
        src = os.path.join(appnm,src)
        dist.copy_to_bootstrap_env(src,dst)
    if dist.compile_bootstrap_exes:
        for exe in dist.get_executables(normalise=False):
            if not exe.include_in_bootstrap_env:
                continue
            relpath = os.path.join("Contents","MacOS",exe.name)
            dist.compile_to_bootstrap_exe(exe,code_source,relpath)
    else:
        #  Copy the core dependencies into the bootstrap env.
        pydir = "python%d.%d" % sys.version_info[:2]
        for nm in ("Python.framework","lib"+pydir+".dylib",):
            try:
                copy_to_bootstrap_env("Contents/Frameworks/" + nm)
            except Exception, e:
                #  Distutils does its own crazy exception-raising which I
                #  have no interest in examining right now.  Eventually this
                #  guard will be more conservative.
                pass
        bsdir = dist.bootstrap_dir
        copy_to_bootstrap_env("Contents/Resources/include")
        if sys.version_info[:2] < (3, 3):
            copy_to_bootstrap_env("Contents/Resources/lib/"+pydir+"/config")
        else:
            copy_to_bootstrap_env("Contents/Resources/lib/"+pydir+"/config-%d.%dm"
                                   % sys.version_info[:2])
            # copy across the zip file that we need to run the boostrap application
            # from the inner package. This only needs to contain
            # a mimimal set of files for the bootstrap
            # handle the bootstrap lib dependencies
            python_name = 'python%d%d' % sys.version_info[:2]
            zip_name = os.path.join('Contents', 'Resources', 'lib', '{}.zip'.format(python_name))

            app_zfname = os.path.join(app_dir, zip_name)
            zfname = os.path.join(bsdir, zip_name)
            with tempfile.TemporaryDirectory() as tdir:
                esky.util.extract_zipfile(app_zfname, tdir)
                member_list = ['_weakrefset.pyc', 'abc.pyc', 'codecs.pyc', 'io.pyc']
                for enc in os.listdir(os.path.join(tdir, 'encodings')):
                    member_list.append(os.path.join('encodings',enc))
                esky.util.create_zipfile(tdir, zfname, members=member_list)

        if sys.version_info[:2] < (3, 3):
            required_libs = ['fcntl']
        else:
            required_libs = ['fcntl', 'zlib']

        for req_lib in required_libs:
            if req_lib not in sys.builtin_module_names:
                dynload = "Contents/Resources/lib/"+pydir+"/lib-dynload"
                for nm in os.listdir(os.path.join(app_dir,dynload)):
                    if nm.startswith(req_lib):
                        copy_to_bootstrap_env(os.path.join(dynload,nm))

        copy_to_bootstrap_env("Contents/Resources/__error__.sh")
        # Copy site.py/site.pyc into the boostrap env, then zero them out.
        if os.path.exists(os.path.join(app_dir, "Contents/Resources/site.py")):
            copy_to_bootstrap_env("Contents/Resources/site.py")
            with open(bsdir + "/Contents/Resources/site.py", "wt") as f:
                pass
        if os.path.exists(os.path.join(app_dir, "Contents/Resources/site.pyc")):
            copy_to_bootstrap_env("Contents/Resources/site.pyc")
            with open(bsdir + "/Contents/Resources/site.pyc", "wb") as f:
                f.write(esky.util.compile_to_bytecode("", "site.py"))
        if os.path.exists(os.path.join(app_dir, "Contents/Resources/site.pyo")):
            copy_to_bootstrap_env("Contents/Resources/site.pyo")
            with open(bsdir + "/Contents/Resources/site.pyo", "wb") as f:
                f.write(imp.get_magic() + struct.pack("<i", 0))
        #  Copy the bootstrapping code into the __boot__.py file.
        copy_to_bootstrap_env("Contents/Resources/__boot__.py")
        with open(bsdir+"/Contents/Resources/__boot__.py","wt") as f:
            f.write(code_source)
        #  Copy the loader program for each script into the bootstrap env.
        copy_to_bootstrap_env("Contents/MacOS/python")
        for exe in dist.get_executables(normalise=False):
            if not exe.include_in_bootstrap_env:
                continue
            exepath = copy_to_bootstrap_env("Contents/MacOS/"+exe.name)
    #  Copy non-python resources (e.g. icons etc) into the bootstrap dir
    copy_to_bootstrap_env("Contents/Info.plist")
    # Include Icon
    if exe.icon is not None:
        copy_to_bootstrap_env("Contents/Resources/"+exe.icon)
    copy_to_bootstrap_env("Contents/PkgInfo")
    with open(os.path.join(app_dir,"Contents","Info.plist"),"rt") as f:
        infotxt = f.read()
    for nm in os.listdir(os.path.join(app_dir,"Contents","Resources")):
        if "<string>%s</string>" % (nm,) in infotxt:
            copy_to_bootstrap_env("Contents/Resources/"+nm)



def zipit(dist,bsdir,zfname):
    """Create the final zipfile of the esky.

    We customize this process for py2app, so that the zipfile contains a
    toplevel "<appname>.app" directory.  This allows users to just extract
    the zipfile and have a proper application all set up and working.
    """
    def get_arcname(fpath):
        return os.path.join(dist.distribution.get_name()+".app",fpath)
    return create_zipfile(bsdir,zfname,get_arcname,compress=True)


def _make_py2app_cmd(dist_dir,distribution,options,exes):
    exe = exes[0]
    extra_exes = exes[1:]
    cmd = py2app(distribution)
    for (nm,val) in options.iteritems():
        setattr(cmd,nm,val)
    cmd.dist_dir = dist_dir
    cmd.app = [Target(script=exe.script,dest_base=exe.name)]
    cmd.extra_scripts = [e.script for e in extra_exes]
    cmd.finalize_options()
    cmd.plist["CFBundleExecutable"] = exe.name
    old_run = cmd.run
    def new_run():
        #  py2app munges the environment in ways that break things.
        old_deployment_target = os.environ.get("MACOSX_DEPLOYMENT_TARGET",None)
        old_run()
        if old_deployment_target is None:
            os.environ.pop("MACOSX_DEPLOYMENT_TARGET",None)
        else:
            os.environ["MACOSX_DEPLOYMENT_TARGET"] = old_deployment_target
        #  We need to script file to have the same name as the exe, which
        #  it won't if they have changed it explicitly.
        resdir = os.path.join(dist_dir,distribution.get_name()+".app","Contents/Resources")
        scriptf = os.path.join(resdir,exe.name+".py")
        if not os.path.exists(scriptf):
           old_scriptf = os.path.basename(exe.script)
           old_scriptf = os.path.join(resdir,old_scriptf)
           shutil.move(old_scriptf,scriptf)
    cmd.run = new_run
    return cmd


#  Code to fake out any bootstrappers that try to import from esky.
_FAKE_ESKY_BOOTSTRAP_MODULE = """
class __fake:
  __all__ = ()
sys.modules["esky"] = __fake()
sys.modules["esky.bootstrap"] = __fake()
"""

#  py2app goes out of its way to set sys.executable to a normal python
#  interpreter, which will break the standard bootstrapping code.
#  Get the original value back.
_EXTRA_BOOTSTRAP_CODE = """
from posix import environ
if sys.version_info[:2] < (3, 3):
    sys.executable = environ["EXECUTABLEPATH"]
    sys.argv[0] = environ["ARGVZERO"]
else:
    sys.executable = environ[b"EXECUTABLEPATH"].decode(sys.getfilesystemencoding())
    sys.argv[0] = environ[b"ARGVZERO"].decode(sys.getfilesystemencoding())
"""