#!/usr/bin/python
# This file is part of ModPipe, Copyright 1997-2010 Andrej Sali
#
# ModPipe is free software: you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ModPipe.  If not, see <http://www.gnu.org/licenses/>.

"""Do basic setup of ModPipe"""

import os
import sys
import glob
import compileall
import commands
from optparse import OptionParser

# ModPipe version number (modify for each new release)
version = "2.2.0"

# On newer Pythons, use subprocess rather than the deprecated os.popen4 to
# run subprocesses
try:
    import subprocess
    def run_command(cmd):
        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                             close_fds=True)
        return (p.stdin, p.stdout)
except ImportError:
    def run_command(cmd):
        return os.popen4(cmd)

# Check to make sure we're in the ModPipe root
if not os.path.exists('Setup'):
  print "Please change into the top directory of your ModPipe checkout,"
  print "then run this script as ./Setup"
  sys.exit(1)

# If we're using an SVN trunk checkout, try to get the revision number
if version == 'SVN':
    (status, output) = commands.getstatusoutput('svnversion')
    if status == 0 and output != 'exported':
        version = "SVN.r" + output

MODPIPEBASE = os.getcwd()

# Parse command line options
class NonvitalPath:
    def __init__(self, s): self.s = s
    def __str__(self): return self.s

parser = OptionParser(version=version)
parser.add_option('--with-cd-hit', dest='cd_hit', type='string',
                  help="""Directory containing the 'cd-hit' executable
                          or 'no' if you do not want ModPipe to use CD-HIT.""",
                  metavar="DIR", default=NonvitalPath("ext/cd-hit/bin"))
parser.add_option('--with-blast', dest='blast', type='string',
                  help="""Directory containing BLAST executables
                          (formatdb, blastall, blastpgp), or 'no' if you
                          do not want ModPipe to use BLAST.""",
                  metavar="DIR", default="ext/blast/bin")
parser.add_option('--with-spasm', dest='spasm', type='string',
                  help="""Directory containing SPASM executables
                          (mkspaz, savant, spasm), or 'no' if you
                          do not want ModPipe to use SPASM.""",
                  metavar="DIR", default=NonvitalPath("ext/spasm/bin"))
parser.add_option('--with-jess', dest='jess', type='string',
                  help="""Directory containing the JESS executables
                          (jess, tess2jess) or 'no' if you do not want
                          ModPipe to use JESS.""",
                  metavar="DIR", default=NonvitalPath("ext/Jess/bin"))
parser.add_option('--with-ce', dest='ce', type='string',
                  help="""Directory containing the CE executables
                          (CE, mkDB) or 'no' if you do not want
                          ModPipe to use CE.""",
                  metavar="DIR", default=NonvitalPath("ext/ce"))
parser.add_option('--with-seg', dest='seg', type='string',
                  help="""Directory containing the 'seg' executable
                          or 'no' if you do not want ModPipe to use SEG.""",
                  metavar="DIR", default=NonvitalPath("ext/seg/bin"))
parser.add_option('--with-tmalign', dest='tmalign', type='string',
                  help="""Directory containing the 'TMalign' executable
                          or 'no' if you do not want ModPipe to use TMalign.""",
                  metavar="DIR", default=NonvitalPath("ext/TM-Align"))
opts, args = parser.parse_args()

def check_external_package(dir, package, option, binaries):
    strdir = str(dir)
    if strdir.upper() in ('NO', 'OFF', '0', 'FALSE'):
        return False
    for bin in binaries:
        path = os.path.join(strdir, bin)
        if not os.path.exists(path) and not os.path.islink(path):
            if isinstance(dir, NonvitalPath):
                print """
Warning: could not find external %s binary '%s' in '%s'.
  Continuing anyway, since %s is not vital for most ModPipe operations.
  To add support, rerun Setup with the %s option to specify a
  different path where it is installed (e.g. %s=/usr/bin), or
  use %s=no to disable support for %s in ModPipe.
""" % (package, bin, strdir, package, option, option, option, package)
                return False
            else: # A 'vital' package, or the user explictly asked for it
                print """
** Could not find external %s binary '%s' in '%s'.
   Please install %s in this location (see the documentation), or
   use the %s option to specify a different path where it is
   installed (e.g. %s=/usr/bin), or use %s=no to disable
   support for %s in ModPipe.
""" % (package, bin, strdir, package, option, option, option, package)
                sys.exit(1)
    return os.path.abspath(strdir)

