Fix previous.
[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\'"')
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
616     # Now, args.output is 'host:' or 'path/'
617
618     if args.work is not None:
619         args.work = os.path.abspath(args.work)
620
621     if args.project is None and args.command != 'shell':
622         raise Error('you must specify -p or --project')
623         
624     globals.quiet = args.quiet
625     globals.command = args.command
626
627     project = Project(args.project, args.directory, args.checkout)
628
629     if not globals.command in commands:
630         e = 'command must be one of:\n' + one_of
631         raise Error('command must be one of:\n%s' % one_of)
632
633     if globals.command == 'build':
634         if args.target is None:
635             raise Error('you must specify -t or --target')
636
637         target = target_factory(args.target, args.debug, args.work)
638         project.checkout(target)
639         target.build_dependencies(project)
640         target.build(project)
641         if not args.keep:
642             target.cleanup()
643
644     elif globals.command == 'package':
645         if args.target is None:
646             raise Error('you must specify -t or --target')
647
648         target = target_factory(args.target, args.debug, args.work)
649
650         packages = target.package(project)
651         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
652             packages = [packages]
653
654         if target.platform == 'linux':
655             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
656             try:
657                 os.makedirs(out)
658             except:
659                 pass
660             for p in packages:
661                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(project, p))))
662         else:
663             for p in packages:
664                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(project, p))))
665
666         if not args.keep:
667             target.cleanup()
668
669     elif globals.command == 'release':
670         if args.minor is False and args.micro is False:
671             raise Error('you must specify --minor or --micro')
672
673         target = SourceTarget()
674         project.checkout(target)
675
676         version = project.version
677         version.to_release()
678         if args.minor:
679             version.bump_minor()
680         else:
681             version.bump_micro()
682
683         set_version_in_wscript(version)
684         append_version_to_changelog(version)
685         append_version_to_debian_changelog(version)
686
687         command('git commit -a -m "Bump version"')
688         command('git tag -m "v%s" v%s' % (version, version))
689
690         version.to_devel()
691         set_version_in_wscript(version)
692         command('git commit -a -m "Bump version"')
693         command('git push')
694         command('git push --tags')
695
696         target.cleanup()
697
698     elif globals.command == 'pot':
699         target = SourceTarget()
700         project.checkout(target)
701
702         pots = project.cscript['make_pot'](target)
703         for p in pots:
704             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
705
706         target.cleanup()
707
708     elif globals.command == 'changelog':
709         target = SourceTarget()
710         project.checkout(target)
711
712         text = open('ChangeLog', 'r')
713         html = tempfile.NamedTemporaryFile()
714         versions = 8
715
716         last = None
717         changes = []
718
719         while True:
720             l = text.readline()
721             if l == '':
722                 break
723
724             if len(l) > 0 and l[0] == "\t":
725                 s = l.split()
726                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
727                     v = Version(s[2])
728                     if v.micro == 0:
729                         if last is not None and len(changes) > 0:
730                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
731                             print >>html,"<ul>"
732                             for c in changes:
733                                 print >>html,"<li>%s" % c
734                             print >>html,"</ul>"
735                         last = s[2]
736                         changes = []
737                         versions -= 1
738                         if versions < 0:
739                             break
740                 else:
741                     c = l.strip()
742                     if len(c) > 0:
743                         if c[0] == '*':
744                             changes.append(c[2:])
745                         else:
746                             changes[-1] += " " + c
747
748         copyfile(html.file, '%schangelog.html' % args.output)
749         html.close()
750         target.cleanup()
751
752     elif globals.command == 'manual':
753         target = SourceTarget()
754         project.checkout(target)
755
756         outs = project.cscript['make_manual'](target)
757         for o in outs:
758             if os.path.isfile(o):
759                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
760             else:
761                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
762
763         target.cleanup()
764
765     elif globals.command == 'doxygen':
766         target = SourceTarget()
767         project.checkout(target)
768
769         dirs = project.cscript['make_doxygen'](target)
770         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
771             dirs = [dirs]
772
773         for d in dirs:
774             copytree(d, '%s%s' % (args.output, 'doc'))
775
776         target.cleanup()
777
778     elif globals.command == 'latest':
779         target = SourceTarget()
780         project.checkout(target)
781
782         f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
783         latest = None
784         while latest is None:
785             t = f.readline()
786             m = re.compile(".*\((.*)\).*").match(t)
787             if m:
788                 tags = m.group(1).split(', ')
789                 for t in tags:
790                     s = t.split()
791                     if len(s) > 1:
792                         t = s[1]
793                     if len(t) > 0 and t[0] == 'v':
794                         v = Version(t[1:])
795                         if args.major is None or v.major == args.major:
796                             latest = v
797
798         print latest
799         target.cleanup()
800
801     elif globals.command == 'test':
802         if args.target is None:
803             raise Error('you must specify -t or --target')
804
805         target = None
806         try:
807             target = target_factory(args.target, args.debug, args.work)
808             target.test(project)
809         except Error as e:
810             if target is not None:
811                 target.cleanup()
812             raise
813
814         if target is not None:
815             target.cleanup()
816
817     elif globals.command == 'shell':
818         if args.target is None:
819             raise Error('you must specify -t or --target')
820
821         target = target_factory(args.target, args.debug, args.work)
822         target.command('bash')
823
824     elif globals.command == 'revision':
825
826         target = SourceTarget()
827         project.checkout(target)
828         print command_and_read('git rev-parse HEAD').readline().strip()[:7]
829         target.cleanup()
830
831     else:
832         raise Error('invalid command %s' % globals.command)
833
834 try:
835     main()
836 except Error as e:
837     print >>sys.stderr,'cdist: %s' % str(e)
838     sys.exit(1)