Skip to content
This repository
Browse code

merged

  • Loading branch information...
commit e21ee0f495a00520ce7b845f21b8c6576a2be815 2 parents ea00a18 + d0ef1bc
C. Titus Brown authored

Showing 41 changed files with 863 additions and 480 deletions. Show diff stats Hide diff stats

  1. +21 6 README.html
  2. +24 6 README.txt
  3. +48 0 client/build-nose.py
  4. +46 0 client/build-quixote
  5. +321 303 client/pony_client.py
  6. +4 0 client/test_client/__init__.py
  7. +104 10 client/test_client/test_git_client.py
  8. +9 1 client/test_client/test_hg_client.py
  9. +163 0 client/test_client/test_svn_client.py
  10. +1 0  examples/change-receiver/github-notify.json
  11. +49 0 examples/change-receiver/github-receiver.cgi
  12. +14 0 examples/change-receiver/test-post-github-notify.py
  13. +4 1 pony_build/coordinator.py
  14. +0 35 pony_build/qx_web/util.py
  15. +0 42 pony_build/server.py
  16. +0 69 pony_build/tests/test_webhook_notify.py
  17. +4 4 pony_build/tests/testutil.py
  18. 0  pony_build/{qx_web → web}/__init__.py
  19. +1 1  pony_build/{qx_web → web}/run.py
  20. 0  pony_build/{qx_web → web}/templates/base.html
  21. 0  pony_build/{qx_web → web}/templates/empty.html
  22. 0  pony_build/{qx_web → web}/templates/feed_base.html
  23. 0  pony_build/{qx_web → web}/templates/feed_generic_index.html
  24. 0  pony_build/{qx_web → web}/templates/feed_generic_package_index.html
  25. 0  pony_build/{qx_web → web}/templates/feed_index.html
  26. 0  pony_build/{qx_web → web}/templates/forms.html
  27. 0  pony_build/{qx_web → web}/templates/img/background.png
  28. 0  pony_build/{qx_web → web}/templates/img/dark_line.png
  29. 0  pony_build/{qx_web → web}/templates/img/light_line.png
  30. 0  pony_build/{qx_web → web}/templates/package_all.html
  31. 0  pony_build/{qx_web → web}/templates/package_base.html
  32. 0  pony_build/{qx_web → web}/templates/package_summary.html
  33. 0  pony_build/{qx_web → web}/templates/results_base.html
  34. 0  pony_build/{qx_web → web}/templates/results_files_index.html
  35. 0  pony_build/{qx_web → web}/templates/results_index.html
  36. 0  pony_build/{qx_web → web}/templates/results_inspect.html
  37. 0  pony_build/{qx_web → web}/templates/style.css
  38. 0  pony_build/{qx_web → web}/templates/top_index.html
  39. 0  pony_build/{qx_web → web}/urls.py
  40. +48 0 pony_build/web/util.py
  41. +2 2 setup.py
27 README.html
@@ -327,13 +327,13 @@ <h1 class="title">pony-build</h1>
327 327 <h1><a id="pony-build-server" name="pony-build-server">pony-build server</a></h1>
328 328 <p>The command:</p>
329 329 <pre class="literal-block">
330   -python -m pony_build.qx_web.run &lt;shelve filename&gt; &lt;port&gt;
  330 +python -m pony_build.web.run &lt;shelve filename&gt; -p &lt;port&gt;
331 331 </pre>
332 332 <p>will run the Quixote-based pony-build Web app on the given port,
333   -reading &amp; writing from the shelve database in 'filename'.</p>
  333 +reading &amp; writing from the sqlite database in 'filename'.</p>
334 334 <p>For example,</p>
335 335 <pre class="literal-block">
336   -python -m pony_build.qx_web.run test.db 8080
  336 +python -m pony_build.web.run test.db -p 8080
337 337 </pre>
338 338 <p>will run a server that can be accessed on <a class="reference" href="http://localhost:8080/">http://localhost:8080/</a>. This
339 339 server will report on whatever results are sent to it by the client (see
@@ -418,7 +418,7 @@ <h1 class="title">pony-build</h1>
418 418 someone else deal with translating those into e-mail alerts, etc.</p>
419 419 <p>All of the RSS feeds that pony-build makes available can be posted to
420 420 pubsubhubbub with the proper configuration (see -P and -S options to
421   -<tt class="docutils literal"><span class="pre">pony_build.qx_web.run</span></tt>). A simple example CGI callback script that
  421 +<tt class="docutils literal"><span class="pre">pony_build.web.run</span></tt>). A simple example CGI callback script that
422 422 sends an e-mail is available in
423 423 <tt class="docutils literal"><span class="pre">examples/push-cgi/notifier/push-subscriber.cgi</span></tt> in the pony-build
424 424 source distribution.</p>
@@ -450,10 +450,15 @@ <h1 class="title">pony-build</h1>
450 450 <div class="section">
451 451 <h1><a id="development" name="development">Development</a></h1>
452 452 <p>pony-build is hosted on github, at: <a class="reference" href="http://github.com/ctb/pony-build">http://github.com/ctb/pony-build</a></p>
453   -<p>To run the tests:</p>
  453 +<p>To run the server tests:</p>
454 454 <pre class="literal-block">
455 455 python -m pony_build.tests.run
456 456 </pre>
  457 +<p>To run the client tests:</p>
  458 +<pre class="literal-block">
  459 +cd client
  460 +nosetests
  461 +</pre>