# Update install location in Modeller's config.py
if not os.path.exists('ext/mod/modlib/modeller'):
    print "You need to have a copy of MODELLER in the ext/mod/ directory."
    print "Please install it (see the documentation) then rerun this script."
    sys.exit(1)
config = 'ext/mod/modlib/modeller/config.py'
lines = file(config).readlines()
f = file(config, 'w')
print >> f, "install_dir = r'%s/ext/mod'" % MODPIPEBASE
for line in lines:
    if line.startswith('license'):
        f.write(line)
f.close()
# End of Modeller config.py

opts.cd_hit = check_external_package(opts.cd_hit, 'CD-HIT', '--with-cd-hit',
                                     ('cd-hit',))
opts.blast = check_external_package(opts.blast, 'BLAST', '--with-blast',
                                    ('formatdb', 'blastall', 'blastpgp'))
opts.spasm = check_external_package(opts.spasm, 'SPASM', '--with-spasm',
                                    ('mkspaz', 'savant', 'spasm'))
opts.jess = check_external_package(opts.jess, 'JESS', '--with-jess',
                                   ('jess', 'tess2jess'))
opts.seg = check_external_package(opts.seg, 'SEG', '--with-seg', ('seg',))
opts.tmalign = check_external_package(opts.tmalign, 'TMalign', '--with-tmalign',
                                      ('TMalign',))
opts.ce = check_external_package(opts.ce, 'CE', '--with-ce', ('CE', 'mkDB'))

detect_arch = """# Detect architecture
U_M=`uname -m 2>/dev/null` || U_M=UNK
case "${U_M}" in
  i386|i586|i686)
    ARCH="i386"
    ALLARCH="i386"
    ;;
  x86_64)
    ARCH="x86_64"
    ALLARCH="x86_64 i386"
    ;;
  ia64)
    ARCH="ia64"
    ALLARCH="ia64"
    ;;
  *)
    echo "Unsupported architecture: ${U_M}"
    exit 1
esac"""

copyright = "Copyright 1997-2010 Andrej Sali"
license_text = \
"""# This file is part of ModPipe, %s
#
# ModPipe is free software: you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ModPipe.  If not, see <http://www.gnu.org/licenses/>.""" \
    % copyright

# Set up search path for scripts
f = file('int/runscript.sh', 'w')
print >> f, """#!/bin/sh
%s

# This script sets up all necessary paths and environment variables to run
# ModPipe scripts. It is autogenerated by ./Setup. It can be run in two ways:
# either directly, in which case the rest of the command line is run with the
# ModPipe environment (e.g. "int/runscript.sh python"); or via a symlink, in
# which case the original ModPipe script is run in the ModPipe environment.
#
# If you are looking at this script and expecting to see a ModPipe Python or
# Perl script, look under this directory in the int/ subdirectory for the
# actual script that does the work. This script only sets up the environment.

BASE="%s"
export PERL5LIB="${BASE}/lib/perl"

""" % (license_text, MODPIPEBASE) + detect_arch + """
# Set up Python environment for Modeller and ModPipe
MOD="${BASE}/ext/mod"
export PYTHONPATH="${MOD}/lib/${ARCH}-intel8:${MOD}/modlib:${BASE}/lib/python"
export LD_LIBRARY_PATH="${MOD}/lib/${ARCH}-intel8"
script="`basename $0`"

if test "${script}" = "runscript.sh" ; then
  # If run directly, now run the rest of the command line with our
  # modified environment
  exec "$@"
else
  # If run via a symlink, transfer control to the real script
  dir="`dirname $0`"
  exec "${dir}/int/${script}" "$@"
fi
"""
f.close()
os.chmod('int/runscript.sh', 0755)

