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