#!/usr/bin/env python

# Copyright (c) 2002 Trent Mick
# 
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# 
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
    px/p4lib.py build script

    Usage:
        python build.py [<options>...] [<targets>...]

    Options:
        --help, -h      Print this help and exit.
        --version       Print the version of this script and exit.
        --targets, -t   List all available targets.

    This is the primary build script for the px/p4lib.py project. It
    exists to assist in building, maintaining, and distributing this
    project.
    
    It is intended to have Makefile semantics. I.e. 'python build.py'
    will build execute the default target, 'python build.py foo' will
    build target foo, etc. However, there is no intelligent target
    interdependency tracking (I suppose I could do that with function
    attributes).
"""

import os
import sys
import getopt
import types
import getpass
import glob
import shutil


class BuildError(Exception):
    pass

#---- globals

# Site upload information.
gSiteUser = 'tmick'
gSite = 'starship.python.net'
gSiteDownloadsDir = '~tmick/public_html/downloads'


#---- internal logging facility

class _Logger:
    DEBUG, INFO, WARN, ERROR, CRITICAL = range(5)
    def __init__(self, threshold=None, streamOrFileName=sys.stderr):
        if threshold is None:
            self.threshold = self.WARN
        else:
            self.threshold = threshold
        if type(streamOrFileName) == types.StringType:
            self.stream = open(streamOrFileName, 'w')
            self._opennedStream = 1
        else:
            self.stream = streamOrFileName
            self._opennedStream = 0
    def __del__(self):
        if self._opennedStream:
            self.stream.close()
    def _getLevelName(self, level):
        levelNameMap = {
            self.DEBUG: "DEBUG",
            self.INFO: "INFO",
            self.WARN: "WARN",
            self.ERROR: "ERROR",
            self.CRITICAL: "CRITICAL",
        }
        return levelNameMap[level]
    def log(self, level, msg):
        if level < self.threshold:
            return
        message = "%s: " % self._getLevelName(level).lower()
        message = message + msg + "\n"
        self.stream.write(message)
        self.stream.flush()
    def debug(self, msg):
        self.log(self.DEBUG, msg)
    def info(self, msg):
        self.log(self.INFO, msg)
    def warn(self, msg):
        self.log(self.WARN, msg)
    def error(self, msg):
        self.log(self.ERROR, msg)
    def fatal(self, msg):
        self.log(self.CRITICAL, msg)

if 1:   # normal
    log = _Logger(_Logger.INFO)
else:   # debugging
    log = _Logger(_Logger.DEBUG)



#---- globals

_version_ = (0, 1, 0)



#---- support routines

def _rmtree_OnError(rmFunction, filePath, excInfo):
    if excInfo[0] == OSError:
        # presuming because file is read-only
        os.chmod(filePath, 0777)
        rmFunction(filePath)

def _rmtree(dirname):
    shutil.rmtree(dirname, 0, _rmtree_OnError)


def _getTargets():
    """Find all targets and return a dict of targetName:targetFunc items."""
    targets = {}
    for name, attr in sys.modules[__name__].__dict__.items():
        if name.startswith('target_'):
            targets[ name[len('target_'):] ] = attr
    return targets

def _listTargets(targets):
    """Pretty print a list of targets."""
    width = 77
    nameWidth = 15 # min width
    for name in targets.keys():
        nameWidth = max(nameWidth, len(name))
    nameWidth += 2  # space btwn name and doc
    format = "%%-%ds%%s" % nameWidth
    print format % ("TARGET", "DESCRIPTION")
    for name, func in targets.items():
        doc = func.__doc__ or ""
        if len(doc) > (width - nameWidth):
            doc = doc[:(width-nameWidth-3)] + "..."
        print format % (name, doc)


#---- the targets

def target_default():
    """Build all distributions (that *can* be built on this plat)."""
    log.info("target: default")
    if sys.platform.startswith('win'):
        target_px_launcher()
    elif sys.platform.startswith('linux'):
        target_px_script()
    target_sdist()

def target_clean():
    log.info("target: clean")
    if sys.platform.startswith('win'):
        cmd = "nmake -f Makefile.win clean"
        retval = os.system(cmd)
        if retval:
            raise BuildError("Error cleaning launcher: cmd=%r" % cmd)

    import px
    ver = '.'.join([str(part) for part in px._version_])
    dirs = ["dist", "build", "px-%s" % ver]
    for d in dirs:
        print "removing '%s'" % d
        if os.path.isdir(d): _rmtree(d)

    patterns = ["*.pyc", "*~", "MANIFEST", "px",
                os.path.join("test", "*~"),
                os.path.join("test", "*.pyc"),
               ]
    for pattern in patterns:
        for file in glob.glob(pattern):
            print "removing '%s'" % file
            os.unlink(file)

def target_px_script():
    """Rename the px script as appropriate for linux."""
    log.info("target: px_script")
    if sys.platform.startswith("linux"):
        shutil.copy("px.py", "px")

def target_px_launcher():
    """Build the px launcher executable."""
    log.info("target: px_launcher")
    if sys.platform.startswith("win"):
        cmd = "nmake -f Makefile.win px.exe"
        retval = os.system(cmd)
        if retval:
            raise BuildError("Error building px_launcher: cmd=%r" % cmd)

def target_sdist():
    """Build a source distribution for the current platform."""
    if sys.platform.startswith("win"):
        target_sdist_win()
    else:
        target_sdist_linux()

def target_sdist_linux():
    """Build a Un*x source distribution."""
    log.info("target: sdist_linux")
    cmd = 'python setup.py sdist --format=gztar'
    retval = os.system(cmd)
    if retval:
        raise BuildError("Error building sdist_linux: cmd=%r" % cmd)

def target_sdist_win():
    """Build a Windows source distribution."""
    log.info("target: sdist_win")
    if not sys.platform.startswith("win"):
        log.warn("Building a Windows src dist on a non-Windows platform. "\
                 "The file line endings will be screwed up.\n")
    cmd = 'python setup.py sdist --format=zip'
    retval = os.system(cmd)
    if retval:
        raise BuildError("Error building sdist_win: cmd=%r" % cmd)

def target_install():
    """Use the setup.py script to install px/p4lib.py."""
    log.info("target: install")
    cmd = 'python setup.py install'
    retval = os.system(cmd)
    if retval:
        raise BuildError("Error installing: cmd=%r" % cmd)

def target_upload():
    """Upload binary and source distribution to starship."""
    log.info("target: upload")
    import px
    ver = '.'.join([str(i) for i in px._version_])
    if sys.platform.startswith("win"):
        srcs = [os.path.join('dist', 'px-%s.zip' % ver)]
        doNotExist = [s for s in srcs if not os.path.exists(s)]
        if doNotExist:
            raise BuildError("Cannot upload the following because they do "\
                             "not exist: %r" % doNotExist)
        passwd = getpass.getpass("%s@%s's password: " % (gSiteUser, gSite))
        dstDir = gSiteDownloadsDir + '/px/%s/win32/' % ver
        # Ensure the dst dir is made.
        cmd = 'plink -pw %s %s@%s mkdir -p %s'\
              % (passwd, gSiteUser, gSite, dstDir)
        log.info("Create %s on %s." % (dstDir, gSite))
        retval = os.system(cmd)
        if retval:
            raise BuildError("Error creating %r on %r." % (dstDir, gSite))
        # Upload the src files.
        for src in srcs:
            log.info("Upload %s to %s." % (src, dstDir))
            cmd = 'pscp -pw %s %s %s@%s:%s'\
                  % (passwd, src, gSiteUser, gSite, dstDir)
            retval = os.system(cmd)
            if retval:
                raise BuildError("Error uploading to %r" % dstDir)
    else:
        srcs = [os.path.join('dist', 'px-%s.tar.gz' % ver)]
        doNotExist = [s for s in srcs if not os.path.exists(s)]
        if doNotExist:
            raise BuildError("Cannot upload the following because they do "\
                             "not exist: %r" % doNotExist)
        dstDir = gSiteDownloadsDir + '/px/%s/linux/' % ver
        # Ensure the dst dir is made.
        cmd = 'ssh %s@%s mkdir -p %s' % (gSiteUser, gSite, dstDir)
        log.info("Create %s on %s." % (dstDir, gSite))
        retval = os.system(cmd)
        if retval:
            raise BuildError("Error creating %r on %r." % (dstDir, gSite))
        # Upload the src files.
        for src in srcs:
            log.info("Upload %s to %s." % (src, dstDir))
            cmd = 'scp %s %s@%s:%s' % (src, gSiteUser, gSite, dstDir)
            retval = os.system(cmd)
            if retval:
                raise BuildError("Error uploading to %r" % dstDir)

def target_check_license():
    """List files whose license information is out of date."""
    license = """\
