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