Support older cscripts.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program; if not, write to the Free Software
17 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 import os
20 import sys
21 import shutil
22 import glob
23 import tempfile
24 import argparse
25 import datetime
26 import subprocess
27 import re
28 import copy
29 import inspect
30
31 class Error(Exception):
32     def __init__(self, value):
33         self.value = value
34     def __str__(self):
35         return '\x1b[31m%s\x1b[0m' % repr(self.value)
36     def __repr__(self):
37         return str(self)
38
39 #
40 # Configuration
41 #
42
43 class Config:
44     def __init__(self):
45         self.keys = ['linux_dir_in_chroot',
46                      'linux_chroot_prefix',
47                      'windows_environment_prefix',
48                      'mingw_prefix',
49                      'git_prefix',
50                      'osx_build_host',
51                      'osx_dir_in_host',
52                      'osx_environment_prefix',
53                      'osx_sdk_prefix',
54                      'osx_sdk']
55
56         self.dict = dict()
57
58         try:
59             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
60             while 1:
61                 l = f.readline()
62                 if l == '':
63                     break
64
65                 if len(l) > 0 and l[0] == '#':
66                     continue
67
68                 s = l.strip().split()
69                 if len(s) == 2:
70                     for k in self.keys:
71                         if k == s[0]:
72                             self.dict[k] = s[1]
73         except:
74             raise
75
76     def get(self, k):
77         if k in self.dict:
78             return self.dict[k]
79
80         raise Error('Required setting %s not found' % k)
81
82 config = Config()
83
84 #
85 # Utility bits
86
87
88 def log(m):
89     if not args.quiet:
90         print '\x1b[33m* %s\x1b[0m' % m
91
92 def copytree(a, b):
93     log('copy %s -> %s' % (a, b))
94     shutil.copytree(a, b)
95
96 def copyfile(a, b):
97     log('copy %s -> %s' % (a, b))
98     shutil.copyfile(a, b)
99
100 def rmdir(a):
101     log('remove %s' % a)
102     os.rmdir(a)
103
104 def rmtree(a):
105     log('remove %s' % a)
106     shutil.rmtree(a, ignore_errors=True)
107
108 def command(c, can_fail=False):
109     log(c)
110     r = os.system(c)
111     if (r >> 8) and not can_fail:
112         raise Error('command %s failed' % c)
113
114 def command_and_read(c):
115     log(c)
116     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
117     f = os.fdopen(os.dup(p.stdout.fileno()))
118     return f
119
120 def read_wscript_variable(directory, variable):
121     f = open('%s/wscript' % directory, 'r')
122     while 1:
123         l = f.readline()
124         if l == '':
125             break
126         
127         s = l.split()
128         if len(s) == 3 and s[0] == variable:
129             f.close()
130             return s[2][1:-1]
131
132     f.close()
133     return None
134
135 #
136 # Version
137 #
138
139 class Version:
140     def __init__(self, s):
141         self.devel = False
142
143         if s.startswith("'"):
144             s = s[1:]
145         if s.endswith("'"):
146             s = s[0:-1]
147         
148         if s.endswith('devel'):
149             s = s[0:-5]
150             self.devel = True
151
152         if s.endswith('pre'):
153             s = s[0:-3]
154
155         p = s.split('.')
156         self.major = int(p[0])
157         self.minor = int(p[1])
158         if len(p) == 3:
159             self.micro = int(p[2])
160         else:
161             self.micro = 0
162
163     def bump_minor(self):
164         self.minor += 1
165         self.micro = 0
166
167     def bump_micro(self):
168         self.micro += 1
169
170     def to_devel(self):
171         self.devel = True
172
173     def to_release(self):
174         self.devel = False
175
176     def __str__(self):
177         s = '%d.%02d.%d' % (self.major, self.minor, self.micro)
178         if self.devel:
179             s += 'devel'
180
181         return s
182
183 #
184 # Targets
185 #
186
187 class Target(object):
188     def __init__(self, platform, parallel):
189         self.platform = platform
190         self.parallel = parallel
191         # Environment variables that we will use when we call cscripts
192         self.variables = {}
193         self.debug = False
194
195     def build_dependencies(self, project):
196         cwd = os.getcwd()
197         if 'dependencies' in project.cscript:
198             print project.cscript['dependencies'](self)
199             for d in project.cscript['dependencies'](self):
200                 log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
201                 dep = Project(d[0], '.', d[1])
202                 dep.checkout(self)
203                 self.build_dependencies(dep)
204
205                 # Make the options to pass in from the option_defaults of the thing
206                 # we are building and any options specified by the parent.
207                 options = {}
208                 if 'option_defaults' in dep.cscript:
209                     options = dep.cscript['option_defaults']()
210                     if len(d) > 2:
211                         for k, v in d[2].iteritems():
212                             options[k] = v
213
214                 self.build(dep, options)
215
216         os.chdir(cwd)
217
218     def build(self, project, options=None):
219         variables = copy.copy(self.variables)
220         print 'Target %s builds %s with %s' % (self.platform, project.name, self.variables)
221         if len(inspect.getargspec(project.cscript['build']).args) == 2:
222             project.cscript['build'](self, options)
223         else:
224             project.cscript['build'](self)
225         self.variables = variables
226
227     def package(self, project):
228         project.checkout(self)
229         self.build_dependencies(project)
230         self.build(project)
231         return project.cscript['package'](self, project.version)
232
233     def test(self, project):
234         project.checkout(self)
235         self.build_dependencies(project)
236         self.build(project)
237         project.cscript['test'](self)
238
239     def set(self, a, b):
240         print "Target set %s=%s" % (a, b)
241         self.variables[a] = b
242
243     def unset(self, a):
244         del(self.variables[a])
245
246     def get(self, a):
247         return self.variables[a]
248
249     def variables_string(self, escaped_quotes=False):
250         e = ''
251         for k, v in self.variables.iteritems():
252             if escaped_quotes:
253                 v = v.replace('"', '\\"')
254             e += '%s=%s ' % (k, v)
255         return e
256
257     def cleanup(self):
258         pass
259
260
261 # Windows
262 #
263
264 class WindowsTarget(Target):
265     # @param directory directory to work in; if None, we will use a temporary directory
266     def __init__(self, bits, directory=None):
267         super(WindowsTarget, self).__init__('windows', 2)
268         self.bits = bits
269         if directory is None:
270             self.directory = tempfile.mkdtemp()
271             self.rmdir = True
272         else:
273             self.directory = directory
274             self.rmdir = False
275         
276         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
277         if not os.path.exists(self.windows_prefix):
278             raise Error('windows prefix %s does not exist' % self.windows_prefix)
279             
280         if self.bits == 32:
281             self.mingw_name = 'i686'
282         else:
283             self.mingw_name = 'x86_64'
284
285         mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
286         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
287
288         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
289         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.work_dir_cscript(), self.work_dir_cscript()))
290         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
291         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
292         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
293         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
294         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
295         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
296         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.work_dir_cscript())
297         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.work_dir_cscript())
298         for p in self.mingw_prefixes:
299             cxx += ' -I%s/include' % p
300             link += ' -L%s/lib' % p
301         self.set('CXXFLAGS', '"%s"' % cxx)
302         self.set('LINKFLAGS', '"%s"' % link)
303
304     def work_dir_cdist(self):
305         return '%s/%d' % (self.directory, self.bits)
306
307     def work_dir_cscript(self):
308         return '%s/%d' % (self.directory, self.bits)
309
310     def command(self, c):
311         log('host -> %s' % c)
312         command('%s %s' % (self.variables_string(), c))
313
314     def cleanup(self):
315         if self.rmdir:
316             rmtree(self.directory)
317
318 #
319 # Linux
320 #
321
322 class LinuxTarget(Target):
323     def __init__(self, distro, version, bits, directory=None):
324         "directory -- directory to work in; if None, we will use the configured linux_dir_in_chroot"
325         super(LinuxTarget, self).__init__('linux', 2)
326         self.distro = distro
327         self.version = version
328         self.bits = bits
329         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
330         if directory is None:
331             self.dir_in_chroot = config.get('linux_dir_in_chroot')
332         else:
333             self.dir_in_chroot = directory
334
335         for g in glob.glob('%s/*' % self.work_dir_cdist()):
336             rmtree(g)
337
338         self.set('CXXFLAGS', '-I%s/include' % self.work_dir_cscript())
339         self.set('LINKFLAGS', '-L%s/lib' % self.work_dir_cscript())
340         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.work_dir_cscript())
341         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
342
343     def work_dir_cdist(self):
344         return '%s/%s%s' % (config.get('linux_chroot_prefix'), self.chroot, self.dir_in_chroot)
345
346     def work_dir_cscript(self):
347         return self.dir_in_chroot
348
349     def command(self, c):
350         # Work out the cwd for the chrooted command
351         cwd = os.getcwd()
352         prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
353         assert(cwd.startswith(prefix))
354         cwd = cwd[len(prefix):]
355
356         log('schroot [%s] -> %s' % (cwd, c))
357         command('%s schroot -c %s -d %s -p -- %s' % (self.variables_string(), self.chroot, cwd, c))
358
359     def cleanup(self):
360         for g in glob.glob('%s/*' % self.work_dir_cdist()):
361             rmtree(g)
362
363 #
364 # OS X
365 #
366
367 class OSXTarget(Target):
368     def __init__(self, directory=None):
369         "directory -- directory to work in; if None, we will use the configured osx_dir_in_host"
370         super(OSXTarget, self).__init__('osx', 4)
371
372         if directory is None:
373             self.dir_in_host = config.get('osx_dir_in_host')
374         else:
375             self.dir_in_host = directory
376
377         for g in glob.glob('%s/*' % self.dir_in_host):
378             rmtree(g)
379
380     def command(self, c):
381         command('%s %s' % (self.variables_string(False), c))
382
383
384 class OSXSingleTarget(OSXTarget):
385     def __init__(self, bits, directory=None):
386         super(OSXSingleTarget, self).__init__(directory)
387         self.bits = bits
388
389         if bits == 32:
390             arch = 'i386'
391         else:
392             arch = 'x86_64'
393
394         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
395         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
396
397         # Environment variables
398         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.work_dir_cscript(), enviro, flags))
399         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.work_dir_cscript(), enviro, flags))
400         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.work_dir_cscript(), enviro, flags))
401         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.work_dir_cscript(), enviro, flags))
402         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.work_dir_cscript(), enviro))
403         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
404         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
405
406     def work_dir_cdist(self):
407         return self.work_dir_cscript()
408
409     def work_dir_cscript(self):
410         return '%s/%d' % (self.dir_in_host, self.bits)
411
412     def package(self, project):
413         raise Error('cannot package non-universal OS X versions')
414
415
416 class OSXUniversalTarget(OSXTarget):
417     def __init__(self, directory=None):
418         super(OSXUniversalTarget, self).__init__(directory)
419         self.parts = []
420         self.parts.append(OSXSingleTarget(32, directory))
421         self.parts.append(OSXSingleTarget(64, directory))
422
423     def work_dir_cscript(self):
424         return self.dir_in_host
425
426     def package(self, project):
427         for p in self.parts:
428             project.checkout(p)
429             p.build_dependencies(project)
430             p.build(project)
431
432         return project.cscript['package'](self, project.version)
433     
434
435 #
436 # Source
437 #
438
439 class SourceTarget(Target):
440     def __init__(self):
441         super(SourceTarget, self).__init__('source', 2)
442         self.directory = tempfile.mkdtemp()
443
444     def work_dir_cdist(self):
445         return self.directory
446
447     def work_dir_cscript(self):
448         return self.directory
449
450     def command(self, c):
451         log('host -> %s' % c)
452         command('%s %s' % (self.variables_string(), c))
453
454     def cleanup(self):
455         rmtree(self.directory)
456
457     def package(self, project):
458         project.checkout(self)
459         name = read_wscript_variable(os.getcwd(), 'APPNAME')
460         command('./waf dist')
461         return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
462
463
464 # @param s Target string:
465 #       windows-{32,64}
466 #    or ubuntu-version-{32,64}
467 #    or debian-version-{32,64}
468 #    or centos-version-{32,64}
469 #    or osx-{32,64}
470 #    or source      
471 # @param debug True to build with debugging symbols (where possible)
472 def target_factory(s, debug, work):
473     target = None
474     if s.startswith('windows-'):
475         target = WindowsTarget(int(s.split('-')[1]), work)
476     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
477         p = s.split('-')
478         if len(p) != 3:
479             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
480             sys.exit(1)
481         target = LinuxTarget(p[0], p[1], int(p[2]), work)
482     elif s.startswith('osx-'):
483         target = OSXSingleTarget(int(s.split('-')[1]), work)
484     elif s == 'osx':
485         if args.command == 'build':
486             target = OSXSingleTarget(64, work)
487         else:
488             target = OSXUniversalTarget(work)
489     elif s == 'source':
490         target = SourceTarget()
491
492     if target is not None:
493         target.debug = debug
494
495     return target
496
497
498 #
499 # Project
500 #
501  
502 class Project(object):
503     def __init__(self, name, directory, specifier=None):
504         self.name = name
505         self.directory = directory
506         self.version = None
507         self.specifier = specifier
508         if self.specifier is None:
509             self.specifier = 'master'
510
511     def checkout(self, target):
512         flags = ''
513         redirect = ''
514         if args.quiet:
515             flags = '-q'
516             redirect = '>/dev/null'
517         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.work_dir_cdist(), self.name))
518         os.chdir('%s/src/%s' % (target.work_dir_cdist(), self.name))
519         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
520         command('git submodule init')
521         command('git submodule update')
522         os.chdir(self.directory)
523
524         proj = '%s/src/%s/%s' % (target.work_dir_cdist(), self.name, self.directory)
525
526         self.read_cscript('%s/cscript' % proj)
527         
528         if os.path.exists('%s/wscript' % proj):
529             v = read_wscript_variable(proj, "VERSION");
530             if v is not None:
531                 self.version = Version(v)
532
533     def read_cscript(self, s):
534         self.cscript = {}
535         execfile(s, self.cscript)
536
537 def set_version_in_wscript(version):
538     f = open('wscript', 'rw')
539     o = open('wscript.tmp', 'w')
540     while 1:
541         l = f.readline()
542         if l == '':
543             break
544
545         s = l.split()
546         if len(s) == 3 and s[0] == "VERSION":
547             print "Writing %s" % version
548             print >>o,"VERSION = '%s'" % version
549         else:
550             print >>o,l,
551     f.close()
552     o.close()
553
554     os.rename('wscript.tmp', 'wscript')
555
556 def append_version_to_changelog(version):
557     try:
558         f = open('ChangeLog', 'r')
559     except:
560         log('Could not open ChangeLog')
561         return
562
563     c = f.read()
564     f.close()
565
566     f = open('ChangeLog', 'w')
567     now = datetime.datetime.now()
568     f.write('%d-%02d-%02d  Carl Hetherington  <cth@carlh.net>\n\n\t* Version %s released.\n\n' % (now.year, now.month, now.day, version))
569     f.write(c)
570
571 def append_version_to_debian_changelog(version):
572     if not os.path.exists('debian'):
573         log('Could not find debian directory')
574         return
575
576     command('dch -b -v %s-1 "New upstream release."' % version)
577
578 #
579 # Command-line parser
580 #
581
582 parser = argparse.ArgumentParser()
583 parser.add_argument('command')
584 parser.add_argument('-p', '--project', help='project name')
585 parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
586 parser.add_argument('--minor', help='minor version number bump', action='store_true')
587 parser.add_argument('--micro', help='micro version number bump', action='store_true')
588 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
589 parser.add_argument('-o', '--output', help='output directory', default='.')
590 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
591 parser.add_argument('-t', '--target', help='target')
592 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
593 parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
594 parser.add_argument('-w', '--work', help='override default work directory')
595 args = parser.parse_args()
596
597 args.output = os.path.abspath(args.output)
598 if args.work is not None:
599     args.work = os.path.abspath(args.work)
600
601 if args.project is None and args.command != 'shell':
602     raise Error('you must specify -p or --project')
603
604 project = Project(args.project, args.directory, args.checkout)
605
606 if args.command == 'build':
607     if args.target is None:
608         raise Error('you must specify -t or --target')
609
610     target = target_factory(args.target, args.debug, args.work)
611     project.checkout(target)
612     target.build_dependencies(project)
613     target.build(project)
614     if not args.keep:
615         target.cleanup()
616
617 elif args.command == 'package':
618     if args.target is None:
619         raise Error('you must specify -t or --target')
620         
621     target = target_factory(args.target, args.debug, args.work)
622
623     packages = target.package(project)
624     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
625         packages = [packages]
626
627     if target.platform == 'linux':
628         out = '%s/%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
629         try:
630             os.makedirs(out)
631         except:
632             pass
633         for p in packages:
634             copyfile(p, '%s/%s' % (out, os.path.basename(p)))
635     else:
636         for p in packages:
637             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
638
639     if not args.keep:
640         target.cleanup()
641
642 elif args.command == 'release':
643     if args.minor is False and args.micro is False:
644         raise Error('you must specify --minor or --micro')
645
646     target = SourceTarget()
647     project.checkout(target)
648
649     version = project.version
650     version.to_release()
651     if args.minor:
652         version.bump_minor()
653     else:
654         version.bump_micro()
655
656     set_version_in_wscript(version)
657     append_version_to_changelog(version)
658     append_version_to_debian_changelog(version)
659
660     command('git commit -a -m "Bump version"')
661     command('git tag -m "v%s" v%s' % (version, version))
662
663     version.to_devel()
664     set_version_in_wscript(version)
665     command('git commit -a -m "Bump version"')
666     command('git push')
667     command('git push --tags')
668
669     target.cleanup()
670
671 elif args.command == 'pot':
672     target = SourceTarget()
673     project.checkout(target)
674
675     pots = project.cscript['make_pot'](target)
676     for p in pots:
677         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
678
679     target.cleanup()
680
681 elif args.command == 'changelog':
682     target = SourceTarget()
683     project.checkout(target)
684
685     text = open('ChangeLog', 'r')
686     html = open('%s/changelog.html' % args.output, 'w')
687     versions = 8
688     
689     last = None
690     changes = []
691     
692     while 1:
693         l = text.readline()
694         if l == '':
695             break
696     
697         if len(l) > 0 and l[0] == "\t":
698             s = l.split()
699             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
700                 v = Version(s[2])
701                 if v.micro == 0:
702                     if last is not None and len(changes) > 0:
703                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
704                         print >>html,"<ul>"
705                         for c in changes:
706                             print >>html,"<li>%s" % c
707                         print >>html,"</ul>"
708                     last = s[2]
709                     changes = []
710                     versions -= 1
711                     if versions < 0:
712                         break
713             else:
714                 c = l.strip()
715                 if len(c) > 0:
716                     if c[0] == '*':
717                         changes.append(c[2:])
718                     else:
719                         changes[-1] += " " + c
720
721     target.cleanup()
722
723 elif args.command == 'manual':
724     target = SourceTarget()
725     project.checkout(target)
726
727     outs = project.cscript['make_manual'](target)
728     for o in outs:
729         if os.path.isfile(o):
730             copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
731         else:
732             copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
733
734     target.cleanup()
735
736 elif args.command == 'doxygen':
737     target = SourceTarget()
738     project.checkout(target)
739
740     dirs = project.cscript['make_doxygen'](target)
741     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
742         dirs = [dirs]
743
744     for d in dirs:
745         copytree(d, '%s/%s' % (args.output, 'doc'))
746
747     target.cleanup()
748
749 elif args.command == 'latest':
750     target = SourceTarget()
751     project.checkout(target)
752
753     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
754     t = f.readline()
755     m = re.compile(".*\((.*)\).*").match(t)
756     latest = None
757     if m:
758         tags = m.group(1).split(', ')
759         for t in tags:
760             s = t.split()
761             if len(s) > 1:
762                 t = s[1]
763             if len(t) > 0 and t[0] == 'v':
764                 latest = t[1:]
765
766     print latest
767     target.cleanup()
768
769 elif args.command == 'test':
770     if args.target is None:
771         raise Error('you must specify -t or --target')
772
773     target = None
774     try:
775         target = target_factory(args.target, args.debug, args.work)
776         target.test(project)
777     except Error as e:
778         if target is not None:
779             target.cleanup()
780         raise
781         
782     if target is not None:
783         target.cleanup()
784
785 elif args.command == 'shell':
786     if args.target is None:
787         raise Error('you must specify -t or --target')
788
789     target = target_factory(args.target, args.debug, args.work)
790     target.command('bash')
791
792 else:
793     raise Error('invalid command %s' % args.command)