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