#!/usr/bin/python

"""
Template for building Unix/Linux installers for CCTBX-based packages, e.g.
Phenix or LABELIT.  This is derived from the Phenix installer, with options
specific to Phenix removed.

The installer makes several assumptions about the distribution bundle, and
will break if it does not encounter the expected files and directories.  The
recommended way to create an installer package is to start with the script
setup_installer.py in the same directory.  The expected directory structure is:

INSTALLER_DIR/bin/
INSTALLER_DIR/bin/install   # main executable, subclasses 'installer' class
INSTALLER_DIR/lib
INSTALLER_DIR/lib/libtbx/   # complete libtbx distribution
INSTALLER_DIR/bundles/      # compressed binary distribution
INSTALLER_DIR/source/       # source tarballs
INSTALLER_DIR/dependencies  # third-party software

The last four directories contain the actual distribution; depending on whether
the installer is source or binary, they may not all be present.  The 'bundles'
directory will have one or more tar.gz files containing all compiled files
(and many Python modules).  The 'sources' directory will contain a set of
tar.gz files for the distributed modules, one of which will be a CCTBX source
bundle (plus various add-ons like ccp4io, cbflib, etc.).  'dependencies' is for
third-party packages such as Python that need to be compiled for source
installation.

In addition to the required contents, the installer may also contain files
named README, LICENSE, etc.; these will automatically be propagated to the
installed package.  Information describing the version may also be provided
but the implementation of this is left up to each product.  Installers may also
have additional directories for non-compiled packages such as geometry
restraints or documentation.

To implement an installer for distribution, an install script should be written
along these lines:

  from libtbx.auto_build import install_distribution
  import os.path
  import sys

  class installer(install_distribution.installer):
    product_name = "PHENIX"
    dest_dir_prefix = "phenix"
    make_apps = ["phenix"]
    installer_dir = os.path.dirname(os.path.dirname(__file__))

  if (__name__ == "__main__"):
    installer(sys.argv[1:])

Note that the location of the install script is not mandatory, but it must be
able to correctly detect the base installer directory.
"""

from __future__ import absolute_import, division, print_function
from optparse import OptionParser
import os.path as op
import os
import sys
# set nproc automatically if possible
try :
  import multiprocessing
  NPROC_DEFAULT = min(24, multiprocessing.cpu_count())
except Exception :
  NPROC_DEFAULT = 1
# local imports
# XXX HACK
libtbx_path = op.abspath(op.dirname(op.dirname(__file__)))
if (not libtbx_path in sys.path):
  sys.path.append(libtbx_path)
from libtbx.auto_build import write_gui_dispatcher_include
from libtbx.auto_build import regenerate_module_files
from libtbx.auto_build import install_base_packages
from libtbx.auto_build import create_mac_app
from libtbx.auto_build.installer_utils import *

class InstallerError(Exception):
  __orig_module__ = __module__
  # trick to get just "Sorry" instead of "libtbx.utils.Sorry"
  __module__ = Exception.__module__

  def reset_module(self):
    """
    Reset the class module on an instance to libtbx.utils.
    """
    self.__class__.__module__ = self.__class__.__orig_module__

