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