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