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