#!/usr/bin/env python

from distutils.core import setup, Extension
import distutils.sysconfig
from distutils.cmd import Command
from distutils import log
import sys
import os.path
from jToolkit import __version__
# py2exe only available on Windows
try:
  import py2exe
  build_exe = py2exe.build_exe.py2exe
  Distribution = py2exe.Distribution
except ImportError:
  py2exe = None
  build_exe = Command

join = os.path.join

jtoolkitversion = __version__.ver

packagesdir = distutils.sysconfig.get_python_lib()
sitepackages = packagesdir.replace(sys.prefix + os.sep, '')

class fileset(list):
  """this is a installation list of a set of files from a directory"""
  def __init__(self, src, dest, destsubdir, exclude=["CVS"]):
    """creates the fileset by walking through src directory"""
    self.src = src
    self.dest = dest
    self.destsubdir = destsubdir
    self.exclude = exclude
    # this calls self.adddirfiles(None, dirname, names) for each subdirectory dirname of self.src
    os.path.walk(self.src, self.adddirfiles, None)

  def adddirfiles(self, arg, dirname, names):
    """adds the files names from dirname to self (which is a list)"""
    # arg is ignored
    filenames = []
    for name in names:
      if name in self.exclude:
        continue
      filename = join(dirname,name)
      if not os.path.isdir(filename):
        filenames.append(filename)
    if len(filenames) > 0:
      destsubdirname = dirname.replace(self.src,self.destsubdir,1)
      destpath = join(self.dest,destsubdirname)
      self.append((destpath,filenames))

class InnoScript:
    """class that builds an InnoSetup script"""
    def __init__(self, name, lib_dir, dist_dir, exe_files = [], other_files = [], install_scripts = [], version = "1.0"):
        self.lib_dir = lib_dir
        self.dist_dir = dist_dir
        if not self.dist_dir.endswith(os.sep):
            self.dist_dir += os.sep
        self.name = name
        self.version = version
        self.exe_files = [self.chop(p) for p in exe_files]
        self.other_files = [self.chop(p) for p in other_files]
        self.install_scripts = install_scripts

    def getcompilecommand(self):
        try:
            import _winreg
            compile_key = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT, "innosetupscriptfile\\shell\\compile\\command")
            compilecommand = _winreg.QueryValue(compile_key, "")
            compile_key.Close()
        except:
            compilecommand = "compil32.exe"
        return compilecommand

    def chop(self, pathname):
        """returns the path relative to self.dist_dir"""
        assert pathname.startswith(self.dist_dir)
        return pathname[len(self.dist_dir):]

    def create(self, pathname=None):
        """creates the InnoSetup script"""
        if pathname is None:
          self.pathname = os.path.join(self.dist_dir, self.name + os.extsep + "iss")
        else:
          self.pathname = pathname
        ofi = self.file = open(self.pathname, "w")
        print >> ofi, "; WARNING: This script has been created by py2exe. Changes to this script"
        print >> ofi, "; will be overwritten the next time py2exe is run!"
        print >> ofi, r"[Setup]"
        print >> ofi, r"AppName=%s" % self.name
        print >> ofi, r"AppVerName=%s %s" % (self.name, self.version)
        print >> ofi, r"DefaultDirName={pf}\%s" % self.name
        print >> ofi, r"DefaultGroupName=%s" % self.name
        print >> ofi, r"OutputBaseFilename=%s-%s-setup" % (self.name, self.version)
        print >> ofi
        print >> ofi, r"[Files]"
        for path in self.exe_files + self.other_files:
            print >> ofi, r'Source: "%s"; DestDir: "{app}\%s"; Flags: ignoreversion' % (path, os.path.dirname(path))
        print >> ofi
        print >> ofi, r"[Icons]"
        for path in self.exe_files:
            if path in self.install_scripts:
                continue
            linkname = os.path.splitext(os.path.basename(path))[0]
            print >> ofi, r'Name: "{group}\%s"; Filename: "{app}\%s"; WorkingDir: "{app}"; Flags: dontcloseonexit' % \
                  (linkname, path)
        print >> ofi, 'Name: "{group}\Uninstall %s"; Filename: "{uninstallexe}"' % self.name
        if self.install_scripts:
            print >> ofi, r"[Run]"
            for path in self.install_scripts:
                print >> ofi, r'Filename: "{app}\%s"; WorkingDir: "{app}"; Parameters: "-install"' % path
            print >> ofi
            print >> ofi, r"[UninstallRun]"
            for path in self.install_scripts:
                print >> ofi, r'Filename: "{app}\%s"; WorkingDir: "{app}"; Parameters: "-remove"' % path
        print >> ofi
        ofi.close()

    def compile(self):
        """compiles the script using InnoSetup"""
        shellcompilecommand = self.getcompilecommand()
        compilecommand = shellcompilecommand.replace('"%1"', self.pathname)
        result = os.system(compilecommand)
        if result:
            print "Error compiling iss file"
            print "Opening iss file, use InnoSetup GUI to compile manually"
            os.startfile(self.pathname)

