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

  esky.bdist_esky:  distutils command to freeze apps in esky format

Importing this module makes "bdist_esky" available as a distutils command.
This command will freeze the given scripts and package them into a zipfile
named with the application name, version and platform.

The main interface is the 'Esky' class, which represents a frozen app.  An Esky
must be given the path to the top-level directory of the frozen app, and a
'VersionFinder' object that it will use to search for updates.
"""

from __future__ import with_statement


import os
import re
import sys
import shutil
import zipfile
import tempfile
import hashlib
import inspect
from glob import glob

import distutils.command
from distutils.core import Command
from distutils.util import convert_path

import esky.patch
from esky.util import get_platform, is_core_dependency, create_zipfile, \
                      split_app_version, join_app_version, ESKY_CONTROL_DIR, \
                      ESKY_APPDATA_DIR, really_rmtree

if sys.platform == "win32":
    from esky import winres
    from xml.dom import minidom

try:
    from esky.bdist_esky import pypyc
except ImportError, e:
    pypyc = None
    PYPYC_ERROR = e
    COMPILED_BOOTSTRAP_CACHE = None
else:
    COMPILED_BOOTSTRAP_CACHE = os.path.dirname(__file__)
    if not os.path.isdir(COMPILED_BOOTSTRAP_CACHE):
        COMPILED_BOOTSTRAP_CACHE = None



#  setuptools likes to be imported before anything else that
#  might monkey-patch distutils.  We don't actually use it,
#  this is just to avoid errors with cx_Freeze.
try:
    import setuptools
except ImportError:
    pass


_FREEZERS = {}
try:
    from esky.bdist_esky import f_py2exe
    _FREEZERS["py2exe"] = f_py2exe
except ImportError:
    _FREEZERS["py2exe"] = None
try:
    from esky.bdist_esky import f_py2app
    _FREEZERS["py2app"] = f_py2app
except ImportError:
    _FREEZERS["py2app"] = None
try:
    from esky.bdist_esky import f_bbfreeze
    _FREEZERS["bbfreeze"] = f_bbfreeze
except ImportError:
    _FREEZERS["bbfreeze"] = None
try:
    from esky.bdist_esky import f_cxfreeze
    _FREEZERS["cxfreeze"] = f_cxfreeze
    _FREEZERS["cx_Freeze"] = f_cxfreeze
    _FREEZERS["cx_freeze"] = f_cxfreeze
except ImportError:
    _FREEZERS["cxfreeze"] = None
    _FREEZERS["cx_Freeze"] = None
    _FREEZERS["cx_freeze"] = None


class Executable(unicode):
    """Class to hold information about a specific executable.

    This class provides a uniform way to specify extra meta-data about
    a frozen executable.  By setting various keyword arguments, you can
    specify e.g. the icon, and whether it is a gui-only script.

    Some freezer modules require all items in the "scripts" argument to
    be strings naming real files.  This is therefore a subclass of unicode,
    and if it refers only to in-memory code then its string value will be
    the path to this very file.  I know it's ugly, but it works.
    """

    def __new__(cls,script,**kwds):
        if isinstance(script,basestring):
            return unicode.__new__(cls,script)
        else:
            return unicode.__new__(cls,__file__)

    def __init__(self,script,name=None,icon=None,gui_only=None,
                      include_in_bootstrap_env=True,**kwds):
        unicode.__init__(self)
        if isinstance(script,Executable):
            script = script.script
            if name is None:
                name = script.name
            if gui_only is None:
                gui_only = script.gui_only
        if not isinstance(script,basestring):
            if name is None:
                raise TypeError("Must specify name if script is not a file")
        self.script = script
        self.include_in_bootstrap_env = include_in_bootstrap_env
        self.icon = i
con
        self._name = name
        self._gui_only = gui_only
        self
._kwds = kwds

    @property
    def name(self):
        if self._name is not None:
            nm = self._name
        else:
            if not isinstance(self.script,basestring):
                raise TypeError("Must specify name if script is not a file")
            nm = os.path.basename(self.script)
            if nm.endswith(".py"):
                nm = nm[:-3]
            elif nm.endswith(".pyw"):
                nm = nm[:-4]
        if sys.platform == "win32" and not nm.endswith(".exe"):
            nm += ".exe"
        return nm

    @property
    def gui_only(self):
        if self._gui_only is None:
            if not isinstance(self.script,basestring):
                return False
            else:
                return self.script.endswith(".pyw")
        else:
            return self._gui_only



class bdist_esky(Command):
    """Create a frozen application in 'esky' format.

    This distutils command can be used to freeze an application in the
    format expected by esky.  It interprets the following standard 
    distutils options:

       scripts:  list of scripts to freeze as executables;
                 to make a gui-only script, name it 'script.pyw'

       data_files:  copied into the frozen app directory

       package_data:  copied into library.zip alongside the module code

    To further customize the behaviour of the bdist_esky command, you can
    specify the following custom options:

        includes:  a list of modules to explicitly include in the freeze

        excludes:  a list of modules to explicitly exclude from the freeze

        freezer_module:  name of freezer module to use; currently py2exe,
                         py2app,  bbfreeze and cx-freeze are supported.

        freezer_options: dict of options to pass through to the underlying
                         freezer module.

        bootstrap_module:  a custom module to use for esky bootstrapping;
                           the default calls esky.bootstrap.bootstrap()

        bootstrap_code:  a custom code string to use for esky bootstrapping;
                         this precludes the use of the bootstrap_module option.
                         If a non-string object is given, its source is taken
                         using inspect.getsource().

        compile_bootstrap_exes:  whether to compile the bootstrapping code to a
                                 stand-alone exe; this requires PyPy installed
                                 and the bootstrap code to be valid RPython.
                                 When false, the bootstrap env will use a
                                 trimmed-down copy of the freezer module exe.

        dont_run_startup_hooks:  don't force all executables to call
                                 esky.run_startup_hooks() on startup.

        bundle_msvcrt:  whether to bundle the MSVCRT DLLs, manifest files etc
                        as a private assembly.  The default is False; only
                        those with a valid license to redistriute these files
                        should enable it.



        pre_freeze_callback:  function to call just before starting to freeze
                              the application; this is a good opportunity to
                              customize the bdist_esky instance.

        pre_zip_callback:  function to call just before starting to zip up
                           the frozen application; this is a good opportunity
                           to e.g. sign the resulting executables.
    
    """

    description = "create a frozen app in 'esky' format"

    user_options = [
                    ('dist-dir=', 'd',
                     "directory to put final built distributions in"),
                    ('freezer-module=', None,
                     "module to use for freezing the application"),
                    ('freezer-options=', None,
                     "options to pass to the underlying freezer module"),
                    ('bootstrap-module=', None,
                     "module to use for bootstrapping the application"),
                    ('bootstrap-code=', None,
                     "code to use for bootstrapping the application"),
                    ('compile