python/which/build.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 #!/usr/bin/env python
     2 # Copyright (c) 2002-2005 ActiveState
     3 # See LICENSE.txt for license details.
     5 """
     6     which.py dev build script
     8     Usage:
     9         python build.py [<options>...] [<targets>...]
    11     Options:
    12         --help, -h      Print this help and exit.
    13         --targets, -t   List all available targets.
    15     This is the primary build script for the which.py project. It exists
    16     to assist in building, maintaining, and distributing this project.
    18     It is intended to have Makefile semantics. I.e. 'python build.py'
    19     will build execute the default target, 'python build.py foo' will
    20     build target foo, etc. However, there is no intelligent target
    21     interdependency tracking (I suppose I could do that with function
    22     attributes).
    23 """
    25 import os
    26 from os.path import basename, dirname, splitext, isfile, isdir, exists, \
    27                     join, abspath, normpath
    28 import sys
    29 import getopt
    30 import types
    31 import getpass
    32 import shutil
    33 import glob
    34 import logging
    35 import re
    39 #---- exceptions
    41 class Error(Exception):
    42     pass
    46 #---- globals
    48 log = logging.getLogger("build")
    53 #---- globals
    55 _project_name_ = "which"
    59 #---- internal support routines
    61 def _get_trentm_com_dir():
    62     """Return the path to the local trentm.com source tree."""
    63     d = normpath(join(dirname(__file__), os.pardir, "trentm.com"))
    64     if not isdir(d):
    65         raise Error("could not find 'trentm.com' src dir at '%s'" % d)
    66     return d
    68 def _get_local_bits_dir():
    69     import imp
    70     info = imp.find_module("tmconfig", [_get_trentm_com_dir()])
    71     tmconfig = imp.load_module("tmconfig", *info)
    72     return tmconfig.bitsDir
    74 def _get_project_bits_dir():
    75     d = normpath(join(dirname(__file__), "bits"))
    76     return d
    78 def _get_project_version():
    79     import imp, os
    80     data = imp.find_module(_project_name_, [os.path.dirname(__file__)])
    81     mod = imp.load_module(_project_name_, *data)
    82     return mod.__version__
    85 # Recipe: run (0.5.1) in /Users/trentm/tm/recipes/cookbook
    86 _RUN_DEFAULT_LOGSTREAM = ("RUN", "DEFAULT", "LOGSTREAM")
    87 def __run_log(logstream, msg, *args, **kwargs):
    88     if not logstream:
    89         pass
    90     elif logstream is _RUN_DEFAULT_LOGSTREAM:
    91         try:
    92             log.debug(msg, *args, **kwargs)
    93         except NameError:
    94             pass
    95     else:
    96         logstream(msg, *args, **kwargs)
    98 def _run(cmd, logstream=_RUN_DEFAULT_LOGSTREAM):
    99     """Run the given command.
   101         "cmd" is the command to run
   102         "logstream" is an optional logging stream on which to log the command.
   103             If None, no logging is done. If unspecifed, this looks for a Logger
   104             instance named 'log' and logs the command on log.debug().
   106     Raises OSError is the command returns a non-zero exit status.
   107     """
   108     __run_log(logstream, "running '%s'", cmd)
   109     retval = os.system(cmd)
   110     if hasattr(os, "WEXITSTATUS"):
   111         status = os.WEXITSTATUS(retval)
   112     else:
   113         status = retval
   114     if status:
   115         #TODO: add std OSError attributes or pick more approp. exception
   116         raise OSError("error running '%s': %r" % (cmd, status))
   118 def _run_in_dir(cmd, cwd, logstream=_RUN_DEFAULT_LOGSTREAM):
   119     old_dir = os.getcwd()
   120     try:
   121         os.chdir(cwd)
   122         __run_log(logstream, "running '%s' in '%s'", cmd, cwd)
   123         _run(cmd, logstream=None)
   124     finally:
   125         os.chdir(old_dir)
   128 # Recipe: rmtree (0.5) in /Users/trentm/tm/recipes/cookbook
   129 def _rmtree_OnError(rmFunction, filePath, excInfo):
   130     if excInfo[0] == OSError:
   131         # presuming because file is read-only
   132         os.chmod(filePath, 0777)
   133         rmFunction(filePath)
   134 def _rmtree(dirname):
   135     import shutil
   136     shutil.rmtree(dirname, 0, _rmtree_OnError)
   139 # Recipe: pretty_logging (0.1) in /Users/trentm/tm/recipes/cookbook
   140 class _PerLevelFormatter(logging.Formatter):
   141     """Allow multiple format string -- depending on the log level.
   143     A "fmtFromLevel" optional arg is added to the constructor. It can be
   144     a dictionary mapping a log record level to a format string. The
   145     usual "fmt" argument acts as the default.
   146     """
   147     def __init__(self, fmt=None, datefmt=None, fmtFromLevel=None):
   148         logging.Formatter.__init__(self, fmt, datefmt)
   149         if fmtFromLevel is None:
   150             self.fmtFromLevel = {}
   151         else:
   152             self.fmtFromLevel = fmtFromLevel
   153     def format(self, record):
   154         record.levelname = record.levelname.lower()
   155         if record.levelno in self.fmtFromLevel:
   156             #XXX This is a non-threadsafe HACK. Really the base Formatter
   157             #    class should provide a hook accessor for the _fmt
   158             #    attribute. *Could* add a lock guard here (overkill?).
   159             _saved_fmt = self._fmt
   160             self._fmt = self.fmtFromLevel[record.levelno]
   161             try:
   162                 return logging.Formatter.format(self, record)
   163             finally:
   164                 self._fmt = _saved_fmt
   165         else:
   166             return logging.Formatter.format(self, record)
   168 def _setup_logging():
   169     hdlr = logging.StreamHandler()
   170     defaultFmt = "%(name)s: %(levelname)s: %(message)s"
   171     infoFmt = "%(name)s: %(message)s"
   172     fmtr = _PerLevelFormatter(fmt=defaultFmt,
   173                               fmtFromLevel={logging.INFO: infoFmt})
   174     hdlr.setFormatter(fmtr)
   175     logging.root.addHandler(hdlr)
   176     log.setLevel(logging.INFO)
   179 def _getTargets():
   180     """Find all targets and return a dict of targetName:targetFunc items."""
   181     targets = {}
   182     for name, attr in sys.modules[__name__].__dict__.items():
   183         if name.startswith('target_'):
   184             targets[ name[len('target_'):] ] = attr
   185     return targets
   187 def _listTargets(targets):
   188     """Pretty print a list of targets."""
   189     width = 77
   190     nameWidth = 15 # min width
   191     for name in targets.keys():
   192         nameWidth = max(nameWidth, len(name))
   193     nameWidth += 2  # space btwn name and doc
   194     format = "%%-%ds%%s" % nameWidth
   195     print format % ("TARGET", "DESCRIPTION")
   196     for name, func in sorted(targets.items()):
   197         doc = _first_paragraph(func.__doc__ or "", True)
   198         if len(doc) > (width - nameWidth):
   199             doc = doc[:(width-nameWidth-3)] + "..."
   200         print format % (name, doc)
   203 # Recipe: first_paragraph (1.0.1) in /Users/trentm/tm/recipes/cookbook
   204 def _first_paragraph(text, join_lines=False):
   205     """Return the first paragraph of the given text."""
   206     para = text.lstrip().split('\n\n', 1)[0]
   207     if join_lines:
   208         lines = [line.strip() for line in  para.splitlines(0)]
   209         para = ' '.join(lines)
   210     return para
   214 #---- build targets
   216 def target_default():
   217     target_all()
   219 def target_all():
   220     """Build all release packages."""
   221     log.info("target: default")
   222     if sys.platform == "win32":
   223         target_launcher()
   224     target_sdist()
   225     target_webdist()
   228 def target_clean():
   229     """remove all build/generated bits"""
   230     log.info("target: clean")
   231     if sys.platform == "win32":
   232         _run("nmake -f Makefile.win clean")
   234     ver = _get_project_version()
   235     dirs = ["dist", "build", "%s-%s" % (_project_name_, ver)]
   236     for d in dirs:
   237         print "removing '%s'" % d
   238         if os.path.isdir(d): _rmtree(d)
   240     patterns = ["*.pyc", "*~", "MANIFEST",
   241                 os.path.join("test", "*~"),
   242                 os.path.join("test", "*.pyc"),
   243                ]
   244     for pattern in patterns:
   245         for file in glob.glob(pattern):
   246             print "removing '%s'" % file
   247             os.unlink(file)
   250 def target_launcher():
   251     """Build the Windows launcher executable."""
   252     log.info("target: launcher")
   253     assert sys.platform == "win32", "'launcher' target only supported on Windows"
   254     _run("nmake -f Makefile.win")
   257 def target_docs():
   258     """Regenerate some doc bits from project-info.xml."""
   259     log.info("target: docs")
   260     _run("projinfo -f project-info.xml -R -o README.txt --force")
   261     _run("projinfo -f project-info.xml --index-markdown -o index.markdown --force")
   264 def target_sdist():
   265     """Build a source distribution."""
   266     log.info("target: sdist")
   267     target_docs()
   268     bitsDir = _get_project_bits_dir()
   269     _run("python setup.py sdist -f --formats zip -d %s" % bitsDir,
   270          log.info)
   273 def target_webdist():
   274     """Build a web dist package.
   276     "Web dist" packages are zip files with '.web' package. All files in
   277     the zip must be under a dir named after the project. There must be a
   278     webinfo.xml file at <projname>/webinfo.xml. This file is "defined"
   279     by the parsing in trentm.com/build.py.
   280     """ 
   281     assert sys.platform != "win32", "'webdist' not implemented for win32"
   282     log.info("target: webdist")
   283     bitsDir = _get_project_bits_dir()
   284     buildDir = join("build", "webdist")
   285     distDir = join(buildDir, _project_name_)
   286     if exists(buildDir):
   287         _rmtree(buildDir)
   288     os.makedirs(distDir)
   290     target_docs()
   292     # Copy the webdist bits to the build tree.
   293     manifest = [
   294         "project-info.xml",
   295         "index.markdown",
   296         "LICENSE.txt",
   297         "which.py",
   298         "logo.jpg",
   299     ]
   300     for src in manifest:
   301         if dirname(src):
   302             dst = join(distDir, dirname(src))
   303             os.makedirs(dst)
   304         else:
   305             dst = distDir
   306         _run("cp %s %s" % (src, dst))
   308     # Zip up the webdist contents.
   309     ver = _get_project_version()
   310     bit = abspath(join(bitsDir, "%s-%s.web" % (_project_name_, ver)))
   311     if exists(bit):
   312         os.remove(bit)
   313     _run_in_dir("zip -r %s %s" % (bit, _project_name_), buildDir, log.info)
   316 def target_install():
   317     """Use the setup.py script to install."""
   318     log.info("target: install")
   319     _run("python setup.py install")
   322 def target_upload_local():
   323     """Update release bits to *local* trentm.com bits-dir location.
   325     This is different from the "upload" target, which uploads release
   326     bits remotely to trentm.com.
   327     """
   328     log.info("target: upload_local")
   329     assert sys.platform != "win32", "'upload_local' not implemented for win32"
   331     ver = _get_project_version()
   332     localBitsDir = _get_local_bits_dir()
   333     uploadDir = join(localBitsDir, _project_name_, ver)
   335     bitsPattern = join(_get_project_bits_dir(),
   336                        "%s-*%s*" % (_project_name_, ver))
   337     bits = glob.glob(bitsPattern)
   338     if not bits:
   339         log.info("no bits matching '%s' to upload", bitsPattern)
   340     else:
   341         if not exists(uploadDir):
   342             os.makedirs(uploadDir)
   343         for bit in bits:
   344             _run("cp %s %s" % (bit, uploadDir), log.info)
   347 def target_upload():
   348     """Upload binary and source distribution to trentm.com bits
   349     directory.
   350     """
   351     log.info("target: upload")
   353     ver = _get_project_version()
   354     bitsDir = _get_project_bits_dir()
   355     bitsPattern = join(bitsDir, "%s-*%s*" % (_project_name_, ver))
   356     bits = glob.glob(bitsPattern)
   357     if not bits:
   358         log.info("no bits matching '%s' to upload", bitsPattern)
   359         return
   361     # Ensure have all the expected bits.
   362     expectedBits = [
   363         re.compile("%s-.*\.zip$" % _project_name_),
   364         re.compile("%s-.*\.web$" % _project_name_)
   365     ]
   366     for expectedBit in expectedBits:
   367         for bit in bits:
   368             if expectedBit.search(bit):
   369                 break
   370         else:
   371             raise Error("can't find expected bit matching '%s' in '%s' dir"
   372                         % (expectedBit.pattern, bitsDir))
   374     # Upload the bits.
   375     user = "trentm"
   376     host = "trentm.com"
   377     remoteBitsBaseDir = "~/data/bits"
   378     remoteBitsDir = join(remoteBitsBaseDir, _project_name_, ver)
   379     if sys.platform == "win32":
   380         ssh = "plink"
   381         scp = "pscp -unsafe"
   382     else:
   383         ssh = "ssh"
   384         scp = "scp"
   385     _run("%s %s@%s 'mkdir -p %s'" % (ssh, user, host, remoteBitsDir), log.info)
   386     for bit in bits:
   387         _run("%s %s %s@%s:%s" % (scp, bit, user, host, remoteBitsDir),
   388              log.info)
   391 def target_check_version():
   392     """grep for version strings in source code
   394     List all things that look like version strings in the source code.
   395     Used for checking that versioning is updated across the board.  
   396     """
   397     sources = [
   398         "which.py",
   399         "project-info.xml",
   400     ]
   401     pattern = r'[0-9]\+\(\.\|, \)[0-9]\+\(\.\|, \)[0-9]\+'
   402     _run('grep -n "%s" %s' % (pattern, ' '.join(sources)), None)
   406 #---- mainline
   408 def build(targets=[]):
   409     log.debug("build(targets=%r)" % targets)
   410     available = _getTargets()
   411     if not targets:
   412         if available.has_key('default'):
   413             return available['default']()
   414         else:   
   415             log.warn("No default target available. Doing nothing.")
   416     else:
   417         for target in targets:
   418             if available.has_key(target):
   419                 retval = available[target]()
   420                 if retval:
   421                     raise Error("Error running '%s' target: retval=%s"\
   422                                 % (target, retval))
   423             else:
   424                 raise Error("Unknown target: '%s'" % target)
   426 def main(argv):
   427     _setup_logging()
   429     # Process options.
   430     optlist, targets = getopt.getopt(argv[1:], 'ht', ['help', 'targets'])
   431     for opt, optarg in optlist:
   432         if opt in ('-h', '--help'):
   433             sys.stdout.write(__doc__ + '\n')
   434             return 0
   435         elif opt in ('-t', '--targets'):
   436             return _listTargets(_getTargets())
   438     return build(targets)
   440 if __name__ == "__main__":
   441     sys.exit( main(sys.argv) )

mercurial