# Equivalent Python script to set up search path for Python scripts
# (so this can also be run as "python foo.py")
f = file('int/runpyscript.py', 'w')
print >> f, """#!/usr/bin/python
%s

# This script sets up all necessary paths and environment variables to run
# ModPipe Python scripts. It is autogenerated by ./Setup. See int/runscript.sh
# for more details.

import os, sys

base = r"%s"

# Detect architecture
arch = os.uname()[-1]
if arch in ('i686', 'i586'):
    arch = 'i386'

# Set up Python environment for Modeller and ModPipe
mod = base + "/ext/mod"
os.environ['PYTHONPATH'] = "%%(mod)s/lib/%%(arch)s-intel8:%%(mod)s/modlib:%%(base)s/lib/python" %% locals()
os.environ['LD_LIBRARY_PATH'] = "%%(mod)s/lib/%%(arch)s-intel8" %% locals()

script = os.path.basename(sys.argv[0])
if script == 'runpyscript.py':
    sys.argv.pop(0)
else:
    dir = os.path.dirname(sys.argv[0])
    sys.argv[0] = os.path.join(dir, 'int', script)

# Transfer control to real script
os.execv(sys.executable, ([sys.executable] + sys.argv))""" \
    % (license_text, MODPIPEBASE)
f.close()
os.chmod('int/runpyscript.py', 0755)

# Utility script to run architecture-specific binaries in ext/ subdirectory
f = file('int/runbinary.sh', 'w')
print >> f, """#!/bin/sh

export MODPIPEBASE="%s"
""" % MODPIPEBASE + detect_arch + """
dir="`dirname $0`"
binary="`basename $0`"
for arch in ${ALLARCH}; do
  bin="${dir}/${arch}/${binary}"
  if test -f "${bin}"; then
    exec "${bin}" "$@"
  fi
done
echo "${binary} is not supported on this architecture (${ARCH})."
exit 1"""
f.close()
os.chmod('int/runbinary.sh', 0755)

# Check for Modeller presence and features
(child_stdin, child_stdout) = run_command('int/runpyscript.py')
print >> child_stdin, """import sys
inst="\\nPlease install Modeller version 9v7 or later in the ext/mod/ directory."
try:
    import modeller
except ImportError:
    print "ModPipe needs Modeller to function." + inst
    sys.exit(1)
import inspect
spec = inspect.getargspec(modeller.alignment.append)
if 'allow_alternates' not in spec[0]:
    print "ModPipe needs a version of Modeller new enough to have the"
    print "allow_alternates keyword in alignment.append()." + inst
"""
child_stdin.close()
child_stdout = child_stdout.read()
if child_stdout:
    sys.stdout.write(child_stdout)
    sys.exit(1)

# Link all scripts to search path utility script
scripts = []
dirs = ['main', 'src', 'python']
if os.path.exists('aux'):
    dirs.append('aux')
    scripts.append('aux/int/CopyFile')
for dir in dirs:
    scripts.extend(glob.glob(dir + '/int/*.pl') + glob.glob(dir + '/int/*.sh') \
                   + glob.glob(dir + '/int/*.py'))
for script in scripts:
    spl = script.split(os.path.sep)
    del spl[1:-1]
    if spl[-1] != '__init__.py':
        dest = os.path.sep.join(spl)
        try:
            os.unlink(dest)
        except OSError:
            pass
        if dest.endswith('.py'):
            os.symlink('../int/runpyscript.py', dest)
        else:
            os.symlink('../int/runscript.sh', dest)

# Do autoconf-style replacements:
for f in ['ext/procheck/setup.scr', 'ext/procheck/i686/procheck.scr',
          'ext/mod/bin/modpy.sh']:
    if os.path.exists(f + '.in'):
        fin = file(f + '.in', 'r')
        fout = file(f, 'w')
        for line in fin:
            line = line.replace('@MODPIPEBASE@', MODPIPEBASE)
            fout.write(line)
    else:
        print "Warning: input script %s.in not found" % f
for f in ['ext/procheck/setup.scr', 'ext/procheck/i686/procheck.scr',
          'ext/mod/bin/modpy.sh']:
    if os.path.exists(f):
        os.chmod(f, 0755)

