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

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

mercurial