Various tidying up and fixes for OS X.
[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 p
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(self, 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.startswith('osx-'):
525         target = OSXSingleTarget(int(s.split('-')[1]), work)
526     elif s == 'osx':
527         if globals.command == 'build':
528             target = OSXSingleTarget(64, work)
529         else:
530             target = OSXUniversalTarget(work)
531     elif s == 'source':
532         target = SourceTarget()
533
534     if target is not None:
535         target.debug = debug
536
537     return target
538
539
540 #
541 # Tree
542 #
543  
544 class Tree(object):
545     """Description of a tree, which is a checkout of a project,
546        possibly built.  This class is never exposed to cscripts.
547        Attributes:
548            name -- name of git repository (without the .git)
549            specifier -- git tag or revision to use
550            target --- target object that we are using
551            version --- version from the wscript (if one is present)
552            git_commit -- git revision that is actually being used
553            built --- true if the tree has been built yet in this run
554     """
555
556     def __init__(self, name, specifier, target):
557         self.name = name
558         self.specifier = specifier
559         self.target = target
560         self.version = None
561         self.git_commit = None
562         self.built = False
563
564         cwd = os.getcwd()
565
566         flags = ''
567         redirect = ''
568         if globals.quiet:
569             flags = '-q'
570             redirect = '>/dev/null'
571         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
572         os.chdir('%s/src/%s' % (target.directory, self.name))
573
574         spec = self.specifier
575         if spec is None:
576             spec = 'master'
577
578         command('git checkout %s %s %s' % (flags, spec, redirect))
579         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
580         command('git submodule init --quiet')
581         command('git submodule update --quiet')
582
583         proj = '%s/src/%s' % (target.directory, self.name)
584
585         self.cscript = {}
586         execfile('%s/cscript' % proj, self.cscript)
587
588         if os.path.exists('%s/wscript' % proj):
589             v = read_wscript_variable(proj, "VERSION");
590             if v is not None:
591                 self.version = Version(v)
592
593         os.chdir(cwd)
594
595     def call(self, function, *args):
596         with TreeDirectory(self):
597             return self.cscript[function](self.target, *args)
598
599     def build_dependencies(self):
600         if 'dependencies' in self.cscript:
601             for d in self.cscript['dependencies'](self.target):
602                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
603                 dep = globals.trees.get(d[0], d[1], self.target)
604                 dep.build_dependencies()
605
606                 # Make the options to pass in from the option_defaults of the thing
607                 # we are building and any options specified by the parent.
608                 options = {}
609                 if 'option_defaults' in dep.cscript:
610                     options = dep.cscript['option_defaults']()
611                     if len(d) > 2:
612                         for k, v in d[2].iteritems():
613                             options[k] = v
614
615                 dep.build(options)
616
617     def build(self, options=None):
618         if self.built:
619             return
620
621         variables = copy.copy(self.target.variables)
622
623         if len(inspect.getargspec(self.cscript['build']).args) == 2:
624             self.call('build', options)
625         else:
626             self.call('build')
627         
628         self.target.variables = variables
629         self.built = True
630
631 #
632 # Command-line parser
633 #
634
635 def main():
636
637     commands = {
638         "build": "build project",
639         "package": "package and build project",
640         "release": "release a project using its next version number (changing wscript and tagging)",
641         "pot": "build the project's .pot files",
642         "changelog": "generate a simple HTML changelog",
643         "manual": "build the project's manual",
644         "doxygen": "build the project's Doxygen documentation",
645         "latest": "print out the latest version",
646         "test": "run the project's unit tests",
647         "shell": "build the project then start a shell in its chroot",
648         "revision": "print the head git revision number"
649     }
650
651     one_of = "Command is one of:\n"
652     summary = ""
653     for k, v in commands.iteritems():
654         one_of += "\t%s\t%s\n" % (k, v)
655         summary += k + " "
656
657     parser = argparse.ArgumentParser()
658     parser.add_argument('command', help=summary)
659     parser.add_argument('-p', '--project', help='project name')
660     parser.add_argument('--minor', help='minor version number bump', action='store_true')
661     parser.add_argument('--micro', help='micro version number bump', action='store_true')
662     parser.add_argument('--major', help='major version to return with latest', type=int)
663     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
664     parser.add_argument('-o', '--output', help='output directory', default='.')
665     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
666     parser.add_argument('-t', '--target', help='target')
667     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
668     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
669     parser.add_argument('-w', '--work', help='override default work directory')
670     args = parser.parse_args()
671
672     if args.output.find(':') == -1:
673         # This isn't of the form host:path so make it absolute
674         args.output = os.path.abspath(args.output) + '/'
675     else:
676         if args.output[-1] != ':' and args.output[-1] != '/':
677             args.output += '/'
678
679     # Now, args.output is 'host:', 'host:path/' or 'path/'
680
681     if args.work is not None:
682         args.work = os.path.abspath(args.work)
683
684     if args.project is None and args.command != 'shell':
685         raise Error('you must specify -p or --project')
686         
687     globals.quiet = args.quiet
688     globals.command = args.command
689
690     if not globals.command in commands:
691         e = 'command must be one of:\n' + one_of
692         raise Error('command must be one of:\n%s' % one_of)
693
694     if globals.command == 'build':
695         if args.target is None:
696             raise Error('you must specify -t or --target')
697
698         target = target_factory(args.target, args.debug, args.work)
699         tree = globals.trees.get(args.project, args.checkout, target)
700         tree.build_dependencies()
701         tree.build()
702         if not args.keep:
703             target.cleanup()
704
705     elif globals.command == 'package':
706         if args.target is None:
707             raise Error('you must specify -t or --target')
708
709         target = target_factory(args.target, args.debug, args.work)
710         packages, git_commit = target.package(args.project, args.checkout)
711         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
712             packages = [packages]
713
714         if target.platform == 'linux':
715             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
716             try:
717                 makedirs(out)
718             except:
719                 pass
720             for p in packages:
721                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
722         else:
723             try:
724                 makedirs(args.output)
725             except:
726                 pass
727             for p in packages:
728                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
729
730         if not args.keep:
731             target.cleanup()
732
733     elif globals.command == 'release':
734         if args.minor is False and args.micro is False:
735             raise Error('you must specify --minor or --micro')
736
737         target = SourceTarget()
738         tree = globals.trees.get(args.project, args.checkout, target)
739
740         version = tree.version
741         version.to_release()
742         if args.minor:
743             version.bump_minor()
744         else:
745             version.bump_micro()
746
747         set_version_in_wscript(version)
748         append_version_to_changelog(version)
749         append_version_to_debian_changelog(version)
750
751         command('git commit -a -m "Bump version"')
752         command('git tag -m "v%s" v%s' % (version, version))
753
754         version.to_devel()
755         set_version_in_wscript(version)
756         command('git commit -a -m "Bump version"')
757         command('git push')
758         command('git push --tags')
759
760         target.cleanup()
761
762     elif globals.command == 'pot':
763         target = SourceTarget()
764         tree = globals.trees.get(args.project, args.checkout, target)
765
766         pots = tree.call('make_pot')
767         for p in pots:
768             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
769
770         target.cleanup()
771
772     elif globals.command == 'changelog':
773         target = SourceTarget()
774         tree = globals.trees.get(args.project, args.checkout, target)
775
776         with TreeDirectory(tree):
777             text = open('ChangeLog', 'r')
778
779         html = tempfile.NamedTemporaryFile()
780         versions = 8
781
782         last = None
783         changes = []
784
785         while True:
786             l = text.readline()
787             if l == '':
788                 break
789
790             if len(l) > 0 and l[0] == "\t":
791                 s = l.split()
792                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
793                     v = Version(s[2])
794                     if v.micro == 0:
795                         if last is not None and len(changes) > 0:
796                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
797                             print >>html,"<ul>"
798                             for c in changes:
799                                 print >>html,"<li>%s" % c
800                             print >>html,"</ul>"
801                         last = s[2]
802                         changes = []
803                         versions -= 1
804                         if versions < 0:
805                             break
806                 else:
807                     c = l.strip()
808                     if len(c) > 0:
809                         if c[0] == '*':
810                             changes.append(c[2:])
811                         else:
812                             changes[-1] += " " + c
813
814         copyfile(html.file, '%schangelog.html' % args.output)
815         html.close()
816         target.cleanup()
817
818     elif globals.command == 'manual':
819         target = SourceTarget()
820         tree = globals.trees.get(args.project, args.checkout, target)
821
822         outs = tree.call('make_manual')
823         for o in outs:
824             if os.path.isfile(o):
825                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
826             else:
827                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
828
829         target.cleanup()
830
831     elif globals.command == 'doxygen':
832         target = SourceTarget()
833         tree = globals.trees.get(args.project, args.checkout, target)
834
835         dirs = tree.call('make_doxygen')
836         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
837             dirs = [dirs]
838
839         for d in dirs:
840             copytree(d, '%s%s' % (args.output, 'doc'))
841
842         target.cleanup()
843
844     elif globals.command == 'latest':
845         target = SourceTarget()
846         tree = globals.trees.get(args.project, args.checkout, target)
847
848         with TreeDirectory(tree):
849             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
850             latest = None
851             while latest is None:
852                 t = f.readline()
853                 m = re.compile(".*\((.*)\).*").match(t)
854                 if m:
855                     tags = m.group(1).split(', ')
856                     for t in tags:
857                         s = t.split()
858                         if len(s) > 1:
859                             t = s[1]
860                         if len(t) > 0 and t[0] == 'v':
861                             v = Version(t[1:])
862                             if args.major is None or v.major == args.major:
863                                 latest = v
864
865         print latest
866         target.cleanup()
867
868     elif globals.command == 'test':
869         if args.target is None:
870             raise Error('you must specify -t or --target')
871
872         target = None
873         try:
874             target = target_factory(args.target, args.debug, args.work)
875             tree = globals.trees.get(args.project, args.checkout, target)
876             with TreeDirectory(tree):
877                 target.test(tree)
878         except Error as e:
879             if target is not None:
880                 target.cleanup()
881             raise
882
883         if target is not None:
884             target.cleanup()
885
886     elif globals.command == 'shell':
887         if args.target is None:
888             raise Error('you must specify -t or --target')
889
890         target = target_factory(args.target, args.debug, args.work)
891         target.command('bash')
892
893     elif globals.command == 'revision':
894
895         target = SourceTarget()
896         tree = globals.trees.get(args.project, args.checkout, target)
897         with TreeDirectory(tree):
898             print command_and_read('git rev-parse HEAD').readline().strip()[:7]
899         target.cleanup()
900
901     else:
902         raise Error('invalid command %s' % globals.command)
903
904 try:
905     main()
906 except Error as e:
907     print >>sys.stderr,'cdist: %s' % str(e)
908     sys.exit(1)