Store output from doxygen in output_dir, not output_dir/doc.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program; if not, write to the Free Software
17 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 import os
20 import sys
21 import shutil
22 import glob
23 import tempfile
24 import argparse
25 import datetime
26 import subprocess
27 import re
28 import copy
29 import inspect
30
31 TEMPORARY_DIRECTORY = '/tmp'
32
33 class Error(Exception):
34     def __init__(self, value):
35         self.value = value
36     def __str__(self):
37         return self.value
38     def __repr__(self):
39         return str(self)
40
41 class Trees:
42     """
43     Store for Tree objects which re-uses already-created objects
44     and checks for requests for different versions of the same thing.
45     """
46
47     def __init__(self):
48         self.trees = []
49
50     def get(self, name, specifier, target):
51         for t in self.trees:
52             if t.name == name and t.specifier == specifier and t.target == target:
53                 return t
54             elif t.name == name and t.specifier != specifier:
55                 raise Error('conflicting versions of %s requested (%s and %s)' % (name, specifier, t.specifier))
56
57         nt = Tree(name, specifier, target)
58         self.trees.append(nt)
59         return nt
60
61 class Globals:
62     quiet = False
63     command = None
64     trees = Trees()
65
66 globals = Globals()
67
68
69 #
70 # Configuration
71 #
72
73 class Option(object):
74     def __init__(self, key, default=None):
75         self.key = key
76         self.value = default
77
78     def offer(self, key, value):
79         if key == self.key:
80             self.value = value
81
82 class BoolOption(object):
83     def __init__(self, key):
84         self.key = key
85         self.value = False
86
87     def offer(self, key, value):
88         if key == self.key:
89             self.value = (value == 'yes' or value == '1' or value == 'true')
90
91 class Config:
92     def __init__(self):
93         self.options = [ Option('linux_chroot_prefix'),
94                          Option('windows_environment_prefix'),
95                          Option('mingw_prefix'),
96                          Option('git_prefix'),
97                          Option('osx_build_host'),
98                          Option('osx_environment_prefix'),
99                          Option('osx_sdk_prefix'),
100                          Option('osx_sdk'),
101                          Option('parallel', 4) ]
102
103         try:
104             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
105             while True:
106                 l = f.readline()
107                 if l == '':
108                     break
109
110                 if len(l) > 0 and l[0] == '#':
111                     continue
112
113                 s = l.strip().split()
114                 if len(s) == 2:
115                     for k in self.options:
116                         k.offer(s[0], s[1])
117         except:
118             raise
119
120     def get(self, k):
121         for o in self.options:
122             if o.key == k:
123                 return o.value
124
125         raise Error('Required setting %s not found' % k)
126
127 config = Config()
128
129 #
130 # Utility bits
131
132
133 def log(m):
134     if not globals.quiet:
135         print '\x1b[33m* %s\x1b[0m' % m
136
137 def scp_escape(n):
138     s = n.split(':')
139     assert(len(s) == 1 or len(s) == 2)
140     if len(s) == 2:
141         return '%s:"\'%s\'"' % (s[0], s[1])
142     else:
143         return '\"%s\"' % s[0]
144
145 def copytree(a, b):
146     log('copy %s -> %s' % (scp_escape(b), scp_escape(b)))
147     command('scp -r %s %s' % (scp_escape(a), scp_escape(b)))
148
149 def copyfile(a, b):
150     log('copy %s -> %s' % (scp_escape(a), scp_escape(b)))
151     command('scp %s %s' % (scp_escape(a), scp_escape(b)))
152
153 def makedirs(d):
154     if d.find(':') == -1:
155         os.makedirs(d)
156     else:
157         s = d.split(':')
158         command('ssh %s -- mkdir -p %s' % (s[0], s[1]))
159
160 def rmdir(a):
161     log('remove %s' % a)
162     os.rmdir(a)
163
164 def rmtree(a):
165     log('remove %s' % a)
166     shutil.rmtree(a, ignore_errors=True)
167
168 def command(c):
169     log(c)
170     r = os.system(c)
171     if (r >> 8):
172         raise Error('command %s failed' % c)
173
174 def command_and_read(c):
175     log(c)
176     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
177     f = os.fdopen(os.dup(p.stdout.fileno()))
178     return f
179
180 def read_wscript_variable(directory, variable):
181     f = open('%s/wscript' % directory, 'r')
182     while True:
183         l = f.readline()
184         if l == '':
185             break
186         
187         s = l.split()
188         if len(s) == 3 and s[0] == variable:
189             f.close()
190             return s[2][1:-1]
191
192     f.close()
193     return None
194
195 def set_version_in_wscript(version):
196     f = open('wscript', 'rw')
197     o = open('wscript.tmp', 'w')
198     while True:
199         l = f.readline()
200         if l == '':
201             break
202
203         s = l.split()
204         if len(s) == 3 and s[0] == "VERSION":
205             print "Writing %s" % version
206             print >>o,"VERSION = '%s'" % version
207         else:
208             print >>o,l,
209     f.close()
210     o.close()
211
212     os.rename('wscript.tmp', 'wscript')
213
214 def append_version_to_changelog(version):
215     try:
216         f = open('ChangeLog', 'r')
217     except:
218         log('Could not open ChangeLog')
219         return
220
221     c = f.read()
222     f.close()
223
224     f = open('ChangeLog', 'w')
225     now = datetime.datetime.now()
226     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))
227     f.write(c)
228
229 def append_version_to_debian_changelog(version):
230     if not os.path.exists('debian'):
231         log('Could not find debian directory')
232         return
233
234     command('dch -b -v %s-1 "New upstream release."' % version)
235
236 def devel_to_git(git_commit, filename):
237     if git_commit is not None:
238         filename = filename.replace('devel', '-%s' % git_commit)
239     return filename
240
241 class TreeDirectory:
242     def __init__(self, tree):
243         self.tree = tree
244     def __enter__(self):
245         self.cwd = os.getcwd()
246         os.chdir('%s/src/%s' % (self.tree.target.directory, self.tree.name))
247     def __exit__(self, type, value, traceback):
248         os.chdir(self.cwd)
249
250 #
251 # Version
252 #
253
254 class Version:
255     def __init__(self, s):
256         self.devel = False
257
258         if s.startswith("'"):
259             s = s[1:]
260         if s.endswith("'"):
261             s = s[0:-1]
262         
263         if s.endswith('devel'):
264             s = s[0:-5]
265             self.devel = True
266
267         if s.endswith('pre'):
268             s = s[0:-3]
269
270         p = s.split('.')
271         self.major = int(p[0])
272         self.minor = int(p[1])
273         if len(p) == 3:
274             self.micro = int(p[2])
275         else:
276             self.micro = 0
277
278     def bump_minor(self):
279         self.minor += 1
280         self.micro = 0
281
282     def bump_micro(self):
283         self.micro += 1
284
285     def to_devel(self):
286         self.devel = True
287
288     def to_release(self):
289         self.devel = False
290
291     def __str__(self):
292         s = '%d.%d.%d' % (self.major, self.minor, self.micro)
293         if self.devel:
294             s += 'devel'
295
296         return s
297
298 #
299 # Targets
300 #
301
302 class Target(object):
303     """
304     platform -- platform string (e.g. 'windows', 'linux', 'osx')
305     directory -- directory to work in; if None we will use a temporary directory
306     Temporary directories will be removed after use; specified directories will not.
307     """
308     def __init__(self, platform, directory=None):
309         self.platform = platform
310         self.parallel = int(config.get('parallel'))
311
312         # self.directory is the working directory
313         if directory is None:
314             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
315             self.rmdir = True
316         else:
317             self.directory = directory
318             self.rmdir = False
319
320         # Environment variables that we will use when we call cscripts
321         self.variables = {}
322         self.debug = False
323
324     def package(self, project, checkout):
325         tree = globals.trees.get(project, checkout, self)
326         tree.build_dependencies()
327         tree.build(tree)
328         return tree.call('package', tree.version), tree.git_commit
329
330     def test(self, tree):
331         tree.build_dependencies()
332         tree.build()
333         return tree.call('test')
334
335     def set(self, a, b):
336         self.variables[a] = b
337
338     def unset(self, a):
339         del(self.variables[a])
340
341     def get(self, a):
342         return self.variables[a]
343
344     def append_with_space(self, k, v):
345         if not k in self.variables:
346             self.variables[k] = v
347         else:
348             self.variables[k] = '%s %s' % (self.variables[k], v)
349
350     def variables_string(self, escaped_quotes=False):
351         e = ''
352         for k, v in self.variables.iteritems():
353             if escaped_quotes:
354                 v = v.replace('"', '\\"')
355             e += '%s=%s ' % (k, v)
356         return e
357
358     def cleanup(self):
359         if self.rmdir:
360             rmtree(self.directory)
361
362
363 # Windows
364 #
365
366 class WindowsTarget(Target):
367     def __init__(self, bits, directory=None):
368         super(WindowsTarget, self).__init__('windows', directory)
369         self.bits = bits
370
371         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
372         if not os.path.exists(self.windows_prefix):
373             raise Error('windows prefix %s does not exist' % self.windows_prefix)
374             
375         if self.bits == 32:
376             self.mingw_name = 'i686'
377         else:
378             self.mingw_name = 'x86_64'
379
380         self.mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
381         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
382
383         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
384         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
385         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, self.mingw_path, os.environ['PATH']))
386         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
387         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
388         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
389         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
390         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
391         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
392         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
393         for p in self.mingw_prefixes:
394             cxx += ' -I%s/include' % p
395             link += ' -L%s/lib' % p
396         self.set('CXXFLAGS', '"%s"' % cxx)
397         self.set('CPPFLAGS', '')
398         self.set('LINKFLAGS', '"%s"' % link)
399
400     def command(self, c):
401         log('host -> %s' % c)
402         command('%s %s' % (self.variables_string(), c))
403
404 #
405 # Linux
406 #
407
408 class LinuxTarget(Target):
409     def __init__(self, distro, version, bits, directory=None):
410         super(LinuxTarget, self).__init__('linux', directory)
411         self.distro = distro
412         self.version = version
413         self.bits = bits
414         # e.g. ubuntu-14.04-64
415         if self.version is not None and self.bits is not None:
416             self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
417         else:
418             self.chroot = self.distro
419         # e.g. /home/carl/Environments/ubuntu-14.04-64
420         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
421
422         self.set('CXXFLAGS', '-I%s/include' % self.directory)
423         self.set('CPPFLAGS', '')
424         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
425         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
426         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
427
428     def command(self, c):
429         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
430
431 #
432 # OS X
433 #
434
435 class OSXTarget(Target):
436     def __init__(self, directory=None):
437         super(OSXTarget, self).__init__('osx', directory)
438
439     def command(self, c):
440         command('%s %s' % (self.variables_string(False), c))
441
442
443 class OSXSingleTarget(OSXTarget):
444     def __init__(self, bits, directory=None):
445         super(OSXSingleTarget, self).__init__(directory)
446         self.bits = bits
447
448         if bits == 32:
449             arch = 'i386'
450         else:
451             arch = 'x86_64'
452
453         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
454         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
455
456         # Environment variables
457         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
458         self.set('CPPFLAGS', '')
459         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
460         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
461         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
462         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
463         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
464         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
465
466     def package(self, project, checkout):
467         raise Error('cannot package non-universal OS X versions')
468
469
470 class OSXUniversalTarget(OSXTarget):
471     def __init__(self, directory=None):
472         super(OSXUniversalTarget, self).__init__(directory)
473
474     def package(self, project, checkout):
475
476         for b in [32, 64]:
477             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
478             tree = globals.trees.get(project, checkout, target)
479             tree.build_dependencies()
480             tree.build()
481         
482         tree = globals.trees.get(project, checkout, self)    
483         with TreeDirectory(tree):
484             return tree.call('package', tree.version), tree.git_commit
485
486 #
487 # Source
488 #
489
490 class SourceTarget(Target):
491     def __init__(self):
492         super(SourceTarget, self).__init__('source')
493
494     def command(self, c):
495         log('host -> %s' % c)
496         command('%s %s' % (self.variables_string(), c))
497
498     def cleanup(self):
499         rmtree(self.directory)
500
501     def package(self, project, checkout):
502         tree = globals.trees.get(project, checkout, self)
503         with TreeDirectory(tree):
504             name = read_wscript_variable(os.getcwd(), 'APPNAME')
505             command('./waf dist')
506             return os.path.abspath('%s-%s.tar.bz2' % (name, tree.version)), tree.git_commit
507
508
509 # @param s Target string:
510 #       windows-{32,64}
511 #    or ubuntu-version-{32,64}
512 #    or debian-version-{32,64}
513 #    or centos-version-{32,64}
514 #    or osx-{32,64}
515 #    or source      
516 # @param debug True to build with debugging symbols (where possible)
517 def target_factory(s, debug, work):
518     target = None
519     if s.startswith('windows-'):
520         target = WindowsTarget(int(s.split('-')[1]), work)
521     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
522         p = s.split('-')
523         if len(p) != 3:
524             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
525             sys.exit(1)
526         target = LinuxTarget(p[0], p[1], int(p[2]), work)
527     elif s == 'raspbian':
528         target = LinuxTarget(s, None, None, work)
529     elif s.startswith('osx-'):
530         target = OSXSingleTarget(int(s.split('-')[1]), work)
531     elif s == 'osx':
532         if globals.command == 'build':
533             target = OSXSingleTarget(64, work)
534         else:
535             target = OSXUniversalTarget(work)
536     elif s == 'source':
537         target = SourceTarget()
538
539     if target is not None:
540         target.debug = debug
541
542     return target
543
544
545 #
546 # Tree
547 #
548  
549 class Tree(object):
550     """Description of a tree, which is a checkout of a project,
551        possibly built.  This class is never exposed to cscripts.
552        Attributes:
553            name -- name of git repository (without the .git)
554            specifier -- git tag or revision to use
555            target --- target object that we are using
556            version --- version from the wscript (if one is present)
557            git_commit -- git revision that is actually being used
558            built --- true if the tree has been built yet in this run
559     """
560
561     def __init__(self, name, specifier, target):
562         self.name = name
563         self.specifier = specifier
564         self.target = target
565         self.version = None
566         self.git_commit = None
567         self.built = False
568
569         cwd = os.getcwd()
570
571         flags = ''
572         redirect = ''
573         if globals.quiet:
574             flags = '-q'
575             redirect = '>/dev/null'
576         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
577         os.chdir('%s/src/%s' % (target.directory, self.name))
578
579         spec = self.specifier
580         if spec is None:
581             spec = 'master'
582
583         command('git checkout %s %s %s' % (flags, spec, redirect))
584         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
585         command('git submodule init --quiet')
586         command('git submodule update --quiet')
587
588         proj = '%s/src/%s' % (target.directory, self.name)
589
590         self.cscript = {}
591         execfile('%s/cscript' % proj, self.cscript)
592
593         if os.path.exists('%s/wscript' % proj):
594             v = read_wscript_variable(proj, "VERSION");
595             if v is not None:
596                 self.version = Version(v)
597
598         os.chdir(cwd)
599
600     def call(self, function, *args):
601         with TreeDirectory(self):
602             return self.cscript[function](self.target, *args)
603
604     def build_dependencies(self):
605         if 'dependencies' in self.cscript:
606             for d in self.cscript['dependencies'](self.target):
607                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
608                 dep = globals.trees.get(d[0], d[1], self.target)
609                 dep.build_dependencies()
610
611                 # Make the options to pass in from the option_defaults of the thing
612                 # we are building and any options specified by the parent.
613                 options = {}
614                 if 'option_defaults' in dep.cscript:
615                     options = dep.cscript['option_defaults']()
616                     if len(d) > 2:
617                         for k, v in d[2].iteritems():
618                             options[k] = v
619
620                 dep.build(options)
621
622     def build(self, options=None):
623         if self.built:
624             return
625
626         variables = copy.copy(self.target.variables)
627
628         if len(inspect.getargspec(self.cscript['build']).args) == 2:
629             self.call('build', options)
630         else:
631             self.call('build')
632         
633         self.target.variables = variables
634         self.built = True
635
636 #
637 # Command-line parser
638 #
639
640 def main():
641
642     commands = {
643         "build": "build project",
644         "package": "package and build project",
645         "release": "release a project using its next version number (changing wscript and tagging)",
646         "pot": "build the project's .pot files",
647         "changelog": "generate a simple HTML changelog",
648         "manual": "build the project's manual",
649         "doxygen": "build the project's Doxygen documentation",
650         "latest": "print out the latest version",
651         "test": "run the project's unit tests",
652         "shell": "build the project then start a shell in its chroot",
653         "checkout": "check out the project",
654         "revision": "print the head git revision number"
655     }
656
657     one_of = "Command is one of:\n"
658     summary = ""
659     for k, v in commands.iteritems():
660         one_of += "\t%s\t%s\n" % (k, v)
661         summary += k + " "
662
663     parser = argparse.ArgumentParser()
664     parser.add_argument('command', help=summary)
665     parser.add_argument('-p', '--project', help='project name')
666     parser.add_argument('--minor', help='minor version number bump', action='store_true')
667     parser.add_argument('--micro', help='micro version number bump', action='store_true')
668     parser.add_argument('--major', help='major version to return with latest', type=int)
669     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
670     parser.add_argument('-o', '--output', help='output directory', default='.')
671     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
672     parser.add_argument('-t', '--target', help='target')
673     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
674     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
675     parser.add_argument('-w', '--work', help='override default work directory')
676     args = parser.parse_args()
677
678     if args.output.find(':') == -1:
679         # This isn't of the form host:path so make it absolute
680         args.output = os.path.abspath(args.output) + '/'
681     else:
682         if args.output[-1] != ':' and args.output[-1] != '/':
683             args.output += '/'
684
685     # Now, args.output is 'host:', 'host:path/' or 'path/'
686
687     if args.work is not None:
688         args.work = os.path.abspath(args.work)
689
690     if args.project is None and args.command != 'shell':
691         raise Error('you must specify -p or --project')
692         
693     globals.quiet = args.quiet
694     globals.command = args.command
695
696     if not globals.command in commands:
697         e = 'command must be one of:\n' + one_of
698         raise Error('command must be one of:\n%s' % one_of)
699
700     if globals.command == 'build':
701         if args.target is None:
702             raise Error('you must specify -t or --target')
703
704         target = target_factory(args.target, args.debug, args.work)
705         tree = globals.trees.get(args.project, args.checkout, target)
706         tree.build_dependencies()
707         tree.build()
708         if not args.keep:
709             target.cleanup()
710
711     elif globals.command == 'package':
712         if args.target is None:
713             raise Error('you must specify -t or --target')
714
715         target = target_factory(args.target, args.debug, args.work)
716         packages, git_commit = target.package(args.project, args.checkout)
717         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
718             packages = [packages]
719
720         if target.platform == 'linux':
721             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
722             try:
723                 makedirs(out)
724             except:
725                 pass
726             for p in packages:
727                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
728         else:
729             try:
730                 makedirs(args.output)
731             except:
732                 pass
733             for p in packages:
734                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
735
736         if not args.keep:
737             target.cleanup()
738
739     elif globals.command == 'release':
740         if args.minor is False and args.micro is False:
741             raise Error('you must specify --minor or --micro')
742
743         target = SourceTarget()
744         tree = globals.trees.get(args.project, args.checkout, target)
745
746         version = tree.version
747         version.to_release()
748         if args.minor:
749             version.bump_minor()
750         else:
751             version.bump_micro()
752
753         set_version_in_wscript(version)
754         append_version_to_changelog(version)
755         append_version_to_debian_changelog(version)
756
757         command('git commit -a -m "Bump version"')
758         command('git tag -m "v%s" v%s' % (version, version))
759
760         version.to_devel()
761         set_version_in_wscript(version)
762         command('git commit -a -m "Bump version"')
763         command('git push')
764         command('git push --tags')
765
766         target.cleanup()
767
768     elif globals.command == 'pot':
769         target = SourceTarget()
770         tree = globals.trees.get(args.project, args.checkout, target)
771
772         pots = tree.call('make_pot')
773         for p in pots:
774             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
775
776         target.cleanup()
777
778     elif globals.command == 'changelog':
779         target = SourceTarget()
780         tree = globals.trees.get(args.project, args.checkout, target)
781
782         with TreeDirectory(tree):
783             text = open('ChangeLog', 'r')
784
785         html = tempfile.NamedTemporaryFile()
786         versions = 8
787
788         last = None
789         changes = []
790
791         while True:
792             l = text.readline()
793             if l == '':
794                 break
795
796             if len(l) > 0 and l[0] == "\t":
797                 s = l.split()
798                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
799                     v = Version(s[2])
800                     if v.micro == 0:
801                         if last is not None and len(changes) > 0:
802                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
803                             print >>html,"<ul>"
804                             for c in changes:
805                                 print >>html,"<li>%s" % c
806                             print >>html,"</ul>"
807                         last = s[2]
808                         changes = []
809                         versions -= 1
810                         if versions < 0:
811                             break
812                 else:
813                     c = l.strip()
814                     if len(c) > 0:
815                         if c[0] == '*':
816                             changes.append(c[2:])
817                         else:
818                             changes[-1] += " " + c
819
820         copyfile(html.file, '%schangelog.html' % args.output)
821         html.close()
822         target.cleanup()
823
824     elif globals.command == 'manual':
825         target = SourceTarget()
826         tree = globals.trees.get(args.project, args.checkout, target)
827
828         outs = tree.call('make_manual')
829         for o in outs:
830             if os.path.isfile(o):
831                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
832             else:
833                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
834
835         target.cleanup()
836
837     elif globals.command == 'doxygen':
838         target = SourceTarget()
839         tree = globals.trees.get(args.project, args.checkout, target)
840
841         dirs = tree.call('make_doxygen')
842         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
843             dirs = [dirs]
844
845         for d in dirs:
846             copytree(d, args.output)
847
848         target.cleanup()
849
850     elif globals.command == 'latest':
851         target = SourceTarget()
852         tree = globals.trees.get(args.project, args.checkout, target)
853
854         with TreeDirectory(tree):
855             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
856             latest = None
857             while latest is None:
858                 t = f.readline()
859                 m = re.compile(".*\((.*)\).*").match(t)
860                 if m:
861                     tags = m.group(1).split(', ')
862                     for t in tags:
863                         s = t.split()
864                         if len(s) > 1:
865                             t = s[1]
866                         if len(t) > 0 and t[0] == 'v':
867                             v = Version(t[1:])
868                             if args.major is None or v.major == args.major:
869                                 latest = v
870
871         print latest
872         target.cleanup()
873
874     elif globals.command == 'test':
875         if args.target is None:
876             raise Error('you must specify -t or --target')
877
878         target = None
879         try:
880             target = target_factory(args.target, args.debug, args.work)
881             tree = globals.trees.get(args.project, args.checkout, target)
882             with TreeDirectory(tree):
883                 target.test(tree)
884         except Error as e:
885             if target is not None:
886                 target.cleanup()
887             raise
888
889         if target is not None:
890             target.cleanup()
891
892     elif globals.command == 'shell':
893         if args.target is None:
894             raise Error('you must specify -t or --target')
895
896         target = target_factory(args.target, args.debug, args.work)
897         target.command('bash')
898
899     elif globals.command == 'revision':
900
901         target = SourceTarget()
902         tree = globals.trees.get(args.project, args.checkout, target)
903         with TreeDirectory(tree):
904             print command_and_read('git rev-parse HEAD').readline().strip()[:7]
905         target.cleanup()
906
907     elif globals.command == 'checkout':
908
909         if args.output is None:
910             raise Error('you must specify -o or --output')
911
912         target = SourceTarget()
913         tree = globals.trees.get(args.project, args.checkout, target)
914         with TreeDirectory(tree):
915             shutil.copytree('.', args.output)
916         target.cleanup()
917
918     else:
919         raise Error('invalid command %s' % globals.command)
920
921 try:
922     main()
923 except Error as e:
924     print >>sys.stderr,'cdist: %s' % str(e)
925     sys.exit(1)