Bumping manifests a=b2g-bump
[gecko.git] / python / which / build.py
blob3c8f09d39e1afe2c6fec9558ad8e96024686461c
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.
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.
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"),
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",
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.
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.
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_)
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.
397 sources = [
398 "which.py",
399 "project-info.xml",
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) )