class installer(object):
  """
  Base class for installers. Some methods and class attributes must be
  re-implemented in subclasses!
  """
  # Basic configuration variables - override in subclasses
  organization = "gov.lbl.cci"
  product_name = "CCTBX"
  destination = "/usr/local"
  dest_dir_prefix = "cctbx"
  installer_dir = os.environ.get(product_name + "_INSTALLER", None)
  include_gui_packages = True
  # Options passed to install_base_packages.py
  base_package_options = []
  base_modules = []
  modules = [
    "cctbx_project",
  ]
  # Modules that need to be configured for use in the final installation.
  #   this will automatically include dependencies.
  configure_modules = ["mmtbx", "smtbx"]
  # Programs to make graphical .app launchers for (Mac only)
  make_apps = []
  # Architectures supported for a particular distribution. Those listed here
  #   will be detected automatically.
  supported_mtypes = [
    "intel-linux-2.6",
    "intel-linux-2.6-x86_64",
    "mac-intel-osx",
    "mac-intel-osx-x86_64",
    "intel-windows",
    "intel-windows-x86_64",
  ]
  # Boolean flags to control installer behaviour in a fine-grained manner
  flags = [
    'create_versioned_dispatchers'
  ]

  def __init__(self, args=None, out=sys.stdout):
    self.args = args or []
    self.out = out
    self.parse_options()

  def parse_options(self):
    """
    Process command-line options.  These may be supplemented by subclasses that
    override the method add_product_specific_options.
    """
    parser = OptionParser(
      description=("Command-line installer for %s crystallography "+
        "software on Mac and Linux systems.  Please note that Mac users "+
        "may also use the graphical installer, but this is limited to single "+
        "workstations.") % self.product_name)
    parser.add_option("--prefix", dest="prefix", action="store",
      help="Directory where %s is to be installed" % self.product_name,
      default=self.destination)
    parser.add_option("--nproc", dest="nproc", action="store", type="int",
      help="Number of processors for source install", default=NPROC_DEFAULT)
    parser.add_option("--no-app", dest="no_app", action="store_true",
      help="Disable generation of Mac app launcher(s)", default=False)
    parser.add_option("--compact", dest="compact", action="store_true",
      help="Remove unnecessary files such as compiled sources to save space")
    parser.add_option("--try-unsupported", dest="try_unsupported",
      action="store_true", default=False,
      help="Attempt source install on unsupported platform")
    parser.add_option("--verbose", "-v", dest="verbose", action="store_true",
      help="Provide more detailed output during installation", default=False)
    # Source only options
    parser.add_option("--no-gui", dest="no_gui", action="store_true",
      help="Disable building of GUI dependencies (source install only)",
      default=False)
    parser.add_option("--base-only", dest="base_only", action="store_true",
      help="Only install base libraries and files", default=False)
    parser.add_option("--source", dest="source", action="store_true",
      help="Force source installation", default=None)
    parser.add_option("--openmp", dest="openmp", action="store_true",
      help="Enable OpenMP compilation if possible", default=False)
    parser.add_option("--no-opt", dest="no_opt", action="store_true",
      help="Disable optimization during compilation", default=False)
    parser.add_option("--debug", dest="debug", action="store_true",
      help="Turn on debugging during compilation", default=False)
    parser.add_option("--python_static", default=False, action="store_true",
      help="Compile Python as static executable and library (Linux only)")
    parser.add_option("--use-conda", default=False, action="store_true",
      help="Install conda dependencies (source install only)")
    # Deprecated
    parser.add_option("--makedirs", default=False, action="store_true",
      help="Create installation path prefix if not already present")
    parser.add_option("--binary", dest="binary", action="store_true",
      help="Use pre-compiled binary install", default=None)
    parser.add_option("--nopycompile", dest="nopycompile", action="store_true",
      help="Do not precompile python files", default=False)
    self.add_product_specific_options(parser)
    self.options, args = parser.parse_args(self.args)

  def install(self):
    check_python_version()
    self.parse_options()
    self.basic_setup()
    self.product_specific_preinstallation_hook()
    self.check_directories()
    if sys.platform != "win32":
      self.print_banner()
      if not os.path.exists('build'):
        print("No build directory exists; trying source installation.", file=self.out)
        self.options.source = True
      if self.options.source:
        print("Source installation specified.", file=self.out)
        self.install_from_source()
      else:
        self.install_from_binary()
    self.install_finalize()
    self.print_header('Installation complete!')

  def basic_setup(self):
    # Check version
    self.version = self.get_version()
    assert (self.version is not None)
    # GUI Flag
    self.flag_build_gui = False
    if (self.include_gui_packages):
      self.flag_build_gui = not self.options.no_gui

    # Check this is a supported architecture.
    self.mtype = self.machine_type()
    if (self.mtype is None):
      raise InstallerError("Machine type not recognized")
    elif ((not self.mtype in self.supported_mtypes) and
          (not self.options.try_unsupported)):
      raise InstallerError("""
  %(mtype)s is not a supported platform, installation aborted
    use the --try-unsupported option to attempt installation on this platform
    use the --no-gui option for a core %(product)s installation
  """%{ "mtype" : self.mtype, "product" : self.product_name})

  def check_directories(self):
    if sys.platform == "win32":
      self.dest_dir = os.getcwd()
      self.tmp_dir = self.dest_dir
      self.build_dir = op.join(self.dest_dir, "build")
      self.base_dir = op.join(self.dest_dir, "base")
      self.modules_dir = op.join(self.dest_dir, "modules")
      # check for conda
      if os.path.isdir(op.join(self.installer_dir, "conda_base")) or \
         os.path.exists(op.join(self.installer_dir, "conda_base.tar")):
        self.base_dir = op.join(self.dest_dir, "conda_base")
      return
    # The default behavior for nearly all program's --prefix options
    # is to create the directory, so I don't think the --makedirs option
    # is necessary.

    if not os.access(self.options.prefix, os.W_OK):
      if not os.path.exists(self.options.prefix):
        os.makedirs(self.options.prefix)
      else:
        raise InstallerError("""
  Installation directory not writeable:
    %s
  Please specify an alternative directory using the --prefix option."""
      %self.options.prefix)

    # Do not overwrite an existing installation.
    self.dest_dir = op.abspath(op.join(
      self.options.prefix, "%s-%s"%(self.dest_dir_prefix, self.version)))
    if os.path.exists(self.dest_dir):
      raise InstallerError("""
  Installation directory already exists:
    %s
  Please remove this directory and try again.
    """%self.dest_dir)

    # Other useful directories.
    self.tmp_dir = op.join(self.installer_dir, "base_tmp")
    self.build_dir = op.join(self.dest_dir, "build")
    self.base_dir = op.join(self.dest_dir, "base")
    self.modules_dir = op.join(self.dest_dir, "modules")
    for i in [self.dest_dir, self.tmp_dir]:
      if not os.path.exists(i):
        os.makedirs(i)

    # check for conda
    if os.path.isdir(op.join(self.installer_dir, "conda_base")) or \
       os.path.exists(op.join(self.installer_dir, "conda_base.tar")):
      self.base_dir = op.join(self.dest_dir, "conda_base")

    # Environment variables required by other scripts
    os.environ["%s_MTYPE" % self.product_name] = self.mtype
    os.environ["%s_INSTALLER" % self.product_name] = self.installer_dir
    os.environ["%s_LOC" % self.product_name] = self.dest_dir
    os.environ["%s_BUILD" % self.product_name] = self.build_dir

  def print_banner(self):
    print("""
    ==========================================================================
                          %(product)s Installation

                          version: %(version)s
                     machine type: %(mtype)s
                       OS version: %(os)s
                      destination: %(dest)s
                  # of processors: %(nproc)s
    =========================================================================
    """ % { "product" : self.product_name,
            "version" : self.version,
            "mtype"   : self.mtype,
            "os"      : get_os_version(),
            "dest"    : self.dest_dir,
            "nproc"   : self.options.nproc,
      }, file=self.out)

  def machine_type(self):
    """
    Determine the mtype string.  The four pre-defined mtypes will be detected
    automatically (with Linux kernel 3.x treated as 2.6), but additional mtypes
    can be used if this method is re-implemented.
    """
    return machine_type()

  #---------------------------------------------------------------------
  # BINARY INSTALL
  #
  def install_from_binary(self):
    """
    Unpackage the binary bundle in the destination directory.
    """
    # Copy base, build, and modules
    for i in ['base', 'conda_base', 'build', 'modules', 'doc']:
      if os.path.exists(os.path.join(self.installer_dir, i)):
        copy_tree(os.path.join(self.installer_dir, i), os.path.join(self.dest_dir, i))

    # Copy conda_base packaged with conda-pack if available
    # only this file or the conda_base directory will exist in the installer
    conda_base_tarfile = os.path.join(self.installer_dir, 'conda_base.tar')
    if os.path.exists(conda_base_tarfile):
      import subprocess
      import tarfile
      tarball = tarfile.open(conda_base_tarfile)
      dest_dir = os.path.join(self.dest_dir, 'conda_base')
      tarball.extractall(path=dest_dir)
      tarball.close()
      cwd = os.getcwd()
      os.chdir(dest_dir)
      unpack_cmd = os.path.join('.', 'bin', 'conda-unpack')
      if sys.platform == 'win32':
        unpack_cmd = os.path.join('.', 'Scripts', 'conda-unpack.exe')
      subprocess.check_call([unpack_cmd])
      os.chdir(cwd)

    # Reconfigure
    log = open(os.path.join(self.tmp_dir, "binary.log"), "w")
    if not os.path.exists(self.tmp_dir):
      os.makedirs(self.tmp_dir)
    if (sys.platform != "darwin"):
      os.environ["LD_LIBRARY_PATH"] = "lib:%s:%s" % \
        (op.join(self.base_dir, "lib"), op.join(self.base_dir, "lib64"))
    self.product_specific_binary_install(log=log)

    os.chdir(self.build_dir)
    print("Configuring %s components..."%(self.product_name), file=self.out)
    self.reconfigure(log=log)

  #---------------------------------------------------------------------
  # SOURCE INSTALL
  #
  def install_from_source(self):
    log = self.out # open(os.path.join(self.tmp_dir, "source.log"), "w")
    cmd = [
      'python',
      os.path.join('modules', 'cctbx_project', 'libtbx', 'auto_build', 'bootstrap.py'),
      'base',
      'build',
      '--builder', self.dest_dir_prefix,
      '--nproc', str(self.options.nproc),
    ]
    if self.options.use_conda:
      cmd.append('--use-conda')
      self.base_dir = op.join(self.dest_dir, "conda_base")
    call(cmd, log=log)
    self.product_specific_source_install(log=log)
    self.install_from_binary()

  def show_installation_paths(self):
    print("""
%(product)s installation target directory <%(product)s_LOC> set to:
   %(dest_dir)s
%(product)s installation source directory set to:
   %(inst_dir)s
%(product)s installation build directory set to:
   %(build_dir)s
%(product)s temporary build directory set to:
   %(tmp_dir)s
""" % { "product" : self.product_name,
        "dest_dir" : self.dest_dir,
        "build_dir" : self.build_dir,
        "inst_dir" : self.installer_dir,
        "tmp_dir" : self.tmp_dir, }, file=self.out)

  #---------------------------------------------------------------------
  # NON-COMPILED COMPONENTS AND FINAL SETUP
  #
  def reconfigure(self, log):
    """
    Run libtbx/configure.py to configure the build in the new location.
    """
    os.chdir(self.build_dir)

    base_python = os.path.join(self.base_dir, 'bin', 'python')
    if 'win32' in sys.platform:
      base_python = os.path.join(self.base_dir, 'bin', 'python', 'python.exe')

    # check for conda
    if self.base_dir.endswith('conda_base'):
      if 'darwin' in sys.platform:
        base_python = os.path.join(self.base_dir, 'python.app', 'Contents',
                                   'MacOS', 'python')
      elif 'win32' in sys.platform:
        base_python = os.path.join(self.base_dir, 'python.exe')

    args = [
      base_python,
      os.path.join(self.modules_dir, 'cctbx_project', 'libtbx', 'configure.py'),
      "--current_working_directory", self.build_dir
    ]
    if 'win32'==sys.platform:
      args = [
        base_python,
        os.path.join(self.modules_dir, 'cctbx_project', 'libtbx', 'configure.py'),
      ]
    if 'create_versioned_dispatchers' in self.flags:
      args += [ "--command_version_suffix", self.version ]
    args += self.configure_modules

    # check for conda
    if self.base_dir.endswith('conda_base'):
      args += ['--use_conda']

    if self.options.verbose:
      print(self.build_dir)
      print(args)

    if 1: #try :
      call(args=args, log=log, verbose=self.options.verbose)
    else: #except RuntimeError :
      raise InstallerError("Configuration step incomplete!  See the log file for detailed error messages.")

  def install_finalize(self):
    """
    Set up dispatchers and assorted shell scripts, create app bundles, etc.
    """
    self.print_header('Finalizing %s installation'%self.product_name)
    log_path = op.join(self.tmp_dir, "install_finalize.log")
    print("Log file: %s"%log_path, file=self.out)
    log = open(log_path, "w")

    # Write environment files.
    self.write_environment_files()

    # Regenerate module files.
    if (self.flag_build_gui) and (sys.platform != "darwin") and \
      (not self.base_dir.endswith('conda_base')):
      os.environ["LD_LIBRARY_PATH"] = "lib:%s:%s" % \
        (op.join(self.base_dir, "lib"), op.join(self.base_dir, "lib64"))
      regenerate_module_files.run(
        os.path.join(self.dest_dir, 'base'),
        out=self.out)

    # Write dispatcher_include file.
    print("Generating %s environment additions for dispatchers..." % \
      self.product_name, file=self.out)
    fnsuffix = '.sh'
    envcmd = "export"
    if sys.platform == "win32":
      fnsuffix = '.bat'
      envcmd = "set"
    dispatcher = op.join(self.build_dir, "dispatcher_include_%s%s" %
      (self.dest_dir_prefix, fnsuffix))
    if (op.isfile(dispatcher)):
      os.remove(dispatcher)
    env_prefix = self.product_name.upper() # e.g. "Phenix" -> "PHENIX"
    prologue = "\n".join([
      "%s %s=\"%s\"" % (envcmd, env_prefix, self.dest_dir),
      "%s %s_VERSION=%s" % (envcmd, env_prefix, self.version),
      "%s %s_ENVIRONMENT=1" % (envcmd, env_prefix),
      "%s %s_MTYPE=%s" % (envcmd, env_prefix, self.mtype),
    ] + self.product_specific_dispatcher_prologue())
    epilogue = "\n".join(self.product_specific_dispatcher_epilogue())
    dispatcher_opts = [
      "--build_dir=%s" % self.build_dir,
      "--base_dir=%s" % self.base_dir,
      "--suffix=%s" % self.dest_dir_prefix,
      "--gtk_version=2.10.0", # XXX this can change!
      "--quiet",
    ]
    if (not self.flag_build_gui):
      dispatcher_opts.append("--ignore_missing_dirs")
    # check for conda
    if (self.base_dir.endswith('conda_base')):
      dispatcher_opts += ["--use_conda", "--ignore_missing_dirs"]
    # FIXME this will happen regardless of whether the GUI modules are being
    # distributed or not - will this be problematic?
    print('Calling write_gui_dispatcher_include')
    print('  args %s' % dispatcher_opts)
    print('  prologue %s' % prologue)
    print('  epilogue %s' % epilogue)
    write_gui_dispatcher_include.run(
      args=dispatcher_opts,
      prologue=prologue,
      epilogue=epilogue,
      out=self.out)
    assert op.isfile(dispatcher)

    # Run configure.py to generate dispatchers
    print("Configuring %s components..." % self.product_name, file=self.out)
    os.chdir(self.build_dir)
    # ???
    if (op.exists("libtbx_refresh_is_completed")):
      os.remove("libtbx_refresh_is_completed")
    self.reconfigure(log=log)
    os.chdir(self.build_dir)
    assert op.isfile("setpaths%s" %fnsuffix)
    if sys.platform != "win32":
      os.environ["PATH"] = "%s:%s" % (op.join(self.build_dir, "bin"), os.environ["PATH"])
    else:
      os.environ["PATH"] = "%s;%s" % (op.join(self.build_dir, "bin"), os.environ["PATH"])

    if not self.options.nopycompile:
      # Compile .py files
      print("Precompiling .py files...", file=self.out)
      os.chdir(self.modules_dir)
      call(args="libtbx.py_compile_all -i", log=log)

    # Copy README et al.
    for file_name in ["CHANGES", "LICENSE", "README", "README-DEV", "SOURCES"] :
      src_file = op.join(self.installer_dir, file_name)
      if op.exists(src_file):
        dest_file = op.join(self.dest_dir, file_name)
        # XXX use our own implementation instead of shutil.copyfile
        if sys.platform == "win32" and src_file==dest_file:
          # writing to the same file on Windows renders it empty
          continue
        copy_file(src_file, dest_file)

    # generate .app (Mac only)
    apps_built = False
    if ((sys.platform == "darwin") and (len(self.make_apps) > 0) and
        (not self.options.no_app) and self.flag_build_gui):
      os.chdir(self.build_dir)
      for app_name in self.make_apps :
        args = [
          "libtbx.create_mac_app",
          app_name,
          "--app_name=%s-%s" % (app_name, self.version),
          "--dest=%s" % self.dest_dir,
          "--alias_build"
        ]
        print("Generating Mac app launcher for %s..."%app_name, file=self.out)
        try :
          call(args=" ".join(args), log=log)
        except RuntimeError as e:
          print("  ERROR:")
          print("  " + str(e))
          print("  installation will continue anyway.")
        else :
          app_file = op.join(self.dest_dir, "%s-%s.app" %
            (app_name, self.version))
          if (not op.exists(app_file)):
            print(" failed.", file=self.out)
            app_file = None
          else :
            apps_built = True

    # run custom finalization
    self.product_specific_finalize_install(log)

    # remove source files if desired
    if (self.options.compact):
      self.reduce_installation_size()

    self.display_final_message()

    if sys.platform == "win32":
      return

    # Fix permissions
    call([
      'chmod',
      '-R',
      'u+rw,a+rX',
      self.dest_dir
      ])

    # Show the app.
    if apps_built and (not "SSH_CLIENT" in os.environ):
      try:
        call(args=["open", self.dest_dir], log=self.out)
      except Exception:
        # Will fail in non-interactive environments.
        pass

  #---------------------------------------------------------------------
  # STUBS FOR SUBCLASSABLE METHODS

  def write_environment_files(self):
    """
    Generate shell scripts in the top-level installation directory that can
    be used to set up the user environment to run the software.  This is
    implemented as a separate method because some products (e.g. Phenix) may
    have their own needs.
    """
    # csh/tcsh/bat environment setup file
    print("Generating %s environment setup scripts..."%self.product_name, file=self.out)
    env_prefix = self.product_name.upper() # e.g. "Phenix" -> "PHENIX"
    if sys.platform == "win32":
      f = open(os.path.join(self.dest_dir, '%s_env.bat'%self.dest_dir_prefix), 'w')
      f.write("@echo off\n")
      # Use the %~dp0 alias for specifying the full path to where phenix_env.bat will be located.
      # This presumes phenix_env.bat will always reside where it has originally been installed to.
      f.write("set %s=%%~dp0\n" %env_prefix)
      f.write("set %s_VERSION=%s\n" % (env_prefix, self.version))
      f.write("call \"%%%s%%\\build\\setpaths.bat\"\n" % (env_prefix))
      f.close()
      return

    f = open(os.path.join(self.dest_dir, '%s_env.csh'%self.dest_dir_prefix), 'w')
    f.write("#!/bin/csh -f\n")
    f.write("setenv %s \"%s\"\n" % (env_prefix, self.dest_dir))
    f.write("setenv %s_VERSION %s\n" % (env_prefix, self.version))
    f.write("source $%s/build/setpaths.csh\n" % (env_prefix))
    f.close()

    f = open(os.path.join(self.dest_dir, '%s_env.sh'%self.dest_dir_prefix), 'w')
    f.write("#!/bin/sh\n")
    f.write("#\n")
    f.write("export %s=\"%s\"\n" % (env_prefix, self.dest_dir))
    f.write("export %s_VERSION=%s\n" % (env_prefix, self.version))
    f.write(". $%s/build/setpaths.sh\n" % (env_prefix))
    f.close()

  def get_version(self):
    """
    Determine the version suffix (if any) for the destination directory.  This
    must be implemented by subclasses unless a file named VERSION is present
    at the top level of the installer folder.
    """
    version_file = op.join(self.installer_dir, "VERSION")
    if op.isfile(version_file):
      return open(version_file).read().strip()
    return NotImplementedError()

  def reduce_installation_size(self):
    """
    Remove all files not required for program execution to save disk space,
    such as any C++ sources.  This can potentially save well over 100MB and is
    recommended for purely user-facing packages, but may make it more difficult
    to use packages for development purposes.
    """
    print("Removing unnecessary files to reduce disk usage...", end=' ', file=self.out)
    # XXX should this include .o files?
    remove_extensions = [".cpp", ".cc", ".hpp", ".h", ".hh"]
    n_deleted = 0
    for dirname, dirnames, filenames in os.walk(self.modules_dir):
      for file_name in filenames :
        for ext in remove_extensions :
          if file_name.endswith(ext):
            full_path = op.join(dirname, file_name)
            try :
              os.remove(full_path)
            except Exception as e:
              print("  WARNING: error removing %s" % full_path, file=self.out)
              print(str(e), file=self.out)
            else :
              n_deleted += 1
    n_deleted_other = self.product_specific_reduce_installation_size()
    if (n_deleted_other is not None):
      n_deleted += n_deleted_other
    print("%d files deleted" % n_deleted, file=self.out)

  def add_product_specific_options(self, parser):
    """
    Add command-line options specific to the distributed package.
    """
    pass

  def product_specific_prepackage_hook(self, directory):
    """
    Modify files, etc. before the installer package is created.

    :param directory: base directory of the installer package
    """
    pass

  def product_specific_preinstallation_hook(self):
    """
    Perform additional checks on parsed command line options or the
    destination machine before any installation action takes place.
    """
    pass

  def product_specific_binary_install(self, log):
    """
    Perform additional actions required for the binary installation of a
    specific product.
    """
    pass

  def product_specific_setup_before_compile(self, log):
    """
    Perform any necessary modifications to the sources prior to compilation.
    """
    pass

  def product_specific_source_install(self, log):
    """
    Build additional sources, e.g. Rosetta
    """
    pass

  def product_specific_dispatcher_prologue(self):
    """
    Environment modifications to be included near the start of the dispatchers.
    """
    return []

  def product_specific_dispatcher_epilogue(self):
    """
    Environment modifications to be included at the end of the dispatchers.
    """
    return []

  def product_specific_finalize_install(self, log):
    """
    Additional installation setup, file cleanup, more add-ons, etc.
    """
    pass

  def product_specific_reduce_installation_size(self, log):
    """
    Remove unused files specific to this product, and return the number deleted.
    """
    return 0

  def display_final_message(self):
    """
    Final instructions for user, etc.
    """
    pass

  def print_header(self, msg):
    print("", file=self.out)
    print("*"*(len(msg) + 4), file=self.out)
    print("* %s *"%msg, file=self.out)
    print("*"*(len(msg) + 4), file=self.out)
    print("", file=self.out)
