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