def fix_bdist_rpm(setupfile):
    """Fixes bdist_rpm to use the given setup filename instead of setup.py"""
    try:
        from distutils.command import bdist_rpm
        build_rpm = bdist_rpm.bdist_rpm
    except ImportError:
        return
    if not hasattr(build_rpm, "_make_spec_file"):
        return
    orig_make_spec_file = build_rpm._make_spec_file
    def fixed_make_spec_file(self):
        """Generate the text of an RPM spec file and return it as a
        list of strings (one per line).
        """
        orig_spec_file = orig_make_spec_file(self)
        return [line.replace("setup.py", setupfile) for line in orig_spec_file]
    build_rpm._make_spec_file = fixed_make_spec_file

def exclude_python_file(excludefile):
    """Adjusts build_py to not include the given .py file even if it is in a package"""
    # TODO: complete this, some other commands still need to be adjusted
    from distutils.command import build_py
    if hasattr(build_py.build_py, "find_package_modules"):
        orig_find_package_modules = build_py.build_py.find_package_modules
        def find_package_modules_with_exclude(*args, **kwargs):
            """Finds package modules, but excludes excludefile"""
            orig_package_modules = orig_find_package_modules(*args, **kwargs)
            return [(package, module_base, filename) for (package, module_base, filename) in orig_package_modules
                    if not filename.endswith(excludefile)]
        build_py.build_py.find_package_modules = find_package_modules_with_exclude

############# remove_source alterations to distutils ############

def extend_function(orig_function, extended_function):
    def wrapped_function(*args, **kwargs):
         result = orig_function(*args, **kwargs)
         result = extended_function(result, *args, **kwargs)
         return result
    return wrapped_function

def is_removable (py_file):
    """Checks whether a given python file should be removed.
    Can be overridden to only remove selected files; by default always returns True"""
    return True

def map_data_file (data_file):
    """remaps a data_file (could be a directory) to a different location
    Can be overridden to rearrange data locations; by default always returns data_file"""
    return data_file

def remove_source (py_files, verbose=1, dry_run=0):
    """Remove the original source for a collection of Python source files
    (assuming they have been compiled to either .pyc or .pyo form).
    'py_files' is a list of files to remove; any files that don't end in 
    ".py" are silently skipped.

    If 'verbose' is true, prints out a report of each file.  If 'dry_run'
    is true, doesn't actually do anything that would affect the filesystem.

    """
    for file in py_files:
        if file[-3:] != ".py":
            # This lets us be lazy and not filter filenames in
            # the "install_lib" command.
            continue
        if not os.path.exists(file+"c") or os.path.exists(file+"o"):
            log.warn("compiled file does not exist for %s" % (file))
        if os.path.exists(file) and is_removable(file):
            log.info("removing source file %s" % (file))
            if not dry_run:
                os.remove(file)

def bdist_get_inidata_removesource(result, self):
    return result + "\nremove_source=%d" % (self.remove_source)

# in run, set install_lib.remove_source (and compile) appropriately
def reinitialize_command_removesource(result, self, command, reinit_subcommands=0):
    if command == "install_lib":
        # pass the remove_source argument on to install_lib
        result.remove_source = self.remove_source
    return result

def make_finalize_options_removesource(command_source):
    """makes an extender method for getting the removesource option from the given command name"""
    def finalize_options_removesource(result, self):
        self.set_undefined_options(command_source, ('remove_source', 'remove_source'))
        return result
    return finalize_options_removesource