Copyright (c) 2002 Trent Mick

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
    os.path.walk(os.curdir, _checkForLicenseInFiles, license)

def _checkForLicenseInFile(license, filename):
    licLines = license.split('\n')
    fin = open(filename, 'r')
    srcLines = fin.readlines()
    fin.close()
    # Try to find first license line in the first 30 lines.
    for i in range(30):
        if i >= len(srcLines):
            print filename
            return
        elif srcLines[i].find(licLines[0]) != -1:
            break
    else:
        print filename
        return
    # Ensure that the subsequent lines include the rest of the
    # license.
    for licLine in licLines:
        if i >= len(srcLines) or srcLines[i].find(licLine) == -1:
            print filename
            return
        i += 1

def _checkForLicenseInFiles(license, dirname, basenames): 
    """Check that the given license is near the top of the given source
    files. (Called via os.path.walk.)
    """
    exts = ['.cpp', '.py']
    for basename in basenames:
        base, ext = os.path.splitext(basename)
        ext = ext.lower()
        if ext in exts:
            filename = os.path.join(dirname, basename)
            _checkForLicenseInFile(license, filename)


#---- mainline

def build(targets=[]):
    log.info("build(targets=%r)" % targets)
    available = _getTargets()
    if not targets:
        if available.has_key('default'):
            return available['default']()
        else:   
            log.warn("No default target available. Doing nothing.")
    else:
        for target in targets:
            if available.has_key(target):
                retval = available[target]()
                if retval:
                    raise BuildError("Error running '%s' target: retval=%s"\
                                     % (target, retval))
            else:
                raise BuildError("Unknown target: '%s'" % target)

def main(argv):
    # Process options.
    optlist, targets = getopt.getopt(argv[1:], 'hvt',
        ['help', 'version', 'targets'])
    for opt, optarg in optlist:
        if opt in ('-h', '--help'):
            sys.stdout.write(__doc__ + '\n')
            return 0
        elif opt in ('--version',):
            ver = '.'.join([str(i) for i in _version_])
            sys.stdout.write("px/p4lib.py build script %s\n" % ver)
            return 0
        elif opt in ('-t', '--targets'):
            return _listTargets(_getTargets())

    return build(targets)

if __name__ == "__main__":
    sys.exit( main(sys.argv) )

