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