def byte_compile_removesource(self, files):
    if self.remove_source and not (self.compile or self.optimize > 0):
      self.compile = 1
    self.byte_compile_orig(files)
    if self.remove_source:
        remove_source(files, verbose=self.verbose, dry_run=self.dry_run)

def get_outputs_removesource(result, self):
    if self.remove_source:
        filtered_result = []
        for filename in result:
            if filename.endswith(".pycc"):
                continue
            elif filename.endswith(".py"):
                if not is_removable(filename):
                    filtered_result.append(filename)
            else:
                filtered_result.append(filename)
        return filtered_result
    else:
        return result

def initialize_remove_source(result, self):
    self.remove_source = None
    return result

def allow_distutils_remove_source():
    """adds the remove_source capabilities to distutils"""
    from distutils import util
    util.remove_source = remove_source
    option = ('remove-source', None, "don't include original .py source files (remove from distribution)")
    option_passing = {"build_py": "build", "install_lib": "install"}
    def add_remove_source_option(commandclass):
      commandclass.user_options.append(option)
      commandclass.boolean_options.append('remove-source')
      commandclass.initialize_options = extend_function(commandclass.initialize_options, initialize_remove_source)
      if hasattr(commandclass, "byte_compile"):
        commandclass.byte_compile_orig = commandclass.byte_compile
        commandclass.byte_compile = byte_compile_removesource
      if commandclass.__name__ in option_passing:
        finalize_options_removesource = make_finalize_options_removesource(option_passing[commandclass.__name__])
        commandclass.finalize_options = extend_function(commandclass.finalize_options, finalize_options_removesource)
    # bdist_wininst changes
    from distutils.command import bdist_wininst
    wininst = bdist_wininst.bdist_wininst
    add_remove_source_option(wininst)
    wininst.reinitialize_command = extend_function(wininst.reinitialize_command, reinitialize_command_removesource)
    wininst.get_inidata = extend_function(wininst.get_inidata, bdist_get_inidata_removesource)
    # bdist_rpm changes
    from distutils.command import bdist_rpm
    add_remove_source_option(bdist_rpm.bdist_rpm)
    # build changes
    from distutils.command import build
    add_remove_source_option(build.build)
    from distutils.command import build_py
    add_remove_source_option(build_py.build_py)
    # install changes
    from distutils.command import install
    add_remove_source_option(install.install)
    from distutils.command import install_lib
    libinst = install_lib.install_lib
    add_remove_source_option(libinst)
    libinst.get_outputs = extend_function(libinst.get_outputs, get_outputs_removesource)

class build_installer(build_exe):
    """distutils class that first builds the exe file(s), then creates a Windows installer using InnoSetup"""
    description = "create an executable installer for MS Windows using InnoSetup and py2exe"
    user_options = getattr(build_exe, 'user_options', []) + \
        [('install-script=', None,
          "basename of installation script to be run after installation or before deinstallation")]

    def initialize_options(self):
        build_exe.initialize_options(self)
        self.install_script = None

    def reinitialize_command(self, command, reinit_subcommands=0):
        if command == "install_data":
            install_data = build_exe.reinitialize_command(self, command, reinit_subcommands)
            install_data.data_files = self.remap_data_files(install_data.data_files)
            return install_data
        return build_exe.reinitialize_command(self, command, reinit_subcommands)

    def remap_data_files(self, data_files):
        new_data_files = []
        for f in data_files:
            if type(f) in (str, unicode):
                f = map_data_file(f)
            else:
                dir, files = f
                dir = map_data_file(dir)
                if dir is None:
                  f = None
                else:
                  f = dir, files
            if f is not None:
              new_data_files.append(f)
        return new_data_files

    def run(self):
        # First, let py2exe do it's work.
        build_exe.run(self)
        lib_dir = self.lib_dir
        dist_dir = self.dist_dir
        # create the Installer, using the files py2exe has created.
        exe_files = self.windows_exe_files + self.console_exe_files
        install_scripts = self.install_script
        if isinstance(install_scripts, (str, unicode)):
            install_scripts = [install_scripts]
        script = InnoScript(self.distribution.metadata.name, lib_dir, dist_dir, exe_files, self.lib_files, version=self.distribution.metadata.version, install_scripts=install_scripts)
        print "*** creating the inno setup script***"
        script.create()
        print "*** compiling the inno setup script***"
        script.compile()
        # Note: By default the final setup.exe will be in an Output subdirectory.

