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