Use scp to copy files.
[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 True:
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     os.command('scp -r %s %s' % (a, b))
118
119 def copyfile(a, b):
120     log('copy %s -> %s' % (a, b))
121     os.command('scp %s %s' % (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 True:
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 True:
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
568     commands = {
569         "build": "build project",
570         "package": "package and build project",
571         "release": "release a project using its next version number (changing wscript and tagging)",
572         "pot": "build the project's .pot files",
573         "changelog": "generate a simple HTML changelog",
574         "manual": "build the project's manual",
575         "doxygen": "build the project's Doxygen documentation",
576         "latest": "print out the latest version",
577         "test": "run the project's unit tests",
578         "shell": "build the project then start a shell in its chroot",
579         "revision": "print the head git revision number"
580     }
581
582     one_of = "Command is one of:\n"
583     summary = ""
584     for k, v in commands.iteritems():
585         one_of += "\t%s\t%s\n" % (k, v)
586         summary += k + " "
587
588     parser = argparse.ArgumentParser()
589     parser.add_argument('command', help=summary)
590     parser.add_argument('-p', '--project', help='project name')
591     parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
592     parser.add_argument('--minor', help='minor version number bump', action='store_true')
593     parser.add_argument('--micro', help='micro version number bump', action='store_true')
594     parser.add_argument('--major', help='major version to return with latest', type=int)
595     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
596     parser.add_argument('-o', '--output', help='output directory', default='.')
597     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
598     parser.add_argument('-t', '--target', help='target')
599     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
600     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
601     parser.add_argument('-w', '--work', help='override default work directory')
602     args = parser.parse_args()
603
604     args.output = os.path.abspath(args.output)
605     if args.work is not None:
606         args.work = os.path.abspath(args.work)
607
608     if args.project is None and args.command != 'shell':
609         raise Error('you must specify -p or --project')
610         
611     globals.quiet = args.quiet
612     globals.command = args.command
613
614     project = Project(args.project, args.directory, args.checkout)
615
616     if not globals.command in commands:
617         e = 'command must be one of:\n' + one_of
618         raise Error('command must be one of:\n%s' % one_of)
619
620     if globals.command == 'build':
621         if args.target is None:
622             raise Error('you must specify -t or --target')
623
624         target = target_factory(args.target, args.debug, args.work)
625         project.checkout(target)
626         target.build_dependencies(project)
627         target.build(project)
628         if not args.keep:
629             target.cleanup()
630
631     elif globals.command == 'package':
632         if args.target is None:
633             raise Error('you must specify -t or --target')
634
635         target = target_factory(args.target, args.debug, args.work)
636
637         packages = target.package(project)
638         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
639             packages = [packages]
640
641         if target.platform == 'linux':
642             out = '%s/%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
643             try:
644                 os.makedirs(out)
645             except:
646                 pass
647             for p in packages:
648                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(project, p))))
649         else:
650             for p in packages:
651                 copyfile(p, '%s/%s' % (args.output, os.path.basename(devel_to_git(project, p))))
652
653         if not args.keep:
654             target.cleanup()
655
656     elif globals.command == 'release':
657         if args.minor is False and args.micro is False:
658             raise Error('you must specify --minor or --micro')
659
660         target = SourceTarget()
661         project.checkout(target)
662
663         version = project.version
664         version.to_release()
665         if args.minor:
666             version.bump_minor()
667         else:
668             version.bump_micro()
669
670         set_version_in_wscript(version)
671         append_version_to_changelog(version)
672         append_version_to_debian_changelog(version)
673
674         command('git commit -a -m "Bump version"')
675         command('git tag -m "v%s" v%s' % (version, version))
676
677         version.to_devel()
678         set_version_in_wscript(version)
679         command('git commit -a -m "Bump version"')
680         command('git push')
681         command('git push --tags')
682
683         target.cleanup()
684
685     elif globals.command == 'pot':
686         target = SourceTarget()
687         project.checkout(target)
688
689         pots = project.cscript['make_pot'](target)
690         for p in pots:
691             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
692
693         target.cleanup()
694
695     elif globals.command == 'changelog':
696         target = SourceTarget()
697         project.checkout(target)
698
699         text = open('ChangeLog', 'r')
700         html = tempfile.NamedTemporaryFile()
701         versions = 8
702
703         last = None
704         changes = []
705
706         while True:
707             l = text.readline()
708             if l == '':
709                 break
710
711             if len(l) > 0 and l[0] == "\t":
712                 s = l.split()
713                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
714                     v = Version(s[2])
715                     if v.micro == 0:
716                         if last is not None and len(changes) > 0:
717                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
718                             print >>html,"<ul>"
719                             for c in changes:
720                                 print >>html,"<li>%s" % c
721                             print >>html,"</ul>"
722                         last = s[2]
723                         changes = []
724                         versions -= 1
725                         if versions < 0:
726                             break
727                 else:
728                     c = l.strip()
729                     if len(c) > 0:
730                         if c[0] == '*':
731                             changes.append(c[2:])
732                         else:
733                             changes[-1] += " " + c
734
735         copyfile(html.file, '%s/changelog.html' % args.output)
736         html.close()
737         target.cleanup()
738
739     elif globals.command == 'manual':
740         target = SourceTarget()
741         project.checkout(target)
742
743         outs = project.cscript['make_manual'](target)
744         for o in outs:
745             if os.path.isfile(o):
746                 copyfile(o, '%s/%s' % (args.output, os.path.basename(o)))
747             else:
748                 copytree(o, '%s/%s' % (args.output, os.path.basename(o)))
749
750         target.cleanup()
751
752     elif globals.command == 'doxygen':
753         target = SourceTarget()
754         project.checkout(target)
755
756         dirs = project.cscript['make_doxygen'](target)
757         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
758             dirs = [dirs]
759
760         for d in dirs:
761             copytree(d, '%s/%s' % (args.output, 'doc'))
762
763         target.cleanup()
764
765     elif globals.command == 'latest':
766         target = SourceTarget()
767         project.checkout(target)
768
769         f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
770         latest = None
771         while latest is None:
772             t = f.readline()
773             m = re.compile(".*\((.*)\).*").match(t)
774             if m:
775                 tags = m.group(1).split(', ')
776                 for t in tags:
777                     s = t.split()
778                     if len(s) > 1:
779                         t = s[1]
780                     if len(t) > 0 and t[0] == 'v':
781                         v = Version(t[1:])
782                         if args.major is None or v.major == args.major:
783                             latest = v
784
785         print latest
786         target.cleanup()
787
788     elif globals.command == 'test':
789         if args.target is None:
790             raise Error('you must specify -t or --target')
791
792         target = None
793         try:
794             target = target_factory(args.target, args.debug, args.work)
795             target.test(project)
796         except Error as e:
797             if target is not None:
798                 target.cleanup()
799             raise
800
801         if target is not None:
802             target.cleanup()
803
804     elif globals.command == 'shell':
805         if args.target is None:
806             raise Error('you must specify -t or --target')
807
808         target = target_factory(args.target, args.debug, args.work)
809         target.command('bash')
810
811     elif globals.command == 'revision':
812
813         target = SourceTarget()
814         project.checkout(target)
815         print command_and_read('git rev-parse HEAD').readline().strip()[:7]
816         target.cleanup()
817
818     else:
819         raise Error('invalid command %s' % globals.command)
820
821 try:
822     main()
823 except Error as e:
824     print >>sys.stderr,'cdist: %s' % str(e)
825     sys.exit(1)