Add append_with_space method.
[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 append_with_space(self, k, v):
250         if not k in self.variables:
251             self.variables[k] = v
252         else:
253             self.variables[k] = '%s %s' % (self.variables[k], v)
254
255     def variables_string(self, escaped_quotes=False):
256         e = ''
257         for k, v in self.variables.iteritems():
258             if escaped_quotes:
259                 v = v.replace('"', '\\"')
260             e += '%s=%s ' % (k, v)
261         return e
262
263     def cleanup(self):
264         pass
265
266
267 # Windows
268 #
269
270 class WindowsTarget(Target):
271     # @param directory directory to work in; if None, we will use a temporary directory
272     def __init__(self, bits, directory=None):
273         super(WindowsTarget, self).__init__('windows', 2)
274         self.bits = bits
275         if directory is None:
276             self.directory = tempfile.mkdtemp()
277             self.rmdir = True
278         else:
279             self.directory = directory
280             self.rmdir = False
281         
282         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
283         if not os.path.exists(self.windows_prefix):
284             raise Error('windows prefix %s does not exist' % self.windows_prefix)
285             
286         if self.bits == 32:
287             self.mingw_name = 'i686'
288         else:
289             self.mingw_name = 'x86_64'
290
291         mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
292         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
293
294         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
295         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.work_dir_cscript(), self.work_dir_cscript()))
296         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
297         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
298         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
299         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
300         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
301         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
302         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.work_dir_cscript())
303         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.work_dir_cscript())
304         for p in self.mingw_prefixes:
305             cxx += ' -I%s/include' % p
306             link += ' -L%s/lib' % p
307         self.set('CXXFLAGS', '"%s"' % cxx)
308         self.set('LINKFLAGS', '"%s"' % link)
309
310     def work_dir_cdist(self):
311         return '%s/%d' % (self.directory, self.bits)
312
313     def work_dir_cscript(self):
314         return '%s/%d' % (self.directory, self.bits)
315
316     def command(self, c):
317         log('host -> %s' % c)
318         command('%s %s' % (self.variables_string(), c))
319
320     def cleanup(self):
321         if self.rmdir:
322             rmtree(self.directory)
323
324 #
325 # Linux
326 #
327
328 class LinuxTarget(Target):
329     def __init__(self, distro, version, bits, directory=None):
330         "directory -- directory to work in; if None, we will use the configured linux_dir_in_chroot"
331         super(LinuxTarget, self).__init__('linux', 2)
332         self.distro = distro
333         self.version = version
334         self.bits = bits
335         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
336         if directory is None:
337             self.dir_in_chroot = config.get('linux_dir_in_chroot')
338         else:
339             self.dir_in_chroot = directory
340
341         for g in glob.glob('%s/*' % self.work_dir_cdist()):
342             rmtree(g)
343
344         self.set('CXXFLAGS', '-I%s/include' % self.work_dir_cscript())
345         self.set('LINKFLAGS', '-L%s/lib' % self.work_dir_cscript())
346         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.work_dir_cscript())
347         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
348
349     def work_dir_cdist(self):
350         return '%s/%s%s' % (config.get('linux_chroot_prefix'), self.chroot, self.dir_in_chroot)
351
352     def work_dir_cscript(self):
353         return self.dir_in_chroot
354
355     def command(self, c):
356         # Work out the cwd for the chrooted command
357         cwd = os.getcwd()
358         prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
359         assert(cwd.startswith(prefix))
360         cwd = cwd[len(prefix):]
361
362         log('schroot [%s] -> %s' % (cwd, c))
363         command('%s schroot -c %s -d %s -p -- %s' % (self.variables_string(), self.chroot, cwd, c))
364
365     def cleanup(self):
366         for g in glob.glob('%s/*' % self.work_dir_cdist()):
367             rmtree(g)
368
369 #
370 # OS X
371 #
372
373 class OSXTarget(Target):
374     def __init__(self, directory=None):
375         "directory -- directory to work in; if None, we will use the configured osx_dir_in_host"
376         super(OSXTarget, self).__init__('osx', 4)
377
378         if directory is None:
379             self.dir_in_host = config.get('osx_dir_in_host')
380         else:
381             self.dir_in_host = directory
382
383         for g in glob.glob('%s/*' % self.dir_in_host):
384             rmtree(g)
385
386     def command(self, c):
387         command('%s %s' % (self.variables_string(False), c))
388
389
390 class OSXSingleTarget(OSXTarget):
391     def __init__(self, bits, directory=None):
392         super(OSXSingleTarget, self).__init__(directory)
393         self.bits = bits
394
395         if bits == 32:
396             arch = 'i386'
397         else:
398             arch = 'x86_64'
399
400         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
401         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
402
403         # Environment variables
404         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.work_dir_cscript(), enviro, flags))
405         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.work_dir_cscript(), enviro, flags))
406         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.work_dir_cscript(), enviro, flags))
407         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.work_dir_cscript(), enviro, flags))
408         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.work_dir_cscript(), enviro))
409         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
410         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
411
412     def work_dir_cdist(self):
413         return self.work_dir_cscript()
414
415     def work_dir_cscript(self):
416         return '%s/%d' % (self.dir_in_host, self.bits)
417
418     def package(self, project):
419         raise Error('cannot package non-universal OS X versions')
420
421
422 class OSXUniversalTarget(OSXTarget):
423     def __init__(self, directory=None):
424         super(OSXUniversalTarget, self).__init__(directory)
425         self.parts = []
426         self.parts.append(OSXSingleTarget(32, directory))
427         self.parts.append(OSXSingleTarget(64, directory))
428
429     def work_dir_cscript(self):
430         return self.dir_in_host
431
432     def package(self, project):
433         for p in self.parts:
434             project.checkout(p)
435             p.build_dependencies(project)
436             p.build(project)
437
438         return project.cscript['package'](self, project.version)
439     
440
441 #
442 # Source
443 #
444
445 class SourceTarget(Target):
446     def __init__(self):
447         super(SourceTarget, self).__init__('source', 2)
448         self.directory = tempfile.mkdtemp()
449
450     def work_dir_cdist(self):
451         return self.directory
452
453     def work_dir_cscript(self):
454         return self.directory
455
456     def command(self, c):
457         log('host -> %s' % c)
458         command('%s %s' % (self.variables_string(), c))
459
460     def cleanup(self):
461         rmtree(self.directory)
462
463     def package(self, project):
464         project.checkout(self)
465         name = read_wscript_variable(os.getcwd(), 'APPNAME')
466         command('./waf dist')
467         return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
468
469
470 # @param s Target string:
471 #       windows-{32,64}
472 #    or ubuntu-version-{32,64}
473 #    or debian-version-{32,64}
474 #    or centos-version-{32,64}
475 #    or osx-{32,64}
476 #    or source      
477 # @param debug True to build with debugging symbols (where possible)
478 def target_factory(s, debug, work):
479     target = None
480     if s.startswith('windows-'):
481         target = WindowsTarget(int(s.split('-')[1]), work)
482     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
483         p = s.split('-')
484         if len(p) != 3:
485             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
486             sys.exit(1)
487         target = LinuxTarget(p[0], p[1], int(p[2]), work)
488     elif s.startswith('osx-'):
489         target = OSXSingleTarget(int(s.split('-')[1]), work)
490     elif s == 'osx':
491         if args.command == 'build':
492             target = OSXSingleTarget(64, work)
493         else:
494             target = OSXUniversalTarget(work)
495     elif s == 'source':
496         target = SourceTarget()
497
498     if target is not None:
499         target.debug = debug
500
501     return target
502
503
504 #
505 # Project
506 #
507  
508 class Project(object):
509     def __init__(self, name, directory, specifier=None):
510         self.name = name
511         self.directory = directory
512         self.version = None
513         self.specifier = specifier
514         if self.specifier is None:
515             self.specifier = 'master'
516
517     def checkout(self, target):
518         flags = ''
519         redirect = ''
520         if args.quiet:
521             flags = '-q'
522             redirect = '>/dev/null'
523         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.work_dir_cdist(), self.name))
524         os.chdir('%s/src/%s' % (target.work_dir_cdist(), self.name))
525         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
526         command('git submodule init')
527         command('git submodule update')
528         os.chdir(self.directory)
529
530         proj = '%s/src/%s/%s' % (target.work_dir_cdist(), self.name, self.directory)
531
532         self.read_cscript('%s/cscript' % proj)
533         
534         if os.path.exists('%s/wscript' % proj):
535             v = read_wscript_variable(proj, "VERSION");
536             if v is not None:
537                 self.version = Version(v)
538
539     def read_cscript(self, s):
540         self.cscript = {}
541         execfile(s, self.cscript)
542
543 def set_version_in_wscript(version):
544     f = open('wscript', 'rw')
545     o = open('wscript.tmp', 'w')
546     while 1:
547         l = f.readline()
548         if l == '':
549             break
550
551         s = l.split()
552         if len(s) == 3 and s[0] == "VERSION":
553             print "Writing %s" % version
554             print >>o,"VERSION = '%s'" % version
555         else:
556             print >>o,l,
557     f.close()
558     o.close()
559
560     os.rename('wscript.tmp', 'wscript')
561
562 def append_version_to_changelog(version):
563     try:
564         f = open('ChangeLog', 'r')
565     except:
566         log('Could not open ChangeLog')
567         return
568
569     c = f.read()
570     f.close()
571
572     f = open('ChangeLog', 'w')
573     now = datetime.datetime.now()
574     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))
575     f.write(c)
576
577 def append_version_to_debian_changelog(version):
578     if not os.path.exists('debian'):
579         log('Could not find debian directory')
580         return
581
582     command('dch -b -v %s-1 "New upstream release."' % version)
583
584 #
585 # Command-line parser
586 #
587
588 parser = argparse.ArgumentParser()
589 parser.add_argument('command')
590 parser.add_argument('-p', '--project', help='project name')
591 parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
592 parser.add_argument('--minor', help='minor version number bump', action='store_true')
593 parser.add_argument('--micro', help='micro version number bump', action='store_true')
594 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
595 parser.add_argument('-o', '--output', help='output directory', default='.')
596 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
597 parser.add_argument('-t', '--target', help='target')
598 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
599 parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
600 parser.add_argument('-w', '--work', help='override default work directory')
601 args = parser.parse_args()
602
603 args.output = os.path.abspath(args.output)
604 if args.work is not None:
605     args.work = os.path.abspath(args.work)
606
607 if args.project is None and args.command != 'shell':
608     raise Error('you must specify -p or --project')
609
610 project = Project(args.project, args.directory, args.checkout)
611
612 if args.command == 'build':
613     if args.target is None:
614         raise Error('you must specify -t or --target')
615
616     target = target_factory(args.target, args.debug, args.work)
617     project.checkout(target)
618     target.build_dependencies(project)
619     target.build(project)
620     if not args.keep:
621         target.cleanup()
622
623 elif args.command == 'package':
624     if args.target is None:
625         raise Error('you must specify -t or --target')
626         
627     target = target_factory(args.target, args.debug, args.work)
628
629     packages = target.package(project)
630     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
631         packages = [packages]
632
633     if target.platform == 'linux':
634         out = '%s/%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
635         try:
636             os.makedirs(out)
637         except:
638             pass
639         for p in packages:
640             copyfile(p, '%s/%s' % (out, os.path.basename(p)))
641     else:
642         for p in packages:
643             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
644
645     if not args.keep:
646         target.cleanup()
647
648 elif args.command == 'release':
649     if args.minor is False and args.micro is False:
650         raise Error('you must specify --minor or --micro')
651
652     target = SourceTarget()
653     project.checkout(target)
654
655     version = project.version
656     version.to_release()
657     if args.minor:
658         version.bump_minor()
659     else:
660         version.bump_micro()
661
662     set_version_in_wscript(version)
663     append_version_to_changelog(version)
664     append_version_to_debian_changelog(version)
665
666     command('git commit -a -m "Bump version"')
667     command('git tag -m "v%s" v%s' % (version, version))
668
669     version.to_devel()
670     set_version_in_wscript(version)
671     command('git commit -a -m "Bump version"')
672     command('git push')
673     command('git push --tags')
674
675     target.cleanup()
676
677 elif args.command == 'pot':
678     target = SourceTarget()
679     project.checkout(target)
680
681     pots = project.cscript['make_pot'](target)
682     for p in pots:
683         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
684
685     target.cleanup()
686
687 elif args.command == 'changelog':
688     target = SourceTarget()
689     project.checkout(target)
690
691     text = open('ChangeLog', 'r')
692     html = open('%s/changelog.html' % args.output, 'w')
693     versions = 8
694     
695     last = None
696     changes = []
697     
698     while 1:
699         l = text.readline()
700         if l == '':
701             break
702     
703         if len(l) > 0 and l[0] == "\t":
704             s = l.split()
705             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
706                 v = Version(s[2])
707                 if v.micro == 0:
708                     if last is not None and len(changes) > 0:
709                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
710                         print >>html,"<ul>"
711                         for c in changes:
712                             print >>html,"<li>%s" % c
713                         print >>html,"</ul>"
714                     last = s[2]
715                     changes = []
716                     versions -= 1
717                     if versions < 0:
718                         break
719             else:
720                 c = l.strip()
721                 if len(c) > 0:
722                     if c[0] == '*':
723                         changes.append(c[2:])
724                     else:
725                         changes[-1] += " " + c
726
727     target.cleanup()
728
729 elif args.command == 'manual':
730     target = SourceTarget()
731     project.checkout(target)
732
733     outs = project.cscript['make_manual'](target)
734     for o in outs:
735         if os.path.isfile(o):
736             copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
737         else:
738             copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
739
740     target.cleanup()
741
742 elif args.command == 'doxygen':
743     target = SourceTarget()
744     project.checkout(target)
745
746     dirs = project.cscript['make_doxygen'](target)
747     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
748         dirs = [dirs]
749
750     for d in dirs:
751         copytree(d, '%s/%s' % (args.output, 'doc'))
752
753     target.cleanup()
754
755 elif args.command == 'latest':
756     target = SourceTarget()
757     project.checkout(target)
758
759     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
760     t = f.readline()
761     m = re.compile(".*\((.*)\).*").match(t)
762     latest = None
763     if m:
764         tags = m.group(1).split(', ')
765         for t in tags:
766             s = t.split()
767             if len(s) > 1:
768                 t = s[1]
769             if len(t) > 0 and t[0] == 'v':
770                 latest = t[1:]
771
772     print latest
773     target.cleanup()
774
775 elif args.command == 'test':
776     if args.target is None:
777         raise Error('you must specify -t or --target')
778
779     target = None
780     try:
781         target = target_factory(args.target, args.debug, args.work)
782         target.test(project)
783     except Error as e:
784         if target is not None:
785             target.cleanup()
786         raise
787         
788     if target is not None:
789         target.cleanup()
790
791 elif args.command == 'shell':
792     if args.target is None:
793         raise Error('you must specify -t or --target')
794
795     target = target_factory(args.target, args.debug, args.work)
796     target.command('bash')
797
798 else:
799     raise Error('invalid command %s' % args.command)