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