# Build version-specific libraries
f = file("lib/perl/MPLib/Version.pm", "w")
print >> f, """%s

package MPLib::Version;
require Exporter;
@ISA    = qw(Exporter);
@EXPORT = qw(GetVersion VersionMessage);

use strict;
use File::Basename;

sub GetVersion {
  return "%s";
}

sub VersionMessage {
  my $script = basename($0);
  print "$script, part of ModPipe version " . GetVersion() . "\\n";
  print "%s\\n";
  print "License GPLv2: GNU GPL version 2 ";
  print "<http://gnu.org/licenses/gpl.html>\\n";
  print "This is free software: you are free to change and redistribute it.\\n";
  print "There is NO WARRANTY, to the extent permitted by law.\\n";

  exit 1;
}
""" % (license_text, version, copyright)

f = file("lib/python/modpipe/version.py", "w")
print >> f, """%s

def get():
    return "%s"

def message():
    return "%%prog, part of ModPipe version " + get() + \"\"\"
%s
License GPLv2: GNU GPL version 2 <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.\"\"\"""" \
    % (license_text, version, copyright)
f.close()

# Generate files giving paths to binaries
f = file("lib/python/modpipe/binaries.py", "w")
print >> f, """%s

import os
import modpipe
import modpipe.miscutils

# This file is auto-generated by Setup.

_modpipebase = r'%s'""" % (license_text, MODPIPEBASE)

for (method, option, package, longopt) in \
    (('get_blast', opts.blast, 'BLAST', '--with-blast'),
     ('get_spasm', opts.spasm, 'SPASM', '--with-spasm')):
    print >> f, "\ndef %s(bin):" % method
    if option:
        print >> f, "    return os.path.join(r'%s', bin)" % option
    else:
        print >> f, "    raise modpipe.MissingBinaryError('%s is not available, because ModPipe was not configured with it. To fix this, rerun the Setup script specifying the path to the %s package with the %s option')" % (package, package, longopt)

for (method, option, bin, package) in \
    (('get_cd_hit', opts.cd_hit, 'cd-hit', 'CD-HIT'),
     ('get_jess', opts.jess, 'jess', 'JESS'),
     ('get_tess2jess', opts.jess, 'tess2jess', 'JESS'),
     ('get_seg', opts.seg, 'seg', 'SEG'),
     ('get_tmalign', opts.tmalign, 'TMalign', 'TMalign'),
     ('get_ce', opts.ce, 'CE', 'CE'),
     ('get_ce_mkdb', opts.ce, 'mkDB', 'CE')):
    print >> f, "\ndef %s():" % method
    if option:
        print >> f, "    return os.path.join(r'%s', '%s')" % (option, bin)
    else:
        print >> f, "    raise modpipe.MissingBinaryError('ModPipe was not configured with %s')" % package
f.close()

f = file("lib/perl/MPLib/Binaries.pm", "w")
print >> f, """%s

package MPLib::Binaries;
require Exporter;
@ISA    = qw(Exporter);
@EXPORT = qw(GetModeller GetBlast GetModPipeScript);

# This file is auto-generated by Setup.

use strict;

my $modpipebase = '%s';

sub GetModeller {
  return $modpipebase . "/ext/mod/bin/modpy.sh %s";
}

sub GetModPipeScript {
  my ($script) = @_;
  return $modpipebase . "/" . $script;
}

sub GetBlast {
  my ($bin) = @_;""" \
  % (license_text, MODPIPEBASE, sys.executable)

if opts.blast:
    print >> f, "  return '%s' . '/' . $bin;\n}" % opts.blast
else:
    print >> f, "  die 'ModPipe is not configured with BLAST';\n}"
f.close()

# Compile Python modules. This is useful to reduce NFS load for a ModPipe
# run (.pyc files do not then need to be generated on the cluster nodes).
sys.stdout.write("Compiling all Python modules ... ")
sys.stdout.flush()
for topdir in ('lib/python', 'ext'):
    compileall.compile_dir(topdir, quiet=1)
print "done"

