Whitespace; try to fix append_with_space() to cope with quote-enclosed values.
[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) or len(self.variables[k]) == 0:
350             self.variables[k] = v
351         else:
352             e = self.variables[k]
353             if e[0] == '"' and e[-1] == '"':
354                 self.variables[k] = '"%s %s"' % (e[1:-1], v)
355             else:
356                 self.variables[k] = '%s %s' % (e, v)
357
358     def variables_string(self, escaped_quotes=False):
359         e = ''
360         for k, v in self.variables.iteritems():
361             if escaped_quotes:
362                 v = v.replace('"', '\\"')
363             e += '%s=%s ' % (k, v)
364         return e
365
366     def cleanup(self):
367         if self.rmdir:
368             rmtree(self.directory)
369
370 #
371 # Windows
372 #
373
374 class WindowsTarget(Target):
375     def __init__(self, bits, directory=None):
376         super(WindowsTarget, self).__init__('windows', directory)
377         self.bits = bits
378
379         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
380         if not os.path.exists(self.windows_prefix):
381             raise Error('windows prefix %s does not exist' % self.windows_prefix)
382
383         if self.bits == 32:
384             self.mingw_name = 'i686'
385         else:
386             self.mingw_name = 'x86_64'
387
388         self.mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
389         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
390
391         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
392         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
393         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, self.mingw_path, os.environ['PATH']))
394         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
395         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
396         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
397         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
398         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
399         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
400         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
401         for p in self.mingw_prefixes:
402             cxx += ' -I%s/include' % p
403             link += ' -L%s/lib' % p
404         self.set('CXXFLAGS', '"%s"' % cxx)
405         self.set('CPPFLAGS', '')
406         self.set('LINKFLAGS', '"%s"' % link)
407
408     def command(self, c):
409         log('host -> %s' % c)
410         command('%s %s' % (self.variables_string(), c))
411
412 #
413 # Linux
414 #
415
416 class LinuxTarget(Target):
417     def __init__(self, distro, version, bits, directory=None):
418         super(LinuxTarget, self).__init__('linux', directory)
419         self.distro = distro
420         self.version = version
421         self.bits = bits
422         # e.g. ubuntu-14.04-64
423         if self.version is not None and self.bits is not None:
424             self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
425         else:
426             self.chroot = self.distro
427         # e.g. /home/carl/Environments/ubuntu-14.04-64
428         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
429
430         self.set('CXXFLAGS', '-I%s/include' % self.directory)
431         self.set('CPPFLAGS', '')
432         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
433         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
434         self.set('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin')
435
436     def command(self, c):
437         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
438
439 #
440 # OS X
441 #
442
443 class OSXTarget(Target):
444     def __init__(self, directory=None):
445         super(OSXTarget, self).__init__('osx', directory)
446
447     def command(self, c):
448         command('%s %s' % (self.variables_string(False), c))
449
450
451 class OSXSingleTarget(OSXTarget):
452     def __init__(self, bits, directory=None):
453         super(OSXSingleTarget, self).__init__(directory)
454         self.bits = bits
455
456         if bits == 32:
457             arch = 'i386'
458         else:
459             arch = 'x86_64'
460
461         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
462         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
463
464         # Environment variables
465         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
466         self.set('CPPFLAGS', '')
467         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
468         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
469         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
470         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
471         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
472         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
473
474     def package(self, project, checkout):
475         raise Error('cannot package non-universal OS X versions')
476
477
478 class OSXUniversalTarget(OSXTarget):
479     def __init__(self, directory=None):
480         super(OSXUniversalTarget, self).__init__(directory)
481
482     def package(self, project, checkout):
483
484         for b in [32, 64]:
485             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
486             tree = globals.trees.get(project, checkout, target)
487             tree.build_dependencies()
488             tree.build()
489
490         tree = globals.trees.get(project, checkout, self)
491         with TreeDirectory(tree):
492             return tree.call('package', tree.version), tree.git_commit
493
494 #
495 # Source
496 #
497
498 class SourceTarget(Target):
499     def __init__(self):
500         super(SourceTarget, self).__init__('source')
501
502     def command(self, c):
503         log('host -> %s' % c)
504         command('%s %s' % (self.variables_string(), c))
505
506     def cleanup(self):
507         rmtree(self.directory)
508
509     def package(self, project, checkout):
510         tree = globals.trees.get(project, checkout, self)
511         with TreeDirectory(tree):
512             name = read_wscript_variable(os.getcwd(), 'APPNAME')
513             command('./waf dist')
514             return os.path.abspath('%s-%s.tar.bz2' % (name, tree.version)), tree.git_commit
515
516
517 # @param s Target string:
518 #       windows-{32,64}
519 #    or ubuntu-version-{32,64}
520 #    or debian-version-{32,64}
521 #    or centos-version-{32,64}
522 #    or osx-{32,64}
523 #    or source
524 # @param debug True to build with debugging symbols (where possible)
525 def target_factory(s, debug, work):
526     target = None
527     if s.startswith('windows-'):
528         target = WindowsTarget(int(s.split('-')[1]), work)
529     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
530         p = s.split('-')
531         if len(p) != 3:
532             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
533             sys.exit(1)
534         target = LinuxTarget(p[0], p[1], int(p[2]), work)
535     elif s == 'raspbian':
536         target = LinuxTarget(s, None, None, work)
537     elif s.startswith('osx-'):
538         target = OSXSingleTarget(int(s.split('-')[1]), work)
539     elif s == 'osx':
540         if globals.command == 'build':
541             target = OSXSingleTarget(64, work)
542         else:
543             target = OSXUniversalTarget(work)
544     elif s == 'source':
545         target = SourceTarget()
546
547     if target is not None:
548         target.debug = debug
549
550     return target
551
552
553 #
554 # Tree
555 #
556
557 class Tree(object):
558     """Description of a tree, which is a checkout of a project,
559        possibly built.  This class is never exposed to cscripts.
560        Attributes:
561            name -- name of git repository (without the .git)
562            specifier -- git tag or revision to use
563            target --- target object that we are using
564            version --- version from the wscript (if one is present)
565            git_commit -- git revision that is actually being used
566            built --- true if the tree has been built yet in this run
567     """
568
569     def __init__(self, name, specifier, target):
570         self.name = name
571         self.specifier = specifier
572         self.target = target
573         self.version = None
574         self.git_commit = None
575         self.built = False
576
577         cwd = os.getcwd()
578
579         flags = ''
580         redirect = ''
581         if globals.quiet:
582             flags = '-q'
583             redirect = '>/dev/null'
584         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
585         os.chdir('%s/src/%s' % (target.directory, self.name))
586
587         spec = self.specifier
588         if spec is None:
589             spec = 'master'
590
591         command('git checkout %s %s %s' % (flags, spec, redirect))
592         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
593         command('git submodule init --quiet')
594         command('git submodule update --quiet')
595
596         proj = '%s/src/%s' % (target.directory, self.name)
597
598         self.cscript = {}
599         execfile('%s/cscript' % proj, self.cscript)
600
601         if os.path.exists('%s/wscript' % proj):
602             v = read_wscript_variable(proj, "VERSION");
603             if v is not None:
604                 self.version = Version(v)
605
606         os.chdir(cwd)
607
608     def call(self, function, *args):
609         with TreeDirectory(self):
610             return self.cscript[function](self.target, *args)
611
612     def build_dependencies(self):
613         if 'dependencies' in self.cscript:
614             for d in self.cscript['dependencies'](self.target):
615                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
616                 dep = globals.trees.get(d[0], d[1], self.target)
617                 dep.build_dependencies()
618
619                 # Make the options to pass in from the option_defaults of the thing
620                 # we are building and any options specified by the parent.
621                 options = {}
622                 if 'option_defaults' in dep.cscript:
623                     options = dep.cscript['option_defaults']()
624                     if len(d) > 2:
625                         for k, v in d[2].iteritems():
626                             options[k] = v
627
628                 dep.build(options)
629
630     def build(self, options=None):
631         if self.built:
632             return
633
634         variables = copy.copy(self.target.variables)
635
636         if len(inspect.getargspec(self.cscript['build']).args) == 2:
637             self.call('build', options)
638         else:
639             self.call('build')
640
641         self.target.variables = variables
642         self.built = True
643
644 #
645 # Command-line parser
646 #
647
648 def main():
649
650     commands = {
651         "build": "build project",
652         "package": "package and build project",
653         "release": "release a project using its next version number (changing wscript and tagging)",
654         "pot": "build the project's .pot files",
655         "changelog": "generate a simple HTML changelog",
656         "manual": "build the project's manual",
657         "doxygen": "build the project's Doxygen documentation",
658         "latest": "print out the latest version",
659         "test": "run the project's unit tests",
660         "shell": "build the project then start a shell in its chroot",
661         "checkout": "check out the project",
662         "revision": "print the head git revision number"
663     }
664
665     one_of = "Command is one of:\n"
666     summary = ""
667     for k, v in commands.iteritems():
668         one_of += "\t%s\t%s\n" % (k, v)
669         summary += k + " "
670
671     parser = argparse.ArgumentParser()
672     parser.add_argument('command', help=summary)
673     parser.add_argument('-p', '--project', help='project name')
674     parser.add_argument('--minor', help='minor version number bump', action='store_true')
675     parser.add_argument('--micro', help='micro version number bump', action='store_true')
676     parser.add_argument('--major', help='major version to return with latest', type=int)
677     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
678     parser.add_argument('-o', '--output', help='output directory', default='.')
679     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
680     parser.add_argument('-t', '--target', help='target')
681     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
682     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
683     parser.add_argument('-w', '--work', help='override default work directory')
684     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
685     args = parser.parse_args()
686
687     # Override configured stuff
688     if args.git_prefix is not None:
689         config.set('git_prefix', args.git_prefix)
690
691     if args.output.find(':') == -1:
692         # This isn't of the form host:path so make it absolute
693         args.output = os.path.abspath(args.output) + '/'
694     else:
695         if args.output[-1] != ':' and args.output[-1] != '/':
696             args.output += '/'
697
698     # Now, args.output is 'host:', 'host:path/' or 'path/'
699
700     if args.work is not None:
701         args.work = os.path.abspath(args.work)
702
703     if args.project is None and args.command != 'shell':
704         raise Error('you must specify -p or --project')
705
706     globals.quiet = args.quiet
707     globals.command = args.command
708
709     if not globals.command in commands:
710         e = 'command must be one of:\n' + one_of
711         raise Error('command must be one of:\n%s' % one_of)
712
713     if globals.command == 'build':
714         if args.target is None:
715             raise Error('you must specify -t or --target')
716
717         target = target_factory(args.target, args.debug, args.work)
718         tree = globals.trees.get(args.project, args.checkout, target)
719         tree.build_dependencies()
720         tree.build()
721         if not args.keep:
722             target.cleanup()
723
724     elif globals.command == 'package':
725         if args.target is None:
726             raise Error('you must specify -t or --target')
727
728         target = target_factory(args.target, args.debug, args.work)
729         packages, git_commit = target.package(args.project, args.checkout)
730         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
731             packages = [packages]
732
733         if target.platform == 'linux':
734             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
735             try:
736                 makedirs(out)
737             except:
738                 pass
739             for p in packages:
740                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
741         else:
742             try:
743                 makedirs(args.output)
744             except:
745                 pass
746             for p in packages:
747                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
748
749         if not args.keep:
750             target.cleanup()
751
752     elif globals.command == 'release':
753         if args.minor is False and args.micro is False:
754             raise Error('you must specify --minor or --micro')
755
756         target = SourceTarget()
757         tree = globals.trees.get(args.project, args.checkout, target)
758
759         version = tree.version
760         version.to_release()
761         if args.minor:
762             version.bump_minor()
763         else:
764             version.bump_micro()
765
766         set_version_in_wscript(version)
767         append_version_to_changelog(version)
768         append_version_to_debian_changelog(version)
769
770         command('git commit -a -m "Bump version"')
771         command('git tag -m "v%s" v%s' % (version, version))
772
773         version.to_devel()
774         set_version_in_wscript(version)
775         command('git commit -a -m "Bump version"')
776         command('git push')
777         command('git push --tags')
778
779         target.cleanup()
780
781     elif globals.command == 'pot':
782         target = SourceTarget()
783         tree = globals.trees.get(args.project, args.checkout, target)
784
785         pots = tree.call('make_pot')
786         for p in pots:
787             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
788
789         target.cleanup()
790
791     elif globals.command == 'changelog':
792         target = SourceTarget()
793         tree = globals.trees.get(args.project, args.checkout, target)
794
795         with TreeDirectory(tree):
796             text = open('ChangeLog', 'r')
797
798         html = tempfile.NamedTemporaryFile()
799         versions = 8
800
801         last = None
802         changes = []
803
804         while True:
805             l = text.readline()
806             if l == '':
807                 break
808
809             if len(l) > 0 and l[0] == "\t":
810                 s = l.split()
811                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
812                     v = Version(s[2])
813                     if v.micro == 0:
814                         if last is not None and len(changes) > 0:
815                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
816                             print >>html,"<ul>"
817                             for c in changes:
818                                 print >>html,"<li>%s" % c
819                             print >>html,"</ul>"
820                         last = s[2]
821                         changes = []
822                         versions -= 1
823                         if versions < 0:
824                             break
825                 else:
826                     c = l.strip()
827                     if len(c) > 0:
828                         if c[0] == '*':
829                             changes.append(c[2:])
830                         else:
831                             changes[-1] += " " + c
832
833         copyfile(html.file, '%schangelog.html' % args.output)
834         html.close()
835         target.cleanup()
836
837     elif globals.command == 'manual':
838         target = SourceTarget()
839         tree = globals.trees.get(args.project, args.checkout, target)
840
841         outs = tree.call('make_manual')
842         for o in outs:
843             if os.path.isfile(o):
844                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
845             else:
846                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
847
848         target.cleanup()
849
850     elif globals.command == 'doxygen':
851         target = SourceTarget()
852         tree = globals.trees.get(args.project, args.checkout, target)
853
854         dirs = tree.call('make_doxygen')
855         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
856             dirs = [dirs]
857
858         for d in dirs:
859             copytree(d, args.output)
860
861         target.cleanup()
862
863     elif globals.command == 'latest':
864         target = SourceTarget()
865         tree = globals.trees.get(args.project, args.checkout, target)
866
867         with TreeDirectory(tree):
868             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
869             latest = None
870             while latest is None:
871                 t = f.readline()
872                 m = re.compile(".*\((.*)\).*").match(t)
873                 if m:
874                     tags = m.group(1).split(', ')
875                     for t in tags:
876                         s = t.split()
877                         if len(s) > 1:
878                             t = s[1]
879                         if len(t) > 0 and t[0] == 'v':
880                             v = Version(t[1:])
881                             if args.major is None or v.major == args.major:
882                                 latest = v
883
884         print latest
885         target.cleanup()
886
887     elif globals.command == 'test':
888         if args.target is None:
889             raise Error('you must specify -t or --target')
890
891         target = None
892         try:
893             target = target_factory(args.target, args.debug, args.work)
894             tree = globals.trees.get(args.project, args.checkout, target)
895             with TreeDirectory(tree):
896                 target.test(tree)
897         except Error as e:
898             if target is not None:
899                 target.cleanup()
900             raise
901
902         if target is not None:
903             target.cleanup()
904
905     elif globals.command == 'shell':
906         if args.target is None:
907             raise Error('you must specify -t or --target')
908
909         target = target_factory(args.target, args.debug, args.work)
910         target.command('bash')
911
912     elif globals.command == 'revision':
913
914         target = SourceTarget()
915         tree = globals.trees.get(args.project, args.checkout, target)
916         with TreeDirectory(tree):
917             print command_and_read('git rev-parse HEAD').readline().strip()[:7]
918         target.cleanup()
919
920     elif globals.command == 'checkout':
921
922         if args.output is None:
923             raise Error('you must specify -o or --output')
924
925         target = SourceTarget()
926         tree = globals.trees.get(args.project, args.checkout, target)
927         with TreeDirectory(tree):
928             shutil.copytree('.', args.output)
929         target.cleanup()
930
931     else:
932         raise Error('invalid command %s' % globals.command)
933
934 try:
935     main()
936 except Error as e:
937     print >>sys.stderr,'cdist: %s' % str(e)
938     sys.exit(1)