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