diff options
Diffstat (limited to 'python/which/build.py')
-rw-r--r-- | python/which/build.py | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/python/which/build.py b/python/which/build.py new file mode 100644 index 000000000..3c8f09d39 --- /dev/null +++ b/python/which/build.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python +# Copyright (c) 2002-2005 ActiveState +# See LICENSE.txt for license details. + +""" + which.py dev build script + + Usage: + python build.py [<options>...] [<targets>...] + + Options: + --help, -h Print this help and exit. + --targets, -t List all available targets. + + This is the primary build script for the which.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 +from os.path import basename, dirname, splitext, isfile, isdir, exists, \ + join, abspath, normpath +import sys +import getopt +import types +import getpass +import shutil +import glob +import logging +import re + + + +#---- exceptions + +class Error(Exception): + pass + + + +#---- globals + +log = logging.getLogger("build") + + + + +#---- globals + +_project_name_ = "which" + + + +#---- internal support routines + +def _get_trentm_com_dir(): + """Return the path to the local trentm.com source tree.""" + d = normpath(join(dirname(__file__), os.pardir, "trentm.com")) + if not isdir(d): + raise Error("could not find 'trentm.com' src dir at '%s'" % d) + return d + +def _get_local_bits_dir(): + import imp + info = imp.find_module("tmconfig", [_get_trentm_com_dir()]) + tmconfig = imp.load_module("tmconfig", *info) + return tmconfig.bitsDir + +def _get_project_bits_dir(): + d = normpath(join(dirname(__file__), "bits")) + return d + +def _get_project_version(): + import imp, os + data = imp.find_module(_project_name_, [os.path.dirname(__file__)]) + mod = imp.load_module(_project_name_, *data) + return mod.__version__ + + +# Recipe: run (0.5.1) in /Users/trentm/tm/recipes/cookbook +_RUN_DEFAULT_LOGSTREAM = ("RUN", "DEFAULT", "LOGSTREAM") +def __run_log(logstream, msg, *args, **kwargs): + if not logstream: + pass + elif logstream is _RUN_DEFAULT_LOGSTREAM: + try: + log.debug(msg, *args, **kwargs) + except NameError: + pass + else: + logstream(msg, *args, **kwargs) + +def _run(cmd, logstream=_RUN_DEFAULT_LOGSTREAM): + """Run the given command. + + "cmd" is the command to run + "logstream" is an optional logging stream on which to log the command. + If None, no logging is done. If unspecifed, this looks for a Logger + instance named 'log' and logs the command on log.debug(). + + Raises OSError is the command returns a non-zero exit status. + """ + __run_log(logstream, "running '%s'", cmd) + retval = os.system(cmd) + if hasattr(os, "WEXITSTATUS"): + status = os.WEXITSTATUS(retval) + else: + status = retval + if status: + #TODO: add std OSError attributes or pick more approp. exception + raise OSError("error running '%s': %r" % (cmd, status)) + +def _run_in_dir(cmd, cwd, logstream=_RUN_DEFAULT_LOGSTREAM): + old_dir = os.getcwd() + try: + os.chdir(cwd) + __run_log(logstream, "running '%s' in '%s'", cmd, cwd) + _run(cmd, logstream=None) + finally: + os.chdir(old_dir) + + +# Recipe: rmtree (0.5) in /Users/trentm/tm/recipes/cookbook +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): + import shutil + shutil.rmtree(dirname, 0, _rmtree_OnError) + + +# Recipe: pretty_logging (0.1) in /Users/trentm/tm/recipes/cookbook +class _PerLevelFormatter(logging.Formatter): + """Allow multiple format string -- depending on the log level. + + A "fmtFromLevel" optional arg is added to the constructor. It can be + a dictionary mapping a log record level to a format string. The + usual "fmt" argument acts as the default. + """ + def __init__(self, fmt=None, datefmt=None, fmtFromLevel=None): + logging.Formatter.__init__(self, fmt, datefmt) + if fmtFromLevel is None: + self.fmtFromLevel = {} + else: + self.fmtFromLevel = fmtFromLevel + def format(self, record): + record.levelname = record.levelname.lower() + if record.levelno in self.fmtFromLevel: + #XXX This is a non-threadsafe HACK. Really the base Formatter + # class should provide a hook accessor for the _fmt + # attribute. *Could* add a lock guard here (overkill?). + _saved_fmt = self._fmt + self._fmt = self.fmtFromLevel[record.levelno] + try: + return logging.Formatter.format(self, record) + finally: + self._fmt = _saved_fmt + else: + return logging.Formatter.format(self, record) + +def _setup_logging(): + hdlr = logging.StreamHandler() + defaultFmt = "%(name)s: %(levelname)s: %(message)s" + infoFmt = "%(name)s: %(message)s" + fmtr = _PerLevelFormatter(fmt=defaultFmt, + fmtFromLevel={logging.INFO: infoFmt}) + hdlr.setFormatter(fmtr) + logging.root.addHandler(hdlr) + log.setLevel(logging.INFO) + + +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 sorted(targets.items()): + doc = _first_paragraph(func.__doc__ or "", True) + if len(doc) > (width - nameWidth): + doc = doc[:(width-nameWidth-3)] + "..." + print format % (name, doc) + + +# Recipe: first_paragraph (1.0.1) in /Users/trentm/tm/recipes/cookbook +def _first_paragraph(text, join_lines=False): + """Return the first paragraph of the given text.""" + para = text.lstrip().split('\n\n', 1)[0] + if join_lines: + lines = [line.strip() for line in para.splitlines(0)] + para = ' '.join(lines) + return para + + + +#---- build targets + +def target_default(): + target_all() + +def target_all(): + """Build all release packages.""" + log.info("target: default") + if sys.platform == "win32": + target_launcher() + target_sdist() + target_webdist() + + +def target_clean(): + """remove all build/generated bits""" + log.info("target: clean") + if sys.platform == "win32": + _run("nmake -f Makefile.win clean") + + ver = _get_project_version() + dirs = ["dist", "build", "%s-%s" % (_project_name_, ver)] + for d in dirs: + print "removing '%s'" % d + if os.path.isdir(d): _rmtree(d) + + patterns = ["*.pyc", "*~", "MANIFEST", + 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_launcher(): + """Build the Windows launcher executable.""" + log.info("target: launcher") + assert sys.platform == "win32", "'launcher' target only supported on Windows" + _run("nmake -f Makefile.win") + + +def target_docs(): + """Regenerate some doc bits from project-info.xml.""" + log.info("target: docs") + _run("projinfo -f project-info.xml -R -o README.txt --force") + _run("projinfo -f project-info.xml --index-markdown -o index.markdown --force") + + +def target_sdist(): + """Build a source distribution.""" + log.info("target: sdist") + target_docs() + bitsDir = _get_project_bits_dir() + _run("python setup.py sdist -f --formats zip -d %s" % bitsDir, + log.info) + + +def target_webdist(): + """Build a web dist package. + + "Web dist" packages are zip files with '.web' package. All files in + the zip must be under a dir named after the project. There must be a + webinfo.xml file at <projname>/webinfo.xml. This file is "defined" + by the parsing in trentm.com/build.py. + """ + assert sys.platform != "win32", "'webdist' not implemented for win32" + log.info("target: webdist") + bitsDir = _get_project_bits_dir() + buildDir = join("build", "webdist") + distDir = join(buildDir, _project_name_) + if exists(buildDir): + _rmtree(buildDir) + os.makedirs(distDir) + + target_docs() + + # Copy the webdist bits to the build tree. + manifest = [ + "project-info.xml", + "index.markdown", + "LICENSE.txt", + "which.py", + "logo.jpg", + ] + for src in manifest: + if dirname(src): + dst = join(distDir, dirname(src)) + os.makedirs(dst) + else: + dst = distDir + _run("cp %s %s" % (src, dst)) + + # Zip up the webdist contents. + ver = _get_project_version() + bit = abspath(join(bitsDir, "%s-%s.web" % (_project_name_, ver))) + if exists(bit): + os.remove(bit) + _run_in_dir("zip -r %s %s" % (bit, _project_name_), buildDir, log.info) + + +def target_install(): + """Use the setup.py script to install.""" + log.info("target: install") + _run("python setup.py install") + + +def target_upload_local(): + """Update release bits to *local* trentm.com bits-dir location. + + This is different from the "upload" target, which uploads release + bits remotely to trentm.com. + """ + log.info("target: upload_local") + assert sys.platform != "win32", "'upload_local' not implemented for win32" + + ver = _get_project_version() + localBitsDir = _get_local_bits_dir() + uploadDir = join(localBitsDir, _project_name_, ver) + + bitsPattern = join(_get_project_bits_dir(), + "%s-*%s*" % (_project_name_, ver)) + bits = glob.glob(bitsPattern) + if not bits: + log.info("no bits matching '%s' to upload", bitsPattern) + else: + if not exists(uploadDir): + os.makedirs(uploadDir) + for bit in bits: + _run("cp %s %s" % (bit, uploadDir), log.info) + + +def target_upload(): + """Upload binary and source distribution to trentm.com bits + directory. + """ + log.info("target: upload") + + ver = _get_project_version() + bitsDir = _get_project_bits_dir() + bitsPattern = join(bitsDir, "%s-*%s*" % (_project_name_, ver)) + bits = glob.glob(bitsPattern) + if not bits: + log.info("no bits matching '%s' to upload", bitsPattern) + return + + # Ensure have all the expected bits. + expectedBits = [ + re.compile("%s-.*\.zip$" % _project_name_), + re.compile("%s-.*\.web$" % _project_name_) + ] + for expectedBit in expectedBits: + for bit in bits: + if expectedBit.search(bit): + break + else: + raise Error("can't find expected bit matching '%s' in '%s' dir" + % (expectedBit.pattern, bitsDir)) + + # Upload the bits. + user = "trentm" + host = "trentm.com" + remoteBitsBaseDir = "~/data/bits" + remoteBitsDir = join(remoteBitsBaseDir, _project_name_, ver) + if sys.platform == "win32": + ssh = "plink" + scp = "pscp -unsafe" + else: + ssh = "ssh" + scp = "scp" + _run("%s %s@%s 'mkdir -p %s'" % (ssh, user, host, remoteBitsDir), log.info) + for bit in bits: + _run("%s %s %s@%s:%s" % (scp, bit, user, host, remoteBitsDir), + log.info) + + +def target_check_version(): + """grep for version strings in source code + + List all things that look like version strings in the source code. + Used for checking that versioning is updated across the board. + """ + sources = [ + "which.py", + "project-info.xml", + ] + pattern = r'[0-9]\+\(\.\|, \)[0-9]\+\(\.\|, \)[0-9]\+' + _run('grep -n "%s" %s' % (pattern, ' '.join(sources)), None) + + + +#---- mainline + +def build(targets=[]): + log.debug("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 Error("Error running '%s' target: retval=%s"\ + % (target, retval)) + else: + raise Error("Unknown target: '%s'" % target) + +def main(argv): + _setup_logging() + + # Process options. + optlist, targets = getopt.getopt(argv[1:], 'ht', ['help', 'targets']) + for opt, optarg in optlist: + if opt in ('-h', '--help'): + sys.stdout.write(__doc__ + '\n') + return 0 + elif opt in ('-t', '--targets'): + return _listTargets(_getTargets()) + + return build(targets) + +if __name__ == "__main__": + sys.exit( main(sys.argv) ) + |