3cef8b2b548e9213530e1459483178d1ea14d121
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2014 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 import copy
29 import inspect
30
31 TEMPORARY_DIRECTORY = '/tmp'
32
33 class Globals:
34     quiet = False
35     command = None
36
37 globals = Globals()
38
39 class Error(Exception):
40     def __init__(self, value):
41         self.value = value
42     def __str__(self):
43         return self.value
44     def __repr__(self):
45         return str(self)
46
47 #
48 # Configuration
49 #
50
51 class Option(object):
52     def __init__(self, key, default=None):
53         self.key = key
54         self.value = default
55
56     def offer(self, key, value):
57         if key == self.key:
58             self.value = value
59
60 class BoolOption(object):
61     def __init__(self, key):
62         self.key = key
63         self.value = False
64
65     def offer(self, key, value):
66         if key == self.key:
67             self.value = (value == 'yes' or value == '1' or value == 'true')
68
69 class Config:
70     def __init__(self):
71         self.options = [ Option('linux_chroot_prefix'),
72                          Option('windows_environment_prefix'),
73                          Option('mingw_prefix'),
74                          Option('git_prefix'),
75                          Option('osx_build_host'),
76                          Option('osx_environment_prefix'),
77                          Option('osx_sdk_prefix'),
78                          Option('osx_sdk'),
79                          Option('parallel', 4) ]
80
81         try:
82             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
83             while 1:
84                 l = f.readline()
85                 if l == '':
86                     break
87
88                 if len(l) > 0 and l[0] == '#':
89                     continue
90
91                 s = l.strip().split()
92                 if len(s) == 2:
93                     for k in self.options:
94                         k.offer(s[0], s[1])
95         except:
96             raise
97
98     def get(self, k):
99         for o in self.options:
100             if o.key == k:
101                 return o.value
102
103         raise Error('Required setting %s not found' % k)
104
105 config = Config()
106
107 #
108 # Utility bits
109
110
111 def log(m):
112     if not globals.quiet:
113         print '\x1b[33m* %s\x1b[0m' % m
114
115 def copytree(a, b):
116     log('copy %s -> %s' % (a, b))
117     shutil.copytree(a, b)
118
119 def copyfile(a, b):
120     log('copy %s -> %s' % (a, b))
121     shutil.copyfile(a, b)
122
123 def rmdir(a):
124     log('remove %s' % a)
125     os.rmdir(a)
126
127 def rmtree(a):
128     log('remove %s' % a)
129     shutil.rmtree(a, ignore_errors=True)
130
131 def command(c, can_fail=False):
132     log(c)
133     r = os.system(c)
134     if (r >> 8) and not can_fail:
135         raise Error('command %s failed' % c)
136
137 def command_and_read(c):
138     log(c)
139     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
140     f = os.fdopen(os.dup(p.stdout.fileno()))
141     return f
142
143 def read_wscript_variable(directory, variable):
144     f = open('%s/wscript' % directory, 'r')
145     while 1:
146         l = f.readline()
147         if l == '':
148             break
149         
150         s = l.split()
151         if len(s) == 3 and s[0] == variable:
152             f.close()
153             return s[2][1:-1]
154
155     f.close()
156     return None
157
158 #
159 # Version
160 #
161
162 class Version:
163     def __init__(self, s):
164         self.devel = False
165
166         if s.startswith("'"):
167             s = s[1:]
168         if s.endswith("'"):
169             s = s[0:-1]
170         
171         if s.endswith('devel'):
172             s = s[0:-5]
173             self.devel = True
174
175         if s.endswith('pre'):
176             s = s[0:-3]
177
178         p = s.split('.')
179         self.major = int(p[0])
180         self.minor = int(p[1])
181         if len(p) == 3:
182             self.micro = int(p[2])
183         else:
184             self.micro = 0
185
186     def bump_minor(self):
187         self.minor += 1
188         self.micro = 0
189
190     def bump_micro(self):
191         self.micro += 1
192
193     def to_devel(self):
194         self.devel = True
195
196     def to_release(self):
197         self.devel = False
198
199     def __str__(self):
200         s = '%d.%d.%d' % (self.major, self.minor, self.micro)
201         if self.devel:
202             s += 'devel'
203
204         return s
205
206 #
207 # Targets
208 #
209
210 class Target(object):
211     # @param directory directory to work in; if None we will use a temporary directory
212     # Temporary directories will be removed after use; specified directories will not
213     def __init__(self, platform, directory=None):
214         self.platform = platform
215         self.parallel = int(config.get('parallel'))
216
217         if directory is None:
218             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
219             self.rmdir = True
220         else:
221             self.directory = directory
222             self.rmdir = False
223
224         # Environment variables that we will use when we call cscripts
225         self.variables = {}
226         self.debug = False
227
228     def build_dependencies(self, project):
229         cwd = os.getcwd()
230         if 'dependencies' in project.cscript:
231             for d in project.cscript['dependencies'](self):
232                 log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
233                 dep = Project(d[0], '.', d[1])
234                 dep.checkout(self)
235                 self.build_dependencies(dep)
236
237                 # Make the options to pass in from the option_defaults of the thing
238                 # we are building and any options specified by the parent.
239                 options = {}
240                 if 'option_defaults' in dep.cscript:
241                     options = dep.cscript['option_defaults']()
242                     if len(d) > 2:
243                         for k, v in d[2].iteritems():
244                             options[k] = v
245
246                 self.build(dep, options)
247
248         os.chdir(cwd)
249
250     def build(self, project, options=None):
251         variables = copy.copy(self.variables)
252         if len(inspect.getargspec(project.cscript['build']).args) == 2:
253             project.cscript['build'](self, options)
254         else:
255             project.cscript['build'](self)
256         self.variables = variables
257
258     def package(self, project):
259         project.checkout(self)
260         self.build_dependencies(project)
261         self.build(project)
262         return project.cscript['package'](self, project.version)
263
264     def test(self, project):
265         project.checkout(self)
266         self.build_dependencies(project)
267         self.build(project)
268         project.cscript['test'](self)
269
270     def set(self, a, b):
271         self.variables[a] = b
272
273     def unset(self, a):
274         del(self.variables[a])
275
276     def get(self, a):
277         return self.variables[a]
278
279     def append_with_space(self, k, v):
280         if not k in self.variables:
281             self.variables[k] = v
282         else:
283             self.variables[k] = '%s %s' % (self.variables[k], v)
284
285     def variables_string(self, escaped_quotes=False):
286         e = ''
287         for k, v in self.variables.iteritems():
288             if escaped_quotes:
289                 v = v.replace('"', '\\"')
290             e += '%s=%s ' % (k, v)
291         return e
292
293     def cleanup(self):
294         if self.rmdir:
295             rmtree(self.directory)
296
297
298 # Windows
299 #
300
301 class WindowsTarget(Target):
302     def __init__(self, bits, directory=None):
303         super(WindowsTarget, self).__init__('windows', directory)
304         self.bits = bits
305
306         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
307         if not os.path.exists(self.windows_prefix):
308             raise Error('windows prefix %s does not exist' % self.windows_prefix)
309             
310         if self.bits == 32:
311             self.mingw_name = 'i686'
312         else:
313             self.mingw_name = 'x86_64'
314
315         mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
316         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
317
318         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
319         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
320         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
321         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
322         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
323         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
324         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
325         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
326         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
327         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
328         for p in self.mingw_prefixes:
329             cxx += ' -I%s/include' % p
330             link += ' -L%s/lib' % p
331         self.set('CXXFLAGS', '"%s"' % cxx)
332         self.set('CPPFLAGS', '')
333         self.set('LINKFLAGS', '"%s"' % link)
334
335     def command(self, c):
336         log('host -> %s' % c)
337         command('%s %s' % (self.variables_string(), c))
338
339 #
340 # Linux
341 #
342
343 class LinuxTarget(Target):
344     def __init__(self, distro, version, bits, directory=None):
345         super(LinuxTarget, self).__init__('linux', directory)
346         self.distro = distro
347         self.version = version
348         self.bits = bits
349         # e.g. ubuntu-14.04-64
350         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
351         # e.g. /home/carl/Environments/ubuntu-14.04-64
352         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
353
354         self.set('CXXFLAGS', '-I%s/include' % self.directory)
355         self.set('CPPFLAGS', '')
356         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
357         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
358         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
359
360     def command(self, c):
361         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
362
363 #
364 # OS X
365 #
366
367 class OSXTarget(Target):
368     def __init__(self, directory=None):
369         super(OSXTarget, self).__init__('osx', directory)
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.directory, enviro, flags))
390         self.set('CPPFLAGS', '')
391         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
392         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
393         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
394         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
395         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
396         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
397
398     def package(self, project):
399         raise Error('cannot package non-universal OS X versions')
400
401
402 class OSXUniversalTarget(OSXTarget):
403     def __init__(self, directory=None):
404         super(OSXUniversalTarget, self).__init__(directory)
405         self.parts = []
406         self.parts.append(OSXSingleTarget(32, os.path.join(self.directory, '32')))
407         self.parts.append(OSXSingleTarget(64, os.path.join(self.directory, '64')))
408
409     def package(self, project):
410         for p in self.parts:
411             project.checkout(p)
412             p.build_dependencies(project)
413             p.build(project)
414
415         return project.cscript['package'](self, project.version)
416     
417
418 #
419 # Source
420 #
421
422 class SourceTarget(Target):
423     def __init__(self):
424         super(SourceTarget, self).__init__('source')
425
426     def command(self, c):
427         log('host -> %s' % c)
428         command('%s %s' % (self.variables_string(), c))
429
430     def cleanup(self):
431         rmtree(self.directory)
432
433     def package(self, project):
434         project.checkout(self)
435         name = read_wscript_variable(os.getcwd(), 'APPNAME')
436         command('./waf dist')
437         return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
438
439
440 # @param s Target string:
441 #       windows-{32,64}
442 #    or ubuntu-version-{32,64}
443 #    or debian-version-{32,64}
444 #    or centos-version-{32,64}
445 #    or osx-{32,64}
446 #    or source      
447 # @param debug True to build with debugging symbols (where possible)
448 def target_factory(s, debug, work):
449     target = None
450     if s.startswith('windows-'):
451         target = WindowsTarget(int(s.split('-')[1]), work)
452     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
453         p = s.split('-')
454         if len(p) != 3:
455             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
456             sys.exit(1)
457         target = LinuxTarget(p[0], p[1], int(p[2]), work)
458     elif s.startswith('osx-'):
459         target = OSXSingleTarget(int(s.split('-')[1]), work)
460     elif s == 'osx':
461         if globals.command == 'build':
462             target = OSXSingleTarget(64, work)
463         else:
464             target = OSXUniversalTarget(work)
465     elif s == 'source':
466         target = SourceTarget()
467
468     if target is not None:
469         target.debug = debug
470
471     return target
472
473
474 #
475 # Project
476 #
477  
478 class Project(object):
479     def __init__(self, name, directory, specifier=None):
480         self.name = name
481         self.directory = directory
482         self.version = None
483         self.specifier = specifier
484         self.git_commit = None
485         if self.specifier is None:
486             self.specifier = 'master'
487
488     def checkout(self, target):
489         flags = ''
490         redirect = ''
491         if globals.quiet:
492             flags = '-q'
493             redirect = '>/dev/null'
494         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
495         os.chdir('%s/src/%s' % (target.directory, self.name))
496         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
497         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
498         command('git submodule init --quiet')
499         command('git submodule update --quiet')
500         os.chdir(self.directory)
501
502         proj = '%s/src/%s/%s' % (target.directory, self.name, self.directory)
503
504         self.read_cscript('%s/cscript' % proj)
505         
506         if os.path.exists('%s/wscript' % proj):
507             v = read_wscript_variable(proj, "VERSION");
508             if v is not None:
509                 self.version = Version(v)
510
511     def read_cscript(self, s):
512         self.cscript = {}
513         execfile(s, self.cscript)
514
515 def set_version_in_wscript(version):
516     f = open('wscript', 'rw')
517     o = open('wscript.tmp', 'w')
518     while 1:
519         l = f.readline()
520         if l == '':
521             break
522
523         s = l.split()
524         if len(s) == 3 and s[0] == "VERSION":
525             print "Writing %s" % version
526             print >>o,"VERSION = '%s'" % version
527         else:
528             print >>o,l,
529     f.close()
530     o.close()
531
532     os.rename('wscript.tmp', 'wscript')
533
534 def append_version_to_changelog(version):
535     try:
536         f = open('ChangeLog', 'r')
537     except:
538         log('Could not open ChangeLog')
539         return
540
541     c = f.read()
542     f.close()
543
544     f = open('ChangeLog', 'w')
545     now = datetime.datetime.now()
546     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))
547     f.write(c)
548
549 def append_version_to_debian_changelog(version):
550     if not os.path.exists('debian'):
551         log('Could not find debian directory')
552         return
553
554     command('dch -b -v %s-1 "New upstream release."' % version)
555
556 def devel_to_git(project, filename):
557     if project.git_commit is not None:
558         filename = filename.replace('devel', '-%s' % project.git_commit)
559     return filename
560
561
562 #
563 # Command-line parser
564 #
565
566 def main():
567     parser = argparse.ArgumentParser()
568     parser.add_argument('command')
569     parser.add_argument('-p', '--project', help='project name')
570     parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
571     parser.add_argument('--minor', help='minor version number bump', action='store_true')
572     parser.add_argument('--micro', help='micro version number bump', action='store_true')
573     parser.add_argument('--major', help='major version to return with latest', type=int)
574     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
575     parser.add_argument('-o', '--output', help='output directory', default='.')
576     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
577     parser.add_argument('-t', '--target', help='target')
578     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
579     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
580     parser.add_argument('-w', '--work', help='override default work directory')
581     args = parser.parse_args()
582
583     args.output = os.path.abspath(args.output)
584     if args.work is not None:
585         args.work = os.path.abspath(args.work)
586
587     if args.project is None and args.command != 'shell':
588         raise Error('you must specify -p or --project')
589         
590     globals.quiet = args.quiet
591     globals.command = args.command
592
593     project = Project(args.project, args.directory, args.checkout)
594
595     commands = ['build', 'package', 'release', 'pot', 'changelog', 'manual', 'doxygen', 'latest', 'test', 'shell', 'revision']
596     if globals.command not in commands:
597         e = 'command must be one of: '
598         for c in commands:
599             e += '%s ' % c
600         raise Error(e)
601
602     if globals.command == 'build':
603         if args.target is None:
604             raise Error('you must specify -t or --target')
605
606         target = target_factory(args.target, args.debug, args.work)
607         project.checkout(target)
608         target.build_dependencies(project)
609         target.build(project)
610         if not args.keep:
611             target.cleanup()
612
613     elif globals.command == 'package':
614         if args.target is None:
615             raise Error('you must specify -t or --target')
616
617         target = target_factory(args.target, args.debug, args.work)
618
619         packages = target.package(project)
620         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
621             packages = [packages]
622
623         if target.platform == 'linux':
624             out = '%s/%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
625             try:
626                 os.makedirs(out)
627             except:
628                 pass
629             for p in packages:
630                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(project, p))))
631         else:
632             for p in packages:
633                 copyfile(p, '%s/%s' % (args.output, os.path.basename(devel_to_git(project, p))))
634
635         if not args.keep:
636             target.cleanup()
637
638     elif globals.command == 'release':
639         if args.minor is False and args.micro is False:
640             raise Error('you must specify --minor or --micro')
641
642         target = SourceTarget()
643         project.checkout(target)
644
645         version = project.version
646         version.to_release()
647         if args.minor:
648             version.bump_minor()
649         else:
650             version.bump_micro()
651
652         set_version_in_wscript(version)
653         append_version_to_changelog(version)
654         append_version_to_debian_changelog(version)
655
656         command('git commit -a -m "Bump version"')
657         command('git tag -m "v%s" v%s' % (version, version))
658
659         version.to_devel()
660         set_version_in_wscript(version)
661         command('git commit -a -m "Bump version"')
662         command('git push')
663         command('git push --tags')
664
665         target.cleanup()
666
667     elif globals.command == 'pot':
668         target = SourceTarget()
669         project.checkout(target)
670
671         pots = project.cscript['make_pot'](target)
672         for p in pots:
673             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
674
675         target.cleanup()
676
677     elif globals.command == 'changelog':
678         target = SourceTarget()
679         project.checkout(target)
680
681         text = open('ChangeLog', 'r')
682         html = open('%s/changelog.html' % args.output, 'w')
683         versions = 8
684
685         last = None
686         changes = []
687
688         while 1:
689             l = text.readline()
690             if l == '':
691                 break
692
693             if len(l) > 0 and l[0] == "\t":
694                 s = l.split()
695                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
696                     v = Version(s[2])
697                     if v.micro == 0:
698                         if last is not None and len(changes) > 0:
699                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
700                             print >>html,"<ul>"
701                             for c in changes:
702                                 print >>html,"<li>%s" % c
703                             print >>html,"</ul>"
704                         last = s[2]
705                         changes = []
706                         versions -= 1
707                         if versions < 0:
708                             break
709                 else:
710                     c = l.strip()
711                     if len(c) > 0:
712                         if c[0] == '*':
713                             changes.append(c[2:])
714                         else:
715                             changes[-1] += " " + c
716
717         target.cleanup()
718
719     elif globals.command == 'manual':
720         target = SourceTarget()
721         project.checkout(target)
722
723         outs = project.cscript['make_manual'](target)
724         for o in outs:
725             if os.path.isfile(o):
726                 copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
727             else:
728                 copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
729
730         target.cleanup()
731
732     elif globals.command == 'doxygen':
733         target = SourceTarget()
734         project.checkout(target)
735
736         dirs = project.cscript['make_doxygen'](target)
737         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
738             dirs = [dirs]
739
740         for d in dirs:
741             copytree(d, '%s/%s' % (args.output, 'doc'))
742
743         target.cleanup()
744
745     elif globals.command == 'latest':
746         target = SourceTarget()
747         project.checkout(target)
748
749         f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
750         latest = None
751         while latest is None:
752             t = f.readline()
753             m = re.compile(".*\((.*)\).*").match(t)
754             if m:
755                 tags = m.group(1).split(', ')
756                 for t in tags:
757                     s = t.split()
758                     if len(s) > 1:
759                         t = s[1]
760                     if len(t) > 0 and t[0] == 'v':
761                         v = Version(t[1:])
762                         if args.major is None or v.major == args.major:
763                             latest = v
764
765         print latest
766         target.cleanup()
767
768     elif globals.command == 'test':
769         if args.target is None:
770             raise Error('you must specify -t or --target')
771
772         target = None
773         try:
774             target = target_factory(args.target, args.debug, args.work)
775             target.test(project)
776         except Error as e:
777             if target is not None:
778                 target.cleanup()
779             raise
780
781         if target is not None:
782             target.cleanup()
783
784     elif globals.command == 'shell':
785         if args.target is None:
786             raise Error('you must specify -t or --target')
787
788         target = target_factory(args.target, args.debug, args.work)
789         target.command('bash')
790
791     elif globals.command == 'revision':
792
793         target = SourceTarget()
794         project.checkout(target)
795         print command_and_read('git rev-parse HEAD').readline().strip()[:7]
796         target.cleanup()
797
798     else:
799         raise Error('invalid command %s' % globals.command)
800
801 try:
802     main()
803 except Error as e:
804     print >>sys.stderr,'cdist: %s' % str(e)
805     sys.exit(1)