Fix SourceTarget package().
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2014 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 Globals:
34     quiet = False
35     command = None
36
37 globals = Globals()
38
39 class Error(Exception):
40     def __init__(self, value):
41         self.value = value
42     def __str__(self):
43         return self.value
44     def __repr__(self):
45         return str(self)
46
47 class Projects:
48     """
49     Store for Project objects which re-uses already-created objects
50     and checks for requests for different versions of the same thing.
51     """
52
53     def __init__(self):
54         self.projects = []
55
56     def get(self, name, specifier, target):
57         for p in self.projects:
58             if p.name == name and p.specifier == specifier:
59                 return p
60             elif p.name == name and p.specifier != p.specifier:
61                 raise Error('conflicting versions of %s requested (%s and %s)' % (name, specifier, p.specifier))
62
63         np = Project(name, specifier, target)
64         self.projects.append(np)
65         return np
66
67 projects = Projects()
68
69
70 #
71 # Configuration
72 #
73
74 class Option(object):
75     def __init__(self, key, default=None):
76         self.key = key
77         self.value = default
78
79     def offer(self, key, value):
80         if key == self.key:
81             self.value = value
82
83 class BoolOption(object):
84     def __init__(self, key):
85         self.key = key
86         self.value = False
87
88     def offer(self, key, value):
89         if key == self.key:
90             self.value = (value == 'yes' or value == '1' or value == 'true')
91
92 class Config:
93     def __init__(self):
94         self.options = [ Option('linux_chroot_prefix'),
95                          Option('windows_environment_prefix'),
96                          Option('mingw_prefix'),
97                          Option('git_prefix'),
98                          Option('osx_build_host'),
99                          Option('osx_environment_prefix'),
100                          Option('osx_sdk_prefix'),
101                          Option('osx_sdk'),
102                          Option('parallel', 4) ]
103
104         try:
105             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
106             while True:
107                 l = f.readline()
108                 if l == '':
109                     break
110
111                 if len(l) > 0 and l[0] == '#':
112                     continue
113
114                 s = l.strip().split()
115                 if len(s) == 2:
116                     for k in self.options:
117                         k.offer(s[0], s[1])
118         except:
119             raise
120
121     def get(self, k):
122         for o in self.options:
123             if o.key == k:
124                 return o.value
125
126         raise Error('Required setting %s not found' % k)
127
128 config = Config()
129
130 #
131 # Utility bits
132
133
134 def log(m):
135     if not globals.quiet:
136         print '\x1b[33m* %s\x1b[0m' % m
137
138 def scp_escape(n):
139     s = n.split(':')
140     assert(len(s) == 1 or len(s) == 2)
141     if len(s) == 2:
142         return '%s:"\'%s\'"' % (s[0], s[1])
143     else:
144         return '\"%s\"' % s[0]
145
146 def copytree(a, b):
147     log('copy %s -> %s' % (scp_escape(b), scp_escape(b)))
148     command('scp -r %s %s' % (scp_escape(a), scp_escape(b)))
149
150 def copyfile(a, b):
151     log('copy %s -> %s' % (scp_escape(a), scp_escape(b)))
152     command('scp %s %s' % (scp_escape(a), scp_escape(b)))
153
154 def makedirs(d):
155     if d.find(':') == -1:
156         os.makedirs(d)
157     else:
158         s = d.split(':')
159         command('ssh %s -- mkdir -p %s' % (s[0], s[1]))
160
161 def rmdir(a):
162     log('remove %s' % a)
163     os.rmdir(a)
164
165 def rmtree(a):
166     log('remove %s' % a)
167     shutil.rmtree(a, ignore_errors=True)
168
169 def command(c):
170     log(c)
171     r = os.system(c)
172     if (r >> 8):
173         raise Error('command %s failed' % c)
174
175 def command_and_read(c):
176     log(c)
177     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
178     f = os.fdopen(os.dup(p.stdout.fileno()))
179     return f
180
181 def read_wscript_variable(directory, variable):
182     f = open('%s/wscript' % directory, 'r')
183     while True:
184         l = f.readline()
185         if l == '':
186             break
187         
188         s = l.split()
189         if len(s) == 3 and s[0] == variable:
190             f.close()
191             return s[2][1:-1]
192
193     f.close()
194     return None
195
196 def set_version_in_wscript(version):
197     f = open('wscript', 'rw')
198     o = open('wscript.tmp', 'w')
199     while True:
200         l = f.readline()
201         if l == '':
202             break
203
204         s = l.split()
205         if len(s) == 3 and s[0] == "VERSION":
206             print "Writing %s" % version
207             print >>o,"VERSION = '%s'" % version
208         else:
209             print >>o,l,
210     f.close()
211     o.close()
212
213     os.rename('wscript.tmp', 'wscript')
214
215 def append_version_to_changelog(version):
216     try:
217         f = open('ChangeLog', 'r')
218     except:
219         log('Could not open ChangeLog')
220         return
221
222     c = f.read()
223     f.close()
224
225     f = open('ChangeLog', 'w')
226     now = datetime.datetime.now()
227     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))
228     f.write(c)
229
230 def append_version_to_debian_changelog(version):
231     if not os.path.exists('debian'):
232         log('Could not find debian directory')
233         return
234
235     command('dch -b -v %s-1 "New upstream release."' % version)
236
237 def devel_to_git(project, filename):
238     if project.git_commit is not None:
239         filename = filename.replace('devel', '-%s' % project.git_commit)
240     return filename
241
242 class ProjectDirectory:
243     def __init__(self, target, project):
244         self.target = target
245         self.project = project
246     def __enter__(self):
247         self.cwd = os.getcwd()
248         os.chdir('%s/src/%s' % (self.target.directory, self.project.name))
249     def __exit__(self, type, value, traceback):
250         os.chdir(self.cwd)
251
252 #
253 # Version
254 #
255
256 class Version:
257     def __init__(self, s):
258         self.devel = False
259
260         if s.startswith("'"):
261             s = s[1:]
262         if s.endswith("'"):
263             s = s[0:-1]
264         
265         if s.endswith('devel'):
266             s = s[0:-5]
267             self.devel = True
268
269         if s.endswith('pre'):
270             s = s[0:-3]
271
272         p = s.split('.')
273         self.major = int(p[0])
274         self.minor = int(p[1])
275         if len(p) == 3:
276             self.micro = int(p[2])
277         else:
278             self.micro = 0
279
280     def bump_minor(self):
281         self.minor += 1
282         self.micro = 0
283
284     def bump_micro(self):
285         self.micro += 1
286
287     def to_devel(self):
288         self.devel = True
289
290     def to_release(self):
291         self.devel = False
292
293     def __str__(self):
294         s = '%d.%d.%d' % (self.major, self.minor, self.micro)
295         if self.devel:
296             s += 'devel'
297
298         return s
299
300 #
301 # Targets
302 #
303
304 class Target(object):
305     """
306     platform -- platform string (e.g. 'windows', 'linux', 'osx')
307     directory -- directory to work in; if None we will use a temporary directory
308     Temporary directories will be removed after use; specified directories will not.
309     """
310     def __init__(self, platform, directory=None):
311         self.platform = platform
312         self.parallel = int(config.get('parallel'))
313
314         # self.directory is the working directory
315         if directory is None:
316             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
317             self.rmdir = True
318         else:
319             self.directory = directory
320             self.rmdir = False
321
322         # Environment variables that we will use when we call cscripts
323         self.variables = {}
324         self.debug = False
325
326     def build_dependencies(self, project):
327         if 'dependencies' in project.cscript:
328             for d in project.cscript['dependencies'](self):
329                 log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
330                 dep = projects.get(d[0], d[1], self)
331                 self.build_dependencies(dep)
332
333                 # Make the options to pass in from the option_defaults of the thing
334                 # we are building and any options specified by the parent.
335                 options = {}
336                 if 'option_defaults' in dep.cscript:
337                     options = dep.cscript['option_defaults']()
338                     if len(d) > 2:
339                         for k, v in d[2].iteritems():
340                             options[k] = v
341
342                 self.build(dep, options)
343
344     def build(self, project, options=None):
345         if project.built:
346             return
347
348         variables = copy.copy(self.variables)
349
350         if len(inspect.getargspec(project.cscript['build']).args) == 2:
351             project.call(self, 'build', options)
352         else:
353             project.call(self, 'build')
354         
355         self.variables = variables
356         project.built = True
357
358     def package(self, project):
359         self.build_dependencies(project)
360         self.build(project)
361         return project.call(self, 'package', project.version)
362
363     def test(self, project):
364         self.build_dependencies(project)
365         self.build(project)
366         return project.call(self, 'test')
367
368     def set(self, a, b):
369         self.variables[a] = b
370
371     def unset(self, a):
372         del(self.variables[a])
373
374     def get(self, a):
375         return self.variables[a]
376
377     def append_with_space(self, k, v):
378         if not k in self.variables:
379             self.variables[k] = v
380         else:
381             self.variables[k] = '%s %s' % (self.variables[k], v)
382
383     def variables_string(self, escaped_quotes=False):
384         e = ''
385         for k, v in self.variables.iteritems():
386             if escaped_quotes:
387                 v = v.replace('"', '\\"')
388             e += '%s=%s ' % (k, v)
389         return e
390
391     def cleanup(self):
392         if self.rmdir:
393             rmtree(self.directory)
394
395
396 # Windows
397 #
398
399 class WindowsTarget(Target):
400     def __init__(self, bits, directory=None):
401         super(WindowsTarget, self).__init__('windows', directory)
402         self.bits = bits
403
404         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
405         if not os.path.exists(self.windows_prefix):
406             raise Error('windows prefix %s does not exist' % self.windows_prefix)
407             
408         if self.bits == 32:
409             self.mingw_name = 'i686'
410         else:
411             self.mingw_name = 'x86_64'
412
413         self.mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
414         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
415
416         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
417         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
418         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, self.mingw_path, os.environ['PATH']))
419         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
420         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
421         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
422         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
423         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
424         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
425         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
426         for p in self.mingw_prefixes:
427             cxx += ' -I%s/include' % p
428             link += ' -L%s/lib' % p
429         self.set('CXXFLAGS', '"%s"' % cxx)
430         self.set('CPPFLAGS', '')
431         self.set('LINKFLAGS', '"%s"' % link)
432
433     def command(self, c):
434         log('host -> %s' % c)
435         command('%s %s' % (self.variables_string(), c))
436
437 #
438 # Linux
439 #
440
441 class LinuxTarget(Target):
442     def __init__(self, distro, version, bits, directory=None):
443         super(LinuxTarget, self).__init__('linux', directory)
444         self.distro = distro
445         self.version = version
446         self.bits = bits
447         # e.g. ubuntu-14.04-64
448         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
449         # e.g. /home/carl/Environments/ubuntu-14.04-64
450         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
451
452         self.set('CXXFLAGS', '-I%s/include' % self.directory)
453         self.set('CPPFLAGS', '')
454         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
455         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
456         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
457
458     def command(self, c):
459         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
460
461 #
462 # OS X
463 #
464
465 class OSXTarget(Target):
466     def __init__(self, directory=None):
467         super(OSXTarget, self).__init__('osx', directory)
468
469     def command(self, c):
470         command('%s %s' % (self.variables_string(False), c))
471
472
473 class OSXSingleTarget(OSXTarget):
474     def __init__(self, bits, directory=None):
475         super(OSXSingleTarget, self).__init__(directory)
476         self.bits = bits
477
478         if bits == 32:
479             arch = 'i386'
480         else:
481             arch = 'x86_64'
482
483         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
484         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
485
486         # Environment variables
487         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
488         self.set('CPPFLAGS', '')
489         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
490         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
491         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
492         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
493         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
494         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
495
496     def package(self, project):
497         raise Error('cannot package non-universal OS X versions')
498
499
500 class OSXUniversalTarget(OSXTarget):
501     def __init__(self, directory=None):
502         super(OSXUniversalTarget, self).__init__(directory)
503         self.parts = []
504         self.parts.append(OSXSingleTarget(32, os.path.join(self.directory, '32')))
505         self.parts.append(OSXSingleTarget(64, os.path.join(self.directory, '64')))
506
507     def package(self, project):
508         for p in self.parts:
509             p.build_dependencies(project)
510             p.build(project)
511
512         project.call(self, 'package', project.version)
513     
514
515 #
516 # Source
517 #
518
519 class SourceTarget(Target):
520     def __init__(self):
521         super(SourceTarget, self).__init__('source')
522
523     def command(self, c):
524         log('host -> %s' % c)
525         command('%s %s' % (self.variables_string(), c))
526
527     def cleanup(self):
528         rmtree(self.directory)
529
530     def package(self, project):
531         with ProjectDirectory(self, project):
532             name = read_wscript_variable(os.getcwd(), 'APPNAME')
533             command('./waf dist')
534             return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
535
536
537 # @param s Target string:
538 #       windows-{32,64}
539 #    or ubuntu-version-{32,64}
540 #    or debian-version-{32,64}
541 #    or centos-version-{32,64}
542 #    or osx-{32,64}
543 #    or source      
544 # @param debug True to build with debugging symbols (where possible)
545 def target_factory(s, debug, work):
546     target = None
547     if s.startswith('windows-'):
548         target = WindowsTarget(int(s.split('-')[1]), work)
549     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
550         p = s.split('-')
551         if len(p) != 3:
552             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
553             sys.exit(1)
554         target = LinuxTarget(p[0], p[1], int(p[2]), work)
555     elif s.startswith('osx-'):
556         target = OSXSingleTarget(int(s.split('-')[1]), work)
557     elif s == 'osx':
558         if globals.command == 'build':
559             target = OSXSingleTarget(64, work)
560         else:
561             target = OSXUniversalTarget(work)
562     elif s == 'source':
563         target = SourceTarget()
564
565     if target is not None:
566         target.debug = debug
567
568     return target
569
570
571 #
572 # Project
573 #
574  
575 class Project(object):
576     """Description of a project.  This class is never exposed to cscripts.
577        Attributes:
578            name -- name of git repository (without the .git)
579            specifier -- git tag or revision to use
580            target --- target object that we are using
581            version --- version from the wscript (if one is present)
582            git_commit -- git revision that is actually being used
583            built --- true if the project has been built yet in this run
584     """
585
586     def __init__(self, name, specifier, target):
587         self.name = name
588         self.specifier = specifier
589         self.target = target
590         self.version = None
591         self.git_commit = None
592         self.built = False
593
594         cwd = os.getcwd()
595
596         flags = ''
597         redirect = ''
598         if globals.quiet:
599             flags = '-q'
600             redirect = '>/dev/null'
601         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
602         os.chdir('%s/src/%s' % (target.directory, self.name))
603
604         spec = self.specifier
605         if spec is None:
606             spec = 'master'
607
608         command('git checkout %s %s %s' % (flags, spec, redirect))
609         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
610         command('git submodule init --quiet')
611         command('git submodule update --quiet')
612
613         proj = '%s/src/%s' % (target.directory, self.name)
614
615         self.cscript = {}
616         execfile('%s/cscript' % proj, self.cscript)
617
618         if os.path.exists('%s/wscript' % proj):
619             v = read_wscript_variable(proj, "VERSION");
620             if v is not None:
621                 self.version = Version(v)
622
623         os.chdir(cwd)
624
625     def call(self, target, function, *args):
626         with ProjectDirectory(target, self):
627             return self.cscript[function](target, *args)
628
629 #
630 # Command-line parser
631 #
632
633 def main():
634
635     commands = {
636         "build": "build project",
637         "package": "package and build project",
638         "release": "release a project using its next version number (changing wscript and tagging)",
639         "pot": "build the project's .pot files",
640         "changelog": "generate a simple HTML changelog",
641         "manual": "build the project's manual",
642         "doxygen": "build the project's Doxygen documentation",
643         "latest": "print out the latest version",
644         "test": "run the project's unit tests",
645         "shell": "build the project then start a shell in its chroot",
646         "revision": "print the head git revision number"
647     }
648
649     one_of = "Command is one of:\n"
650     summary = ""
651     for k, v in commands.iteritems():
652         one_of += "\t%s\t%s\n" % (k, v)
653         summary += k + " "
654
655     parser = argparse.ArgumentParser()
656     parser.add_argument('command', help=summary)
657     parser.add_argument('-p', '--project', help='project name')
658     parser.add_argument('--minor', help='minor version number bump', action='store_true')
659     parser.add_argument('--micro', help='micro version number bump', action='store_true')
660     parser.add_argument('--major', help='major version to return with latest', type=int)
661     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
662     parser.add_argument('-o', '--output', help='output directory', default='.')
663     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
664     parser.add_argument('-t', '--target', help='target')
665     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
666     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
667     parser.add_argument('-w', '--work', help='override default work directory')
668     args = parser.parse_args()
669
670     if args.output.find(':') == -1:
671         # This isn't of the form host:path so make it absolute
672         args.output = os.path.abspath(args.output) + '/'
673     else:
674         if args.output[-1] != ':' and args.output[-1] != '/':
675             args.output += '/'
676
677     # Now, args.output is 'host:', 'host:path/' or 'path/'
678
679     if args.work is not None:
680         args.work = os.path.abspath(args.work)
681
682     if args.project is None and args.command != 'shell':
683         raise Error('you must specify -p or --project')
684         
685     globals.quiet = args.quiet
686     globals.command = args.command
687
688     if not globals.command in commands:
689         e = 'command must be one of:\n' + one_of
690         raise Error('command must be one of:\n%s' % one_of)
691
692     if globals.command == 'build':
693         if args.target is None:
694             raise Error('you must specify -t or --target')
695
696         target = target_factory(args.target, args.debug, args.work)
697         project = projects.get(args.project, args.checkout, target)
698         target.build_dependencies(project)
699         target.build(project)
700         if not args.keep:
701             target.cleanup()
702
703     elif globals.command == 'package':
704         if args.target is None:
705             raise Error('you must specify -t or --target')
706
707         target = target_factory(args.target, args.debug, args.work)
708         project = projects.get(args.project, args.checkout, target)
709
710         packages = target.package(project)
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(project, 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(project, 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         project = projects.get(args.project, args.checkout, target)
739
740         version = project.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         project = projects.get(args.project, args.checkout, target)
765
766         pots = project.call(target, '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         project = projects.get(args.project, args.checkout, target)
775
776         with ProjectDirectory(target, project):
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         project = projects.get(args.project, args.checkout, target)
821
822         outs = project.call(target, '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         project = projects.get(args.project, args.checkout, target)
834
835         dirs = project.call(target, '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         project = projects.get(args.project, args.checkout, target)
847
848         with ProjectDirectory(target, project):
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             project = projects.get(args.project, args.checkout, target)
876             with ProjectDirectory(target, project):
877                 target.test(project)
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         project = projects.get(args.project, args.checkout, target)
897         with ProjectDirectory(target, project):
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)