457 462 </div>
458 463 <div class="section">
459 464 <h1><a id="design-and-ideas-for-the-future" name="design-and-ideas-for-the-future">Design and Ideas for the Future</a></h1>
@@ -520,6 +525,12 @@ <h1 class="title">pony-build</h1>
520 525 </blockquote>
521 526 </div>
522 527 <div class="section">
  528 +<h2><a id="contributors" name="contributors">Contributors</a></h2>
  529 +<p>Jacob Kaplan-Moss, Max Laite, Jack Carlson, Fatima Cherkaoui, and Khushboo
  530 +Shakya have all contributed code and ideas.</p>
  531 +<p>(If I'm missing anyone, please drop me a note!)</p>
  532 +</div>
  533 +<div class="section">
523 534 <h2><a id="acks" name="acks">Acks</a></h2>
524 535 <p>Titus says: Jesse Noller, Doug Philips, and Josh Williams discussed
525 536 things with me and are, collectively, entirely responsible for any bad
@@ -533,12 +544,16 @@ <h1 class="title">pony-build</h1>
533 544 <a class="reference" href="http://lists.idyll.org/pipermail/testing-in-python/2009-March/001277.html">http://lists.idyll.org/pipermail/testing-in-python/2009-March/001277.html</a></blockquote>
534 545 <p>where Kumar suggests that I just use Hudson for chrissakes. He's
535 546 probably right.</p>
  547 +<p>Eric Holscher and Jacob Kaplan-Moss took the pony-build idea and ran
  548 +with it, producing a parallel universe of Django-based reporting
  549 +servers and REST-ish clients that report via JSON. Check out
  550 +devmason.com and 'pony_barn' to see their approach in action.</p>
536 551 </div>
537 552 <div class="section">
538 553 <h2><a id="references" name="references">References</a></h2>
539 554 <p>webhooks: <a class="reference" href="http://webhooks.pbworks.com/">http://webhooks.pbworks.com/</a></p>
540 555 <p>--</p>
541   -<p>CTB 8/24/09</p>
  556 +<p>CTB 2/24/10</p>
542 557 </div>
543 558 </div>
544 559 </div>
30 README.txt
@@ -48,14 +48,14 @@ pony-build server
48 48
49 49 The command: ::
50 50
51   - python -m pony_build.qx_web.run <shelve filename> <port>
  51 + python -m pony_build.web.run <shelve filename> -p <port>
52 52
53 53 will run the Quixote-based pony-build Web app on the given port,
54   -reading & writing from the shelve database in 'filename'.
  54 +reading & writing from the sqlite database in 'filename'.
55 55
56 56 For example, ::
57 57
58   - python -m pony_build.qx_web.run test.db 8080
  58 + python -m pony_build.web.run test.db -p 8080
59 59
60 60 will run a server that can be accessed on http://localhost:8080/. This
61 61 server will report on whatever results are sent to it by the client (see
@@ -155,7 +155,7 @@ someone else deal with translating those into e-mail alerts, etc.
155 155
156 156 All of the RSS feeds that pony-build makes available can be posted to
157 157 pubsubhubbub with the proper configuration (see -P and -S options to
158   -``pony_build.qx_web.run``). A simple example CGI callback script that
  158 +``pony_build.web.run``). A simple example CGI callback script that
159 159 sends an e-mail is available in
160 160 ``examples/push-cgi/notifier/push-subscriber.cgi`` in the pony-build
161 161 source distribution.
@@ -195,10 +195,15 @@ Development
195 195
196 196 pony-build is hosted on github, at: http://github.com/ctb/pony-build
197 197
198   -To run the tests::
  198 +To run the server tests::
199 199
200 200 python -m pony_build.tests.run
201 201
  202 +To run the client tests::
  203 +
  204 + cd client
  205 + nosetests
  206 +
202 207 Design and Ideas for the Future
203 208 ===============================
204 209
@@ -270,6 +275,14 @@ soon let buildbot pick up the higher-end ideas if they're game, too.
270 275 send "final results, authenticate with update token"
271 276 receive "ack"
272 277
  278 +Contributors
  279 +------------
  280 +
  281 +Jacob Kaplan-Moss, Max Laite, Jack Carlson, Fatima Cherkaoui, and Khushboo
  282 +Shakya have all contributed code and ideas.
  283 +
  284 +(If I'm missing anyone, please drop me a note!)
  285 +
273 286 Acks
274 287 ----
275 288
@@ -290,6 +303,11 @@ You can also read this discussion starting here,
290 303 where Kumar suggests that I just use Hudson for chrissakes. He's
291 304 probably right.
292 305
  306 +Eric Holscher and Jacob Kaplan-Moss took the pony-build idea and ran
  307 +with it, producing a parallel universe of Django-based reporting
  308 +servers and REST-ish clients that report via JSON. Check out
  309 +devmason.com and 'pony_barn' to see their approach in action.
  310 +
293 311 References
294 312 ----------
295 313
@@ -297,4 +315,4 @@ webhooks: http://webhooks.pbworks.com/
297 315
298 316 --
299 317
300   -CTB 8/24/09
  318 +CTB 2/24/10
48 client/build-nose.py
... ... @@ -0,0 +1,48 @@
  1 +#! /usr/bin/env python
  2 +import sys
  3 +import pprint
  4 +from pony_client import BuildCommand, TestCommand, do, send, \
  5 + TempDirectoryContext, SetupCommand, HgClone, check, parse_cmdline, \
  6 + PythonPackageEgg
  7 +
  8 +options, args = parse_cmdline()
  9 +
  10 +python_exe = 'python'
  11 +if args:
  12 + python_exe = args[0]
  13 +
  14 +repo_url = 'http://bitbucket.org/jpellerin/nose/'
  15 +
  16 +tags = ['nose']
  17 +name = 'build-nose'
  18 +
  19 +server_url = options.server_url
  20 +
  21 +if not options.force_build:
  22 + if not check(name, server_url, tags=tags):
  23 + print 'check build says no need to build; bye'
  24 + sys.exit(0)
  25 +
  26 +context = TempDirectoryContext()
  27 +commands = [ HgClone(repo_url, name='checkout'),
  28 + BuildCommand([python_exe, 'setup.py', 'build_ext', '-i'],
  29 + name='compile'),
  30 + TestCommand([python_exe, 'setup.py', 'test'], name='run tests'),
  31 + PythonPackageEgg(python_exe)]
  32 +
  33 +results = do(name, commands, context=context)
  34 +client_info, reslist, files = results
  35 +
  36 +if options.report:
  37 + print 'result: %s; sending' % (client_info['success'],)
  38 + send(server_url, results, tags=tags)
  39 +else:
  40 + print 'build result:'
  41 + pprint.pprint(client_info)
  42 + pprint.pprint(reslist)
  43 +
  44 + print '(NOT SENDING BUILD RESULT TO SERVER)'
  45 +
  46 +if not client_info['success']:
  47 + sys.exit(-1)
  48 +
46 client/build-quixote
... ... @@ -0,0 +1,46 @@
  1 +#! /usr/bin/env python
  2 +import sys
  3 +import pprint
  4 +from pony_client import BuildCommand, TestCommand, do, send, \
  5 + TempDirectoryContext, SetupCommand, GitClone, check, parse_cmdline, \
  6 + PythonPackageEgg, test_python_version
  7 +
  8 +options, _ = parse_cmdline()
  9 +
  10 +python_exe = options.python_executable
  11 +
  12 +if not test_python_version(python_exe):
  13 + print "Unable to find " + python_exe + ". Failing..."
  14 + sys.exit(1)
  15 +
  16 +repo_url = 'git://quixote.ca/quixote'
  17 +
  18 +tags = [python_exe] + options.tagset
  19 +name = 'quixote'
  20 +
  21 +server_url = options.server_url
  22 +
  23 +if not options.force_build:
  24 + if not check(name, server_url, tags=tags):
  25 + print 'check build says no need to build; bye'
  26 + sys.exit(0)
  27 +
  28 +context = TempDirectoryContext()
  29 +commands = [ GitClone(repo_url, name='checkout'),
  30 + BuildCommand([python_exe, 'setup.py', 'install'], name='compile')]
  31 +
  32 +results = do(name, commands, context=context)
  33 +client_info, reslist, files = results
  34 +
  35 +if options.report:
  36 + print 'result: %s; sending' % (client_info['success'],)
  37 + send(server_url, results, tags=tags)
  38 +else:
  39 + print 'build result:'
  40 + pprint.pprint(client_info)
  41 + pprint.pprint(reslist)
  42 +
  43 + print '(NOT SENDING BUILD RESULT TO SERVER)'
  44 +
  45 +if not client_info['success']:
  46 + sys.exit(-1)
624 client/pony_client.py
@@ -26,7 +26,33 @@
26 26
27 27 ###
28 28
  29 +DEBUG_LEVEL = 5
  30 +INFO_LEVEL = 3
  31 +WARNING_LEVEL = 2
  32 +CRITICAL_LEVEL = 1
  33 +_log_level = WARNING_LEVEL
  34 +
  35 +def log_debug(*args):
  36 + log(DEBUG_LEVEL, *args)
  37 +
  38 +def log_info(*args):
  39 + log(INFO_LEVEL, *args)
  40 +
  41 +def log_warning(*args):
  42 + log(WARNING_LEVEL, *args)
  43 +
  44 +def log_critical(*args):
  45 + log(CRITICAL_LEVEL, *args)
29 46
  47 +def log(level, *what):
  48 + if level <= _log_level:
  49 + sys.stdout.write(" ".join([ str(x) for x in what]) + "\n")
  50 +
  51 +def set_log_level(level):
  52 + global _log_level
  53 + _log_level = level
  54 +
  55 +###
30 56
31 57 DEFAULT_CACHE_DIR='~/.pony-build'
32 58 def guess_cache_dir(dirname):
@@ -35,7 +61,7 @@ def guess_cache_dir(dirname):
35 61 parent = os.path.expanduser(parent)
36 62 result = os.path.join(parent, dirname)
37 63
38   - return result
  64 + return (parent, result)
39 65
40 66 def create_cache_dir(cache_dir, dirname):
41 67 # trim the pkg name so we can create the main cache_dir and not the
@@ -49,13 +75,14 @@ def create_cache_dir(cache_dir, dirname):
49 75 cache_dir = cache_dir[:-pkglen]
50 76
51 77 if os.path.isdir(cache_dir):
52   - print 'cache_dir %s exists already!' % cache_dir
  78 + log_info('VCS cache_dir %s exists already!' % cache_dir)
53 79 else:
54 80 try:
55   - print 'Had to create a new cache_dir!'
  81 + log_info('created new VCS cache dir: %s' % cache_dir)
56 82 os.mkdir(cache_dir)
57 83 except OSError:
58   - raise Exception('Unable to create VCS cache_dir: %s' % cache_dir)
  84 + log_critical('Unable to create VCS cache_dir: %s' % cache_dir)
  85 + raise
59 86
60 87 ###
61 88
@@ -64,8 +91,7 @@ def _replace_variables(cmd, variables_d):
64 91 cmd = variables_d[cmd[3:]]
65 92 return cmd
66 93
67   -def _run_command(command_list, cwd=None, variables=None, extra_kwargs={},
68   - verbose=False):
  94 +def _run_command(command_list, cwd=None, variables=None, extra_kwargs={}):
69 95 if variables:
70 96 x = []
71 97 for cmd in command_list:
@@ -78,11 +104,10 @@ def _run_command(command_list, cwd=None, variables=None, extra_kwargs={},
78 104 if extra_kwargs:
79 105 default_kwargs.update(extra_kwargs)
80 106
81   - if verbose:
82   - print 'CWD', os.getcwd()
83   - print 'running in ->', cwd
84   - print 'command_list:', command_list
85   - print 'default kwargs:', default_kwargs
  107 + log_debug('_run_command cwd', os.getcwd())
  108 + log_debug('_run_command running in ->', cwd)
  109 + log_debug('_run_command command list:', command_list)
  110 + log_debug('_run_command default kwargs:', default_kwargs)
86 111
87 112 try:
88 113 p = subprocess.Popen(command_list, cwd=cwd, **default_kwargs)
@@ -94,18 +119,20 @@ def _run_command(command_list, cwd=None, variables=None, extra_kwargs={},
94 119 err = traceback.format_exc()
95 120 ret = -1
96 121
97   - if verbose:
98   - print 'status:', ret
  122 + log_debug('_run_command status', str(ret))
  123 + log_debug('_run_command stdout', out)
  124 + log_debug('_run_command stderr', err)
99 125
100 126 return (ret, out, err)
101 127
102 128 class FileToUpload(object):
103 129 def __init__(self, filename, location, description, visible):
104 130 """
105   -filename - name to publish as
106   -location - full location on build system (not sent to server)
107   -description - brief description of file/arch for server
108   -"""
  131 + filename - name to publish as
  132 + location - full location on build system (not sent to server)
  133 + description - brief description of file/arch for server
  134 + """
  135 +
109 136 self.data = open(location, 'rb').read()
110 137 self.filename = filename
111 138 self.description = description
@@ -152,7 +179,7 @@ def initialize(self):
152 179 self.tempdir = tempfile.mkdtemp()
153 180 self.cwd = os.getcwd()
154 181
155   - print 'changing to temp directory:', self.tempdir
  182 + log_info('changing to temp directory:', self.tempdir)
156 183 os.chdir(self.tempdir)
157 184
158 185 def finish(self):
@@ -161,7 +188,7 @@ def finish(self):
161 188 Context.finish(self)
162 189 finally:
163 190 if self.cleanup:
164   - print 'removing', self.tempdir
  191 + log_info('removing', self.tempdir)
165 192 shutil.rmtree(self.tempdir, ignore_errors=True)
166 193
167 194 def update_client_info(self, info):
@@ -170,15 +197,16 @@ def update_client_info(self, info):
170 197
171 198 class VirtualenvContext(Context):
172 199 """
173   -A context that creates a new virtualenv and does everything within that
174   -environment.
  200 + A context that works within a new virtualenv.
175 201
176   -VirtualenvContext works by modifying the @@CTB
177   -"""
178   - def __init__(self, always_cleanup=True, dependencies=[], python='python'):
  202 + VirtualenvContext works by modifying the path to the Python executable.
  203 + """
  204 + def __init__(self, always_cleanup=True, dependencies=[], optional=[],
  205 + python='python'):
179 206 Context.__init__(self)
180 207 self.cleanup = always_cleanup
181 208 self.dependencies = dependencies
  209 + self.optional = optional # optional dependencies
182 210 self.python = python
183 211
184 212 # Create the virtualenv. Have to do this here so that commands can use
@@ -187,7 +215,7 @@ def __init__(self, always_cleanup=True, dependencies=[], python='python'):
187 215
188 216 self.tempdir = tempfile.mkdtemp()
189 217
190   - print 'creating virtualenv'
  218 + log_inf('creating virtualenv')
191 219 cmdlist = [python, '-m', 'virtualenv', '--no-site-packages',
192 220 self.tempdir]
193 221 (ret, out, err) = _run_command(cmdlist)
@@ -206,19 +234,35 @@ def __init__(self, always_cleanup=True, dependencies=[], python='python'):
206 234
207 235 def initialize(self):
208 236 Context.initialize(self)
209   - print 'changing to temp directory:', self.tempdir
  237 + log_info('changing to temp directory:', self.tempdir)
  238 +
210 239 self.cwd = os.getcwd()
211 240 os.chdir(self.tempdir)
212 241
213 242 # install pip, then use it to install any packages desired
214   - print 'installing pip'
  243 + log_info('installing pip')
  244 +
215 245 (ret, out, err) = _run_command([self.easy_install, '-U', 'pip'])
216 246 if ret != 0:
217 247 raise Exception("error in installing pip: %s, %s" % (out, err))
218 248
219 249 for dep in self.dependencies:
220   - print "installing", dep
221   - _run_command([self.pip, 'install', '-U', '-I'] + [dep])
  250 + log_info('installing dependency:', dep)
  251 + (ret, out, err) = _run_command([self.pip, 'install', '-U', '-I',
  252 + dep])
  253 +
  254 + if ret != 0:
  255 + raise Exception("pip cannot install req dependency: %s" % dep)
  256 +
  257 + for dep in self.optional:
  258 + log_info("installing optional dependency:", dep)
  259 + (ret, out, err) = _run_command([self.pip, 'install', '-U', '-I',
  260 + dep])
  261 +
  262 + # @CTB should record failed installs of optional packages
  263 + # to client?
  264 + if ret != 0:
  265 + log_warning("pip cannot install optional dependency: %s" % dep)
222 266
223 267 def finish(self):
224 268 os.chdir(self.cwd)
@@ -226,7 +270,7 @@ def finish(self):
226 270 Context.finish(self)
227 271 finally:
228 272 if self.cleanup:
229   - print 'removing', self.tempdir
  273 + log_info("VirtualenvContext: removing", self.tempdir)
230 274 shutil.rmtree(self.tempdir, ignore_errors=True)
231 275
232 276 def update_client_info(self, info):
@@ -234,12 +278,15 @@ def update_client_info(self, info):
234 278 info['tempdir'] = self.tempdir
235 279 info['virtualenv'] = True
236 280 info['dependencies'] = self.dependencies
  281 + info['optional'] = self.optional
237 282
238 283
239 284 class UploadAFile(object):
240 285 """
241   -@CTB add glob support
242   -"""
  286 + A build command that arranges to upload a specific file to the server.
  287 +
  288 + @CTB add glob support!
  289 + """
243 290 def __init__(self, filepath, public_name, description, visible=True):
244 291 self.filepath = os.path.realpath(filepath)
245 292 self.public_name = public_name
@@ -268,8 +315,7 @@ def get_results(self):
268 315
269 316 class BaseCommand(object):
270 317 def __init__(self, command_list, name='', run_cwd=None,
271   - subprocess_kwargs=None, ignore_failure=False,
272   - verbose=False):
  318 + subprocess_kwargs=None, ignore_failure=False):
273 319 self.command_list = command_list
274 320 if name:
275 321 self.command_name = name
@@ -287,7 +333,6 @@ def __init__(self, command_list, name='', run_cwd=None,
287 333 self.subprocess_kwargs = dict(subprocess_kwargs)
288 334
289 335 self.ignore_failure = ignore_failure
290   - self.verbose = verbose
291 336
292 337 def __repr__(self):
293 338 return "%s (%s)" % (self.command_name, self.command_type)
@@ -299,8 +344,7 @@ def run(self, context):
299 344 start = time.time()
300 345 (ret, out, err) = _run_command(self.command_list, cwd=self.run_cwd,
301 346 variables=self.variables,
302   - extra_kwargs=self.subprocess_kwargs,
303   - verbose=self.verbose)
  347 + extra_kwargs=self.subprocess_kwargs)
304 348
305 349 self.status = ret
306 350 self.output = out
@@ -354,361 +398,307 @@ def run(self, context):
354 398 'an egg installation file',
355 399 visible=True)
356 400
  401 +class _VersionControlClientBase(SetupCommand):
  402 + """
  403 + Base class for version control clients.
357 404
358   -class GitClone(SetupCommand):
359   - command_name = 'checkout'
  405 + Subclasses should define:
360 406
361   - def __init__(self, repository, branch='master', cache_dir=None,
362   - use_cache=True, **kwargs):
363   - SetupCommand.__init__(self, [], **kwargs)
364   - self.repository = repository
365   - self.branch = branch
  407 + - get_dirname()
  408 + - update_repository()
  409 + - create_repository(url, dirname, step='stepname')
  410 + - record_repository_info(dirname)
366 411
  412 + and optionally override 'get_results()'.
  413 +
  414 + """
  415 +
  416 + def __init__(self, use_cache=True, **kwargs):
  417 + SetupCommand.__init__(self, [], **kwargs)
367 418 self.use_cache = use_cache
368   - self.cache_dir = cache_dir
369   - if cache_dir:
370   - self.cache_dir = os.path.expanduser(cache_dir)
371 419
372 420 self.duration = -1
373 421 self.version_info = ''
374   -
375 422 self.results_dict = {}
376 423
377 424 def run(self, context):
378   - # first, guess the co dir name
379   - p = urlparse.urlparse(self.repository)
380   - path = p[2] # urlparse -> path
381   -
382   - dirname = path.rstrip('/').split('/')[-1]
383   - if dirname.endswith('.git'):
384   - dirname = dirname[:-4]
  425 + # dirname is the directory created by a succesful checkout.
  426 + dirname = self.get_dirname()
385 427
386   - print 'git checkout dirname guessed as: %s' % (dirname,)
387   -
388   - if self.use_cache:
389   - cache_dir = self.cache_dir
390   - if not cache_dir:
391   - # setup some variables for cache folder locations/create
392   - # cache_dir if it does not exist
393   - repo_dir = guess_cache_dir(dirname)
394   - create_cache_dir(repo_dir, dirname)
395   - # trim repo so we know where the users cache should be
396   - # so we can change to for git stuff later
397   - pkglength = len(dirname)
398   - cache_dir = repo_dir[:-pkglength]
399   - ##
  428 + # cwd is the directory we're going to ultimately put dirname under.
  429 + cwd = os.getcwd()
400 430
  431 + # NOTE: we flat out don't like the situation where the
  432 + # directory already exists. Force a clean checkout.
  433 + assert not os.path.exists(dirname)
  434 +
401 435 if self.use_cache:
402   - cwd = os.getcwd()
403   - if os.path.exists(repo_dir):
  436 + # 'repo_dir' is the full cache directory containing the repo.
  437 + # this will be something like '~/.pony-build/<dirname>'.
  438 + #
  439 + # 'cache_dir' is the parent dir.
  440 +
  441 + cache_dir, repo_dir = guess_cache_dir(dirname)
  442 +
  443 + # does the repo already exist?
  444 + if os.path.exists(repo_dir): # YES
404 445 os.chdir(repo_dir)
405   - print 'changed to: ', repo_dir, 'to do fetch.'
  446 + log_info('changed to: ', repo_dir, 'to do fetch.')
  447 + self.update_repository()
  448 + else: # NO
  449 + # do a clone to create the repo dir
  450 + log_info('changing to: ' + cache_dir + ' to make new repo dir')
  451 + os.chdir(cache_dir)
406 452
407   - branchspec = '%s:%s' % (self.branch, self.branch)
408   - cmdlist = ['git', 'fetch', '-ufv', self.repository, branchspec]
409   - (ret, out, err) = _run_command(cmdlist)
  453 + self.create_repository(self.repository, dirname,
  454 + step='create cache')
  455 + assert os.path.isdir(repo_dir)
  456 +
  457 + os.chdir(cwd)
410 458
411   - self.results_dict['cache_update'] = \
412   - dict(status=ret, output=out, errout=err,
413   - command=str(cmdlist))
  459 + log_info('Using the local cache at %s for cloning' % repo_dir)
  460 + location = repo_dir
  461 + else:
  462 + location = self.repository
414 463
415   - if ret != 0:
416   - raise Exception("cannot update cache: %s" % repo_dir)
  464 + self.create_repository(location, dirname, step='clone')
417 465
418   - cmdlist = ['git', 'checkout', '-f', self.branch]
419   - (ret, out, err) = _run_command(cmdlist)
  466 + if not os.path.exists(dirname) and os.path.isdir(dirname):
  467 + log_critical('wrong guess; %s does not exist. whoops' % (dirname,))
  468 + raise Exception
  469 +
  470 + # get some info on what our repository version is
  471 + self.record_repository_info(dirname)
  472 + # record the build directory, too.
  473 + context.build_dir = os.path.join(os.getcwd(), dirname)
  474 + # signal success!
  475 + self.status = 0
420 476
421   - self.results_dict['cache_checkout_head'] = \
422   - dict(status=ret, output=out, errout=err,
423   - command=str(cmdlist))
  477 + def get_results(self):
  478 + self.results_dict['out'] = self.results_dict['errout'] = ''
  479 + self.results_dict['status'] = self.status
  480 + self.results_dict['type'] = self.command_type
  481 + self.results_dict['name'] = self.command_name
424 482
425   - if ret != 0:
426   - raise Exception("cannot reset cache: %s" % repo_dir)
  483 + return self.results_dict
427 484
428   - else: # need to create repo_dir
429   - # do a clone to create the repo dir
430   - print 'changing to: ' + cache_dir + ' to make new repo dir'
431   - os.chdir(cache_dir)
432   - cmdlist = ['git', 'clone', self.repository]
433   - (ret, out, err) = _run_command(cmdlist)
  485 +class GitClone(_VersionControlClientBase):
  486 + """Check out and/or update a git repository."""
  487 +
  488 + command_name = 'checkout'
434 489
435   - if ret != 0:
436   - raise Exception("cannot create cache in %s" % cache_dir)
437   -
438   - os.chdir(cwd)
439   - ##
440   -
441   - # now, do a clone, from either the parent OR the local cache
442   - location = self.repository
443   - if self.use_cache and os.path.isdir(repo_dir):
444   - location = repo_dir
445   - print 'Using the local cache for cloning'
  490 + def __init__(self, repository, branch='master', use_cache=True, **kwargs):
  491 + _VersionControlClientBase.__init__(self, use_cache=use_cache, **kwargs)
  492 +
  493 + self.repository = repository
  494 + self.branch = branch
446 495
447   - cmdlist = ['git', 'clone', location]
  496 + def get_dirname(self):
  497 + "Calculate the directory name resulting from a successful checkout."
  498 + p = urlparse.urlparse(self.repository)
  499 + path = p[2] # urlparse -> path
  500 +
  501 + dirname = path.rstrip('/').split('/')[-1]
  502 + if dirname.endswith('.git'):
  503 + dirname = dirname[:-4]
  504 +
  505 + log_info('git checkout dirname guessed as: %s' % (dirname,))
  506 + return dirname
  507 +
  508 + def update_repository(self):
  509 + branchspec = '%s:%s' % (self.branch, self.branch)
  510 + cmdlist = ['git', 'fetch', '-ufv', self.repository, branchspec]
  511 + print '***', cmdlist
448 512 (ret, out, err) = _run_command(cmdlist)
449 513
450   - self.results_dict['clone'] = \
451   - dict(status=ret, output=out, errout=err,
452   - command=str(cmdlist))
453   - if ret != 0:
454   - return
  514 + self.results_dict['cache_update'] = dict(status=ret, output=out,
  515 + errout=err,
  516 + command=str(cmdlist))
455 517
456   - print cmdlist, out
  518 + if ret != 0:
  519 + raise Exception("cannot update cache: %s" % repo_dir)
457 520
458   - if not os.path.exists(dirname) and os.path.isdir(dirname):
459   - print 'wrong guess; %s does not exist. whoops' % (dirname,)
460   - self.status = -1
461   - return
  521 + cmdlist = ['git', 'checkout', '-f', self.branch]
  522 + (ret, out, err) = _run_command(cmdlist)
462 523
463   - ##
  524 + self.results_dict['cache_checkout_head'] = dict(status=ret, output=out,
  525 + errout=err,
  526 + command=str(cmdlist))
464 527
465   - # check out the right branch
466   - if self.branch != 'master':
467   - cmdlist = ['git', 'checkout', 'origin/'+self.branch]
468   - (ret, out, err) = _run_command(cmdlist, dirname)
  528 + if ret != 0:
  529 + raise Exception("cannot reset cache: %s" % repo_dir)
469 530
470   - print cmdlist, out
  531 + def create_repository(self, url, dirname, step='clone'):
  532 + cmdlist = ['git', 'clone', url]
  533 + (ret, out, err) = _run_command(cmdlist)
471 534
472   - self.results_dict['checkout+origin'] = \
473   - dict(status=ret, output=out, errout=err,
474   - command=str(cmdlist), branch=self.branch)
475   - if ret != 0:
476   - return
  535 + self.results_dict[step] = dict(status=ret, output=out, errout=err,
  536 + command=str(cmdlist))
477 537
478   - cmdlist = ['git', 'checkout', '-b', self.branch]
  538 + if ret != 0:
  539 + cwd = os.getcwd()
  540 + raise Exception("cannot clone repository %s in %s" % (url, cwd))
479 541
480   - print cmdlist, out
  542 + if self.branch != 'master':
  543 + # fetch the right branch
  544 + branchspec = '%s:%s' % (self.branch, self.branch)
  545 + cmdlist = ['git', 'fetch', '-ufv', self.repository, branchspec]
  546 + (ret, out, err) = _run_command(cmdlist, dirname)
  547 + assert ret == 0, (out, err)
481 548
  549 + # check out the right branch
  550 + cmdlist = ['git', 'checkout', '-f', self.branch]
482 551 (ret, out, err) = _run_command(cmdlist, dirname)
483   - self.results_dict['checkout+-b'] = \
484   - dict(status=ret, output=out, errout=err,
485   - command=str(cmdlist), branch=self.branch)
486   - if ret != 0:
487   - return
  552 + assert ret == 0, (out, err)
488 553
489   - # get some info on what our HEAD is
  554 + def record_repository_info(self, repo_dir):
490 555 cmdlist = ['git', 'log', '-1', '--pretty=oneline']
491   - (ret, out, err) = _run_command(cmdlist, dirname)
  556 + (ret, out, err) = _run_command(cmdlist, repo_dir)
492 557
493 558 assert ret == 0, (cmdlist, ret, out, err)
494 559
495 560 self.version_info = out.strip()
496 561
497   - self.status = 0
498   -
499   - # set the build directory, too.
500   - context.build_dir = os.path.join(os.getcwd(),
501   - dirname)
502   -
503 562 def get_results(self):
504   - self.results_dict['out'] = self.results_dict['errout'] = ''
505   - self.results_dict['command'] = 'GitClone(%s, %s)' % (self.repository,
506   - self.branch)
507   - self.results_dict['status'] = self.status
508   - self.results_dict['type'] = self.command_type
509   - self.results_dict['name'] = self.command_name
510   -
  563 + # first, update basic
  564 + _VersionControlClientBase.get_results(self)
  565 +
511 566 self.results_dict['version_type'] = 'git'
512 567 if self.version_info:
513 568 self.results_dict['version_info'] = self.version_info
514 569
  570 + self.results_dict['command'] = 'GitClone(%s, %s)' % (self.repository,
  571 + self.branch)
  572 +
515 573 return self.results_dict
516 574
517   -class HgClone(SetupCommand):
  575 +class HgClone(_VersionControlClientBase):
  576 + """Check out or update an Hg (Mercurial) repository."""
518 577 command_name = 'checkout'
519 578
520   - def __init__(self, repository, branch='default', cache_dir=None,
521   - use_cache=True, **kwargs):
522   - SetupCommand.__init__(self, [], **kwargs)
  579 + def __init__(self, repository, branch='default', use_cache=True, **kwargs):
  580 + _VersionControlClientBase.__init__(self, use_cache=use_cache, **kwargs)
523 581 self.repository = repository
524 582 self.branch = branch
525 583 assert branch == 'default'
526 584
527   - self.use_cache = use_cache
528   - self.cache_dir = cache_dir
529   - if cache_dir:
530   - self.cache_dir = os.path.expanduser(cache_dir)
531   -
532   - self.duration = -1
533   - self.version_info = ''
534   -
535   - self.results_dict = {}
536   -
537   - def run(self, context):
538   - # first, guess the checkout dir name
  585 + def get_dirname(self):
  586 + "Calculate the directory name resulting from a successful checkout."
539 587 p = urlparse.urlparse(self.repository)
540   - path = p[2] # urlparse -> path
  588 + path = p[2] # urlparse -> path
541 589
542 590 dirname = path.rstrip('/').split('/')[-1]
  591 + log_info('git checkout dirname guessed as: %s' % (dirname,))
  592 + return dirname
543 593
544   - print 'hg checkout dirname guessed as: %s' % (dirname,)
545   -
546   - if self.use_cache:
547   - cache_dir = self.cache_dir
548   - if not cache_dir:
549   - repo_dir = guess_cache_dir(dirname)
550   - #repo_dir= os.path.normpath(repo_dir)
551   - print "Here guessed repo_dir before creating "+ repo_dir
552   - create_cache_dir(repo_dir, dirname)
553   - pkglength = len(dirname)
554   - cache_dir = repo_dir[:-pkglength]
555   -
556   - ##
557   -
558   - if self.use_cache:
559   - cwd = os.getcwd()
560   -
561   - if os.path.exists(repo_dir):
562   - #if os.path.isdir(dirname):
563   - os.chdir(repo_dir)
564   - print 'changed to: ', repo_dir, 'to do fetch. '
565   - cmdlist = ['hg', 'pull', self.repository]
566   - (ret, out, err) = _run_command(cmdlist)
567   -
568   - self.results_dict['cache_pull'] = \
569   - dict(status=ret, output=out, errout=err,
570   - command=str(cmdlist))
571   -
572   - if ret != 0:
573   - return
  594 + def update_repository(self):
  595 + cmdlist = ['hg', 'pull', self.repository]
  596 + (ret, out, err) = _run_command(cmdlist)
574 597
575   - cmdlist = ['hg', 'update', '-C']
576   - (ret, out, err) = _run_command(cmdlist)
  598 + self.results_dict['cache_pull'] = dict(status=ret, output=out,
  599 + errout=err,
  600 + command=str(cmdlist))
577 601
578   - self.results_dict['cache_update'] = \
579   - dict(status=ret, output=out, errout=err,
580   - command=str(cmdlist))
  602 + if ret != 0:
  603 + raise Exception, "cannot pull from %s" % self.repository
581 604
582   - if ret != 0:
583   - return
584   - else:
585   - #do a clone to create repo_dir
586   - print 'changing to:' + cache_dir + ' to make new repo_dir'
587   - os.chdir(cache_dir)
588   - cmdlist = ['hg', 'clone', self.repository]
589   - (ret, out, err) = _run_command(cmdlist)
590   - print cmdlist, out
  605 + cmdlist = ['hg', 'update', '-C']
  606 + (ret, out, err) = _run_command(cmdlist)
591 607
592   - os.chdir(cwd)
  608 + self.results_dict['cache_update'] = \
  609 + dict(status=ret, output=out, errout=err,
  610 + command=str(cmdlist))
593 611
594   - ##
  612 + assert ret == 0, (out, err)
595 613
596   - # now, do a clone, from either the parent OR the local cache
597   - location = self.repository
598   - if self.use_cache and os.path.isdir(repo_dir):
599   - location = repo_dir
600   - print 'Using the local cache for cloning'
601   - cmdlist = ['hg', 'clone', location]
  614 + def create_repository(self, url, dirname, step='clone'):
  615 + cmdlist = ['hg', 'clone', url]
602 616 (ret, out, err) = _run_command(cmdlist)
603 617
604   - self.results_dict['clone'] = \
605   - dict(status=ret, output=out, errout=err,
606   - command=str(cmdlist))
607   - if ret != 0:
608   - return
609   -
610   - print cmdlist, out
  618 + self.results_dict[step] = dict(status=ret, output=out, errout=err,
  619 + command=str(cmdlist))
611 620
612   - if not os.path.exists(dirname) and os.path.isdir(dirname):
613   - print 'wrong guess; %s does not exist. whoops' % (dirname,)
614   - self.status = -1
615   - return
616   -
617   - ##
  621 + if ret != 0:
  622 + cwd = os.getcwd()
  623 + raise Exception("cannot clone repository %s in %s" % (url, cwd))
618 624
  625 + # @CTB branch stuff unimplemented
  626 +
  627 + def record_repository_info(self, repo_dir):
619 628 # get some info on what our HEAD is
620   - cmdlist = ['hg', 'log', ]
621   - (ret, out, err) = _run_command(cmdlist, dirname)
622   -
  629 + cmdlist = ['hg', 'id', '-nib']
  630 + (ret, out, err) = _run_command(cmdlist, repo_dir)
623 631 assert ret == 0, (cmdlist, ret, out, err)
624   -
625 632 self.version_info = out.strip()
626 633
627   - self.status = 0
628   -
629   - # set the build directory, too.
630   - context.build_dir = os.path.join(os.getcwd(),
631   - dirname)
632   -
633 634 def get_results(self):
634   - self.results_dict['out'] = self.results_dict['errout'] = ''
  635 + # first, update basic
  636 + _VersionControlClientBase.get_results(self)
  637 +
635 638 self.results_dict['command'] = 'HgCheckout(%s, %s)' % (self.repository,
636 639 self.branch)
637   - self.results_dict['status'] = self.status
638   - self.results_dict['type'] = self.command_type
639   - self.results_dict['name'] = self.command_name
640   -
641 640 self.results_dict['version_type'] = 'hg'
642 641 if self.version_info:
643 642 self.results_dict['version_info'] = self.version_info
644 643
645 644 return self.results_dict
646 645
647   -class SvnUpdate(SetupCommand):
  646 +class SvnCheckout(_VersionControlClientBase):
  647 + """Check out or update a subversion repository."""
648 648 command_name = 'checkout'
649 649
650   - def __init__(self, dirname, repository, cache_dir=None, **kwargs):
651   - SetupCommand.__init__(self, [], **kwargs)
  650 + def __init__(self, dirname, repository, use_cache=True, **kwargs):
  651 + _VersionControlClientBase.__init__(self, use_cache=use_cache)
  652 +
  653 + self.dirname = dirname
652 654 self.repository = repository
653 655
654   - self.cache_dir = None
655   - if cache_dir:
656   - self.cache_dir = os.path.expanduser(cache_dir)
657   - self.duration = -1
658   - self.dirname = dirname
  656 + def get_dirname(self):
  657 + return self.dirname
659 658
660   - def run(self, context):
661   - dirname = self.dirname
  659 + def update_repository(self):
  660 + cmdlist = ['svn', 'update', '--accept', 'theirs-full']
  661 + (ret, out, err) = _run_command(cmdlist)
662 662
663   - ##
  663 + self.results_dict['svn update'] = dict(status=ret, output=out,
  664 + errout=err,
  665 + command=str(cmdlist))
664 666
665   - if self.cache_dir:
666   - print 'updating cache dir:', self.cache_dir
667   - cwd = os.getcwd()
668   - os.chdir(self.cache_dir)
669   - cmdlist = ['svn', 'update']
  667 + if ret != 0:
  668 + log_critical("cannot svn update")
  669 + raise Exception, (cmdlist, ret, out, err)
  670 +
  671 + def create_repository(self, url, dirname, step='clone'):
  672 + if os.path.isdir(url): # local dir? COPY.
  673 + shutil.copytree(url, dirname)
  674 + else: # remote repo? CO.
  675 + cmdlist = ['svn', 'co', url, dirname]
670 676 (ret, out, err) = _run_command(cmdlist)
671   - if ret != 0:
672   - self.command_list = cmdlist
673   - self.status = ret
674   - self.output = out
675   - self.errout = err
676   - return
677   -
678   - subdir = os.path.join(cwd, dirname)
679   - shutil.copytree(self.cache_dir, subdir)
680 677
681   - os.chdir(subdir)
682   - else:
683   - cmdlist = ['svn', 'co', self.repository, dirname]
  678 + self.results_dict[step] = dict(status=ret, output=out, errout=err,
  679 + command=str(cmdlist))
684 680
685   - (ret, out, err) = _run_command(cmdlist)
686 681 if ret != 0:
687   - self.command_list = cmdlist
688   - self.status = ret
689   - self.output = out
690   - self.errout = err
691   -
692   - return
693   -
694   - print cmdlist, out
  682 + log_critical("cannot svn checkout %s into %s" % (url, dirname))
  683 + raise Exception, "cannot svn checkout %s into %s" % (url, dirname)
695 684
696   - if not os.path.exists(dirname) and os.path.isdir(dirname):
697   - self.command_list = cmdlist
698   - self.status = -1
699   - self.output = ''
700   - self.errout = 'pony-build-client cannot find expected svn dir: %s' % (dirname,)
701   -
702   - print 'wrong guess; %s does not exist. whoops' % (dirname,)
703   - return
704   -
705   - os.chdir(dirname)
  685 + def record_repository_info(self, repo_dir):
  686 + cmdlist = ['svnversion']
  687 + (ret, out, err) = _run_command(cmdlist, repo_dir)
  688 + assert ret == 0, (cmdlist, ret, out, err)
  689 + self.version_info = out.strip()
706 690
707   - ##
  691 + def get_results(self):
  692 + # first, update basic
  693 + _VersionControlClientBase.get_results(self)
  694 +
  695 + self.results_dict['command'] = 'SvnCheckout(%s, %s)' %(self.repository,
  696 + self.dirname)
  697 + self.results_dict['version_type'] = 'hg'
  698 + if self.version_info:
  699 + self.results_dict['version_info'] = self.version_info
708 700
709   - self.status = 0 # success
710   - self.output = ''
711   - self.errout = ''
  701 + return self.results_dict
712 702
713 703 ###
714 704
@@ -723,7 +713,7 @@ def get_arch():
723 713 ###
724 714
725 715 def _send(server, info, results):
726   - print 'connecting to', server
  716 + log_info('connecting to', server)
727 717 s = xmlrpclib.ServerProxy(server, allow_none=True)
728 718 (result_key, auth_key) = s.add_results(info, results)
729 719 return str(auth_key)
@@ -749,8 +739,8 @@ def _upload_file(server_url, fileobj, auth_key):
749 739 try:
750 740 http_result = urllib.urlopen(upload_url, fileobj.data)
751 741 except:
752   - print 'file upload failed:', fileobj
753   - print traceback.format_exc()
  742 + log_warning('file upload failed:', str(fileobj))
  743 + log_warning(traceback.format_exc())
754 744
755 745 def do(name, commands, context=None, arch=None, stop_if_failure=True):
756 746 reslist = []
@@ -759,7 +749,7 @@ def do(name, commands, context=None, arch=None, stop_if_failure=True):
759 749 context.initialize()
760 750
761 751 for c in commands:
762   - print 'running:', c
  752 + log_debug('running:', str(c))
763 753 if context:
764 754 context.start_command(c)
765 755 c.run(context)
@@ -804,12 +794,12 @@ def send(server_url, x, hostname=None, tags=()):
804 794 client_info['tags'] = tags
805 795
806 796 server_url = get_server_url(server_url)
807   - print 'using server URL:', server_url
  797 + log_info('using server URL:', server_url)
808 798 auth_key = _send(server_url, client_info, reslist)
809 799
810 800 if files_to_upload:
811 801 for fileobj in files_to_upload:
812   - print 'uploading', fileobj
  802 + log_debug('uploading', str(fileobj))
813 803 _upload_file(server_url, fileobj, auth_key)
814 804
815 805 def check(name, server_url, tags=(), hostname=None, arch=None, reserve_time=0):
@@ -862,17 +852,45 @@ def parse_cmdline(argv=[]):
862 852 cmdline.add_option('-v', '--verbose', dest='verbose',
863 853 action='store_true', default=False,
864 854 help='set verbose reporting')
  855 +
  856 + cmdline.add_option('-e', '--python-executable', dest='python_executable',
  857 + action='store', default='python',
  858 + help='override the version of python used to build with')
  859 +
  860 + cmdline.add_option('-t', '--tagset', dest='tagset',
  861 + action='store', default=[],
  862 + help='comma-delimited list of tags to be applied')
865 863
866 864 if not argv:
867 865 (options, args) = cmdline.parse_args()
868 866 else:
869 867 (options, args) = cmdline.parse_args(argv)
  868 +
  869 + # parse the tagset
  870 + if options.tagset:
  871 + options.tagset = options.tagset.split(',')
  872 +
  873 + # there should be nothing in args.
  874 + # if there is, print a warning, then crash and burn.
  875 + if args:
  876 + print "Error--unknown arguments detected. Failing..."
  877 + sys.exit(0)
870 878
871 879 return options, args
872 880
873 881
874 882 ###
875 883
  884 +
  885 +def test_python_version(python_exe):
  886 + result = subprocess.Popen(python_exe + " -c \"print 'hello, world'\"", shell=True, \
  887 + stdout=subprocess.PIPE).communicate()
  888 + if result[0] != "hello, world\n":
  889 + return False
  890 + return True
  891 +
  892 +###
  893 +
876 894 def get_python_config(options, args):
877 895 if not len(args):
878 896 python_ver = 'python2.5'
@@ -909,7 +927,7 @@ def get_python_config(options, args):
909 927 Python_package_egg
910 928 ]),
911 929 'twill' : (get_python_config,
912   - [ SvnUpdate('twill', 'http://twill.googlecode.com/svn/branches/0.9.2-dev/twill', cache_dir='~/.pony-build/twill'),
  930 + [ SvnCheckout('twill', 'http://twill.googlecode.com/svn/branches/0.9.2-dev/twill', cache_dir='~/.pony-build/twill'),
913 931 PythonBuild,
914 932 PythonTest
915 933 ]),
4 client/test_client/__init__.