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