# the localize directory contains localized files...
localizesrc = 'localize' # join('jLogbook','localize')
localizedest = sitepackages
localizedestsubdir = join('localize')
localizefileset = fileset(localizesrc, localizedest, localizedestsubdir)
sitejtoolkit = join(sitepackages, 'jToolkit')

def get_data_files(*subdir_parts):
    return fileset(join(sitepackages, *subdir_parts), sitepackages, join(*subdir_parts))

datafiles = get_data_files('jToolkit', 'icons') + \
            get_data_files('jToolkit', 'js')

initfiles = [(join(sitepackages,'jToolkit'),[join('jToolkit','__init__.py')]),
             (join(sitepackages,'jToolkit', 'data'),[join('jToolkit', 'data', '__init__.py')]),
             (join(sitepackages,'jToolkit', 'web'),[join('jToolkit', 'web', '__init__.py')]),
             (join(sitepackages,'jToolkit', 'widgets'),[join('jToolkit', 'widgets', '__init__.py')]),
             (join(sitepackages,'jToolkit', 'xml'),[join('jToolkit', 'xml', '__init__.py')])]

setuppackages = ['jToolkit', 'jToolkit.data', 'jToolkit.web', 'jToolkit.widgets', 'jToolkit.xml']

standarddatafiles = localizefileset + datafiles + initfiles

setupdatafiles = standarddatafiles

def buildmanifest_in(file):
  """This writes the required files to a MANIFEST.in file"""
  print >>file, "# MANIFEST.in: the below autogenerated by setup.py from jToolkit %s" % jtoolkitversion
  print >>file, "# things needed by translate setup.py to rebuild"
  print >>file, "graft jToolkit/icons"
  print >>file, "graft jToolkit/demo/tutorial"
  print >>file, "graft jToolkit/js"
  print >>file, "include ChangeLog"
  print >>file, "include COPYING"
  print >>file, "include LICENSE"
  print >>file, "graft jToolkit/js"
  print >>file, "# MANIFEST.in: the above autogenerated by setup.py from translate %s" % jtoolkitversion

def standardsetup(name, version, custompackages=[], customdatafiles=[]):
  try:
    manifest_in = open("MANIFEST.in", "w")
    buildmanifest_in(manifest_in)
    manifest_in.close()
  except IOError, e:
    print >> sys.stderr, "warning: could not recreate MANIFEST.in, continuing anyway. Error was %s" % e
  fix_bdist_rpm(os.path.basename(__file__))
  exclude_python_file(join("jToolkit", "data", "ADODB.py"))
  dosetup(name, version, setuppackages + custompackages, setupdatafiles + customdatafiles)

classifiers = [
  "Development Status :: 4 - Beta",
  "Environment :: Web Environment",
  "Intended Audience :: Developers",
  "License :: OSI Approved :: GNU General Public License (GPL)",
  "Programming Language :: Python",
  "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
  "Topic :: Software Development :: Libraries :: Python Modules",
  "Operating System :: OS Independent",
  "Operating System :: Microsoft :: Windows",
  "Operating System :: Unix"
  ]

jToolkitBlurb = """jToolkit is a Python web application framework built on modpython and Apache. There is also a simple command line webserver for running applications from.
It is aimed at dynamically generated pages rather than mostly-static pages (for which there are templating solutions). Pages can be produced using a variety of widgets. It handles sessions and database connections (and multi-database portability)."""

def dosetup(name, version, packages, datafiles):
  setup(name=name,
        version=version,
        license="GNU General Public License (GPL)",
        description="jToolkit web framework",
        long_description=jToolkitBlurb,
        author="St James Software",
        author_email="info@sjsoft.com",
        url="http://jtoolkit.sourceforge.net/",
        platforms=["any"],
        classifiers=classifiers,
        packages=packages,
        data_files=datafiles
        )

if __name__ == "__main__":
  standardsetup("jToolkit", jtoolkitversion)

