Search /usr/local/lib for .pc files.
[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     target.cleanup()
631
632 elif args.command == 'release':
633     if args.minor is False and args.micro is False:
634         raise Error('you must specify --minor or --micro')
635
636     target = SourceTarget()
637     project.checkout(target)
638
639     version = project.version
640     version.to_release()
641     if args.minor:
642         version.bump_minor()
643     else:
644         version.bump_micro()
645
646     set_version_in_wscript(version)
647     append_version_to_changelog(version)
648     append_version_to_debian_changelog(version)
649
650     command('git commit -a -m "Bump version"')
651     command('git tag -m "v%s" v%s' % (version, version))
652
653     version.to_devel()
654     set_version_in_wscript(version)
655     command('git commit -a -m "Bump version"')
656     command('git push')
657     command('git push --tags')
658
659     target.cleanup()
660
661 elif args.command == 'pot':
662     target = SourceTarget()
663     project.checkout(target)
664
665     pots = project.cscript['make_pot'](target)
666     for p in pots:
667         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
668
669     target.cleanup()
670
671 elif args.command == 'changelog':
672     target = SourceTarget()
673     project.checkout(target)
674
675     text = open('ChangeLog', 'r')
676     html = open('%s/changelog.html' % args.output, 'w')
677     versions = 8
678     
679     last = None
680     changes = []
681     
682     while 1:
683         l = text.readline()
684         if l == '':
685             break
686     
687         if len(l) > 0 and l[0] == "\t":
688             s = l.split()
689             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
690                 v = Version(s[2])
691                 if v.micro == 0:
692                     if last is not None and len(changes) > 0:
693                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
694                         print >>html,"<ul>"
695                         for c in changes:
696                             print >>html,"<li>%s" % c
697                         print >>html,"</ul>"
698                     last = s[2]
699                     changes = []
700                     versions -= 1
701                     if versions < 0:
702                         break
703             else:
704                 c = l.strip()
705                 if len(c) > 0:
706                     if c[0] == '*':
707                         changes.append(c[2:])
708                     else:
709                         changes[-1] += " " + c
710
711     target.cleanup()
712
713 elif args.command == 'manual':
714     target = SourceTarget()
715     project.checkout(target)
716
717     outs = project.cscript['make_manual'](target)
718     for o in outs:
719         if os.path.isfile(o):
720             copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
721         else:
722             copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
723
724     target.cleanup()
725
726 elif args.command == 'doxygen':
727     target = SourceTarget()
728     project.checkout(target)
729
730     dirs = project.cscript['make_doxygen'](target)
731     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
732         dirs = [dirs]
733
734     for d in dirs:
735         copytree(d, '%s/%s' % (args.output, 'doc'))
736
737     target.cleanup()
738
739 elif args.command == 'latest':
740     target = SourceTarget()
741     project.checkout(target)
742
743     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
744     t = f.readline()
745     m = re.compile(".*\((.*)\).*").match(t)
746     latest = None
747     if m:
748         tags = m.group(1).split(', ')
749         for t in tags:
750             if len(t) > 0 and t[0] == 'v':
751                 latest = t[1:]
752
753     print latest
754     target.cleanup()
755
756 elif args.command == 'test':
757     if args.target is None:
758         raise Error('you must specify -t or --target')
759
760     target = None
761     try:
762         target = target_factory(args.target, args.debug, args.work)
763         target.test(project)
764     except Error as e:
765         if target is not None:
766             target.cleanup()
767         raise
768         
769     if target is not None:
770         target.cleanup()
771
772 elif args.command == 'shell':
773     if args.target is None:
774         raise Error('you must specify -t or --target')
775
776     target = target_factory(args.target, args.debug, args.work)
777     target.command('bash')
778
779 else:
780     raise Error('invalid command %s' % args.command)