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