Merge branch 'master' of git.carlh.net:git/cdist
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program; if not, write to the Free Software
17 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 import os
20 import sys
21 import shutil
22 import glob
23 import tempfile
24 import argparse
25 import datetime
26 import subprocess
27 import re
28 import copy
29 import inspect
30
31 TEMPORARY_DIRECTORY = '/var/tmp'
32
33 class Error(Exception):
34     def __init__(self, value):
35         self.value = value
36     def __str__(self):
37         return self.value
38     def __repr__(self):
39         return str(self)
40
41 class Trees:
42     """
43     Store for Tree objects which re-uses already-created objects
44     and checks for requests for different versions of the same thing.
45     """
46
47     def __init__(self):
48         self.trees = []
49
50     def get(self, name, specifier, target):
51         for t in self.trees:
52             if t.name == name and t.specifier == specifier and t.target == target:
53                 return t
54             elif t.name == name and t.specifier != specifier:
55                 raise Error('conflicting versions of %s requested (%s and %s)' % (name, specifier, t.specifier))
56
57         nt = Tree(name, specifier, target)
58         self.trees.append(nt)
59         return nt
60
61 class Globals:
62     quiet = False
63     command = None
64     trees = Trees()
65
66 globals = Globals()
67
68
69 #
70 # Configuration
71 #
72
73 class Option(object):
74     def __init__(self, key, default=None):
75         self.key = key
76         self.value = default
77
78     def offer(self, key, value):
79         if key == self.key:
80             self.value = value
81
82 class BoolOption(object):
83     def __init__(self, key):
84         self.key = key
85         self.value = False
86
87     def offer(self, key, value):
88         if key == self.key:
89             self.value = (value == 'yes' or value == '1' or value == 'true')
90
91 class Config:
92     def __init__(self):
93         self.options = [ Option('linux_chroot_prefix'),
94                          Option('windows_environment_prefix'),
95                          Option('mingw_prefix'),
96                          Option('git_prefix'),
97                          Option('osx_build_host'),
98                          Option('osx_environment_prefix'),
99                          Option('osx_sdk_prefix'),
100                          Option('osx_sdk'),
101                          Option('parallel', 4) ]
102
103         try:
104             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
105             while True:
106                 l = f.readline()
107                 if l == '':
108                     break
109
110                 if len(l) > 0 and l[0] == '#':
111                     continue
112
113                 s = l.strip().split()
114                 if len(s) == 2:
115                     for k in self.options:
116                         k.offer(s[0], s[1])
117         except:
118             raise
119
120     def get(self, k):
121         for o in self.options:
122             if o.key == k:
123                 return o.value
124
125         raise Error('Required setting %s not found' % k)
126
127     def set(self, k, v):
128         for o in self.options:
129             o.offer(k, v)
130
131 config = Config()
132
133 #
134 # Utility bits
135 #
136
137 def log(m):
138     if not globals.quiet:
139         print('\x1b[33m* %s\x1b[0m' % m)
140
141 def scp_escape(n):
142     s = n.split(':')
143     assert(len(s) == 1 or len(s) == 2)
144     if len(s) == 2:
145         return '%s:"\'%s\'"' % (s[0], s[1])
146     else:
147         return '\"%s\"' % s[0]
148
149 def copytree(a, b):
150     log('copy %s -> %s' % (scp_escape(b), scp_escape(b)))
151     command('scp -r %s %s' % (scp_escape(a), scp_escape(b)))
152
153 def copyfile(a, b):
154     log('copy %s -> %s' % (scp_escape(a), scp_escape(b)))
155     command('scp %s %s' % (scp_escape(a), scp_escape(b)))
156
157 def makedirs(d):
158     if d.find(':') == -1:
159         os.makedirs(d)
160     else:
161         s = d.split(':')
162         command('ssh %s -- mkdir -p %s' % (s[0], s[1]))
163
164 def rmdir(a):
165     log('remove %s' % a)
166     os.rmdir(a)
167
168 def rmtree(a):
169     log('remove %s' % a)
170     shutil.rmtree(a, ignore_errors=True)
171
172 def command(c):
173     log(c)
174     r = os.system(c)
175     if (r >> 8):
176         raise Error('command %s failed' % c)
177
178 def command_and_read(c):
179     log(c)
180     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
181     f = os.fdopen(os.dup(p.stdout.fileno()))
182     return f
183
184 def read_wscript_variable(directory, variable):
185     f = open('%s/wscript' % directory, 'r')
186     while True:
187         l = f.readline()
188         if l == '':
189             break
190
191         s = l.split()
192         if len(s) == 3 and s[0] == variable:
193             f.close()
194             return s[2][1:-1]
195
196     f.close()
197     return None
198
199 def set_version_in_wscript(version):
200     f = open('wscript', 'rw')
201     o = open('wscript.tmp', 'w')
202     while True:
203         l = f.readline()
204         if l == '':
205             break
206
207         s = l.split()
208         if len(s) == 3 and s[0] == "VERSION":
209             print("Writing %s" % version)
210             print("VERSION = '%s'" % version, file=o)
211         else:
212             print(l, file=o, end="")
213     f.close()
214     o.close()
215
216     os.rename('wscript.tmp', 'wscript')
217
218 def append_version_to_changelog(version):
219     try:
220         f = open('ChangeLog', 'r')
221     except:
222         log('Could not open ChangeLog')
223         return
224
225     c = f.read()
226     f.close()
227
228     f = open('ChangeLog', 'w')
229     now = datetime.datetime.now()
230     f.write('%d-%02d-%02d  Carl Hetherington  <cth@carlh.net>\n\n\t* Version %s released.\n\n' % (now.year, now.month, now.day, version))
231     f.write(c)
232
233 def append_version_to_debian_changelog(version):
234     if not os.path.exists('debian'):
235         log('Could not find debian directory')
236         return
237
238     command('dch -b -v %s-1 "New upstream release."' % version)
239
240 def devel_to_git(git_commit, filename):
241     if git_commit is not None:
242         filename = filename.replace('devel', '-%s' % git_commit)
243     return filename
244
245 class TreeDirectory:
246     def __init__(self, tree):
247         self.tree = tree
248     def __enter__(self):
249         self.cwd = os.getcwd()
250         os.chdir('%s/src/%s' % (self.tree.target.directory, self.tree.name))
251     def __exit__(self, type, value, traceback):
252         os.chdir(self.cwd)
253
254 #
255 # Version
256 #
257
258 class Version:
259     def __init__(self, s):
260         self.devel = False
261
262         if s.startswith("'"):
263             s = s[1:]
264         if s.endswith("'"):
265             s = s[0:-1]
266
267         if s.endswith('devel'):
268             s = s[0:-5]
269             self.devel = True
270
271         if s.endswith('pre'):
272             s = s[0:-3]
273
274         p = s.split('.')
275         self.major = int(p[0])
276         self.minor = int(p[1])
277         if len(p) == 3:
278             self.micro = int(p[2])
279         else:
280             self.micro = 0
281
282     def bump_minor(self):
283         self.minor += 1
284         self.micro = 0
285
286     def bump_micro(self):
287         self.micro += 1
288
289     def to_devel(self):
290         self.devel = True
291
292     def to_release(self):
293         self.devel = False
294
295     def __str__(self):
296         s = '%d.%d.%d' % (self.major, self.minor, self.micro)
297         if self.devel:
298             s += 'devel'
299
300         return s
301
302 #
303 # Targets
304 #
305
306 class Target(object):
307     """
308     platform -- platform string (e.g. 'windows', 'linux', 'osx')
309     directory -- directory to work in; if None we will use a temporary directory
310     Temporary directories will be removed after use; specified directories will not.
311     """
312     def __init__(self, platform, directory=None):
313         self.platform = platform
314         self.parallel = int(config.get('parallel'))
315
316         # self.directory is the working directory
317         if directory is None:
318             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
319             self.rmdir = True
320         else:
321             self.directory = directory
322             self.rmdir = False
323
324         # Environment variables that we will use when we call cscripts
325         self.variables = {}
326         self.debug = False
327
328     def package(self, project, checkout):
329         tree = globals.trees.get(project, checkout, self)
330         tree.build_dependencies()
331         tree.build(tree)
332         return tree.call('package', tree.version), tree.git_commit
333
334     def test(self, tree):
335         tree.build_dependencies()
336         tree.build()
337         return tree.call('test')
338
339     def set(self, a, b):
340         self.variables[a] = b
341
342     def unset(self, a):
343         del(self.variables[a])
344
345     def get(self, a):
346         return self.variables[a]
347
348     def append_with_space(self, k, v):
349         if (not k in self.variables) or len(self.variables[k]) == 0:
350             self.variables[k] = '"%s"' % v
351         else:
352             e = self.variables[k]
353             if e[0] == '"' and e[-1] == '"':
354                 self.variables[k] = '"%s %s"' % (e[1:-1], v)
355             else:
356                 self.variables[k] = '"%s %s"' % (e, v)
357
358     def variables_string(self, escaped_quotes=False):
359         e = ''
360         for k, v in self.variables.items():
361             if escaped_quotes:
362                 v = v.replace('"', '\\"')
363             e += '%s=%s ' % (k, v)
364         return e
365
366     def cleanup(self):
367         if self.rmdir:
368             rmtree(self.directory)
369
370 #
371 # Windows
372 #
373
374 class WindowsTarget(Target):
375     def __init__(self, bits, directory=None):
376         super(WindowsTarget, self).__init__('windows', directory)
377         self.bits = bits
378
379         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
380         if not os.path.exists(self.windows_prefix):
381             raise Error('windows prefix %s does not exist' % self.windows_prefix)
382
383         if self.bits == 32:
384             self.mingw_name = 'i686'
385         else:
386             self.mingw_name = 'x86_64'
387
388         self.mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
389         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
390
391         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
392         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
393         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, self.mingw_path, os.environ['PATH']))
394         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
395         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
396         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
397         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
398         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
399         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
400         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
401         for p in self.mingw_prefixes:
402             cxx += ' -I%s/include' % p
403             link += ' -L%s/lib' % p
404         self.set('CXXFLAGS', '"%s"' % cxx)
405         self.set('CPPFLAGS', '')
406         self.set('LINKFLAGS', '"%s"' % link)
407         self.set('LDFLAGS', '"%s"' % link)
408
409     def command(self, c):
410         log('host -> %s' % c)
411         command('%s %s' % (self.variables_string(), c))
412
413 class LinuxTarget(Target):
414     """Parent for Linux targets"""
415     def __init__(self, distro, version, bits, directory=None):
416         super(LinuxTarget, self).__init__('linux', directory)
417         self.distro = distro
418         self.version = version
419         self.bits = bits
420
421         self.set('CXXFLAGS', '-I%s/include' % self.directory)
422         self.set('CPPFLAGS', '')
423         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
424         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
425         self.set('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin')
426
427 class ChrootTarget(LinuxTarget):
428     """Build in a chroot"""
429     def __init__(self, distro, version, bits, directory=None):
430         super(ChrootTarget, self).__init__(distro, version, bits, directory)
431         # e.g. ubuntu-14.04-64
432         if self.version is not None and self.bits is not None:
433             self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
434         else:
435             self.chroot = self.distro
436         # e.g. /home/carl/Environments/ubuntu-14.04-64
437         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
438
439     def command(self, c):
440         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
441
442
443 class HostTarget(LinuxTarget):
444     """Build directly on the host"""
445     def __init__(self, distro, version, bits, directory=None):
446         super(HostTarget, self).__init__(distro, version, bits, directory)
447
448     def command(self, c):
449         command('%s %s' % (self.variables_string(), c))
450
451 #
452 # OS X
453 #
454
455 class OSXTarget(Target):
456     def __init__(self, directory=None):
457         super(OSXTarget, self).__init__('osx', directory)
458         self.sdk = config.get('osx_sdk')
459         self.sdk_prefix = config.get('osx_sdk_prefix')
460
461     def command(self, c):
462         command('%s %s' % (self.variables_string(False), c))
463
464
465 class OSXSingleTarget(OSXTarget):
466     def __init__(self, bits, directory=None):
467         super(OSXSingleTarget, self).__init__(directory)
468         self.bits = bits
469
470         if bits == 32:
471             arch = 'i386'
472         else:
473             arch = 'x86_64'
474
475         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (self.sdk_prefix, self.sdk, arch)
476         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
477
478         # Environment variables
479         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
480         self.set('CPPFLAGS', '')
481         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
482         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
483         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
484         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
485         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
486         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
487
488     def package(self, project, checkout):
489         raise Error('cannot package non-universal OS X versions')
490
491
492 class OSXUniversalTarget(OSXTarget):
493     def __init__(self, directory=None):
494         super(OSXUniversalTarget, self).__init__(directory)
495
496     def package(self, project, checkout):
497
498         for b in [32, 64]:
499             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
500             tree = globals.trees.get(project, checkout, target)
501             tree.build_dependencies()
502             tree.build()
503
504         tree = globals.trees.get(project, checkout, self)
505         with TreeDirectory(tree):
506             return tree.call('package', tree.version), tree.git_commit
507
508 class SourceTarget(Target):
509     """Build a source .tar.bz2"""
510     def __init__(self):
511         super(SourceTarget, self).__init__('source')
512
513     def command(self, c):
514         log('host -> %s' % c)
515         command('%s %s' % (self.variables_string(), c))
516
517     def cleanup(self):
518         rmtree(self.directory)
519
520     def package(self, project, checkout):
521         tree = globals.trees.get(project, checkout, self)
522         with TreeDirectory(tree):
523             name = read_wscript_variable(os.getcwd(), 'APPNAME')
524             command('./waf dist')
525             return os.path.abspath('%s-%s.tar.bz2' % (name, tree.version)), tree.git_commit
526
527
528 # @param s Target string:
529 #       windows-{32,64}
530 #    or ubuntu-version-{32,64}
531 #    or debian-version-{32,64}
532 #    or centos-version-{32,64}
533 #    or osx-{32,64}
534 #    or source
535 # @param debug True to build with debugging symbols (where possible)
536 def target_factory(s, debug, work):
537     target = None
538     if s.startswith('windows-'):
539         target = WindowsTarget(int(s.split('-')[1]), work)
540     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
541         p = s.split('-')
542         if len(p) != 3:
543             raise Error("Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s)
544         target = ChrootTarget(p[0], p[1], int(p[2]), work)
545     elif s.startswith('arch-'):
546         p = s.split('-')
547         if len(p) != 2:
548             raise Error("Bad Arch target name `%s'; must be arch-32 or arch-64")
549         target = ChrootTarget(p[0], None, p[1], work)
550     elif s == 'raspbian':
551         target = ChrootTarget(s, None, None, work)
552     elif s == 'host':
553         try:
554             f = open('/etc/fedora-release', 'r')
555             l = f.readline().strip().split()
556             if command_and_read('uname -m').read().strip() == 'x86_64':
557                 bits = 64
558             else:
559                 bits = 32
560             target = HostTarget("fedora", l[2], bits, work)
561         except Exception as e:
562             raise Error("could not identify distribution for `host' target (%s)" % e)
563     elif s.startswith('osx-'):
564         target = OSXSingleTarget(int(s.split('-')[1]), work)
565     elif s == 'osx':
566         if globals.command == 'build':
567             target = OSXSingleTarget(64, work)
568         else:
569             target = OSXUniversalTarget(work)
570     elif s == 'source':
571         target = SourceTarget()
572
573     if target is None:
574         raise Error("Bad target `%s'" % s)
575
576     target.debug = debug
577     return target
578
579
580 #
581 # Tree
582 #
583
584 class Tree(object):
585     """Description of a tree, which is a checkout of a project,
586        possibly built.  This class is never exposed to cscripts.
587        Attributes:
588            name -- name of git repository (without the .git)
589            specifier -- git tag or revision to use
590            target --- target object that we are using
591            version --- version from the wscript (if one is present)
592            git_commit -- git revision that is actually being used
593            built --- true if the tree has been built yet in this run
594     """
595
596     def __init__(self, name, specifier, target):
597         self.name = name
598         self.specifier = specifier
599         self.target = target
600         self.version = None
601         self.git_commit = None
602         self.built = False
603
604         cwd = os.getcwd()
605
606         flags = ''
607         redirect = ''
608         if globals.quiet:
609             flags = '-q'
610             redirect = '>/dev/null'
611         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
612         os.chdir('%s/src/%s' % (target.directory, self.name))
613
614         spec = self.specifier
615         if spec is None:
616             spec = 'master'
617
618         command('git checkout %s %s %s' % (flags, spec, redirect))
619         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
620         command('git submodule init --quiet')
621         command('git submodule update --quiet')
622
623         proj = '%s/src/%s' % (target.directory, self.name)
624
625         self.cscript = {}
626         execfile('%s/cscript' % proj, self.cscript)
627
628         if os.path.exists('%s/wscript' % proj):
629             v = read_wscript_variable(proj, "VERSION");
630             if v is not None:
631                 self.version = Version(v)
632
633         os.chdir(cwd)
634
635     def call(self, function, *args):
636         with TreeDirectory(self):
637             return self.cscript[function](self.target, *args)
638
639     def build_dependencies(self):
640         if 'dependencies' in self.cscript:
641             for d in self.cscript['dependencies'](self.target):
642                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
643                 dep = globals.trees.get(d[0], d[1], self.target)
644                 dep.build_dependencies()
645
646                 # Make the options to pass in from the option_defaults of the thing
647                 # we are building and any options specified by the parent.
648                 options = {}
649                 if 'option_defaults' in dep.cscript:
650                     options = dep.cscript['option_defaults']()
651                     if len(d) > 2:
652                         for k, v in d[2].items():
653                             options[k] = v
654
655                 dep.build(options)
656
657     def build(self, options=None):
658         if self.built:
659             return
660
661         variables = copy.copy(self.target.variables)
662
663         if len(inspect.getargspec(self.cscript['build']).args) == 2:
664             self.call('build', options)
665         else:
666             self.call('build')
667
668         self.target.variables = variables
669         self.built = True
670
671 #
672 # Command-line parser
673 #
674
675 def main():
676
677     commands = {
678         "build": "build project",
679         "package": "package and build project",
680         "release": "release a project using its next version number (changing wscript and tagging)",
681         "pot": "build the project's .pot files",
682         "changelog": "generate a simple HTML changelog",
683         "manual": "build the project's manual",
684         "doxygen": "build the project's Doxygen documentation",
685         "latest": "print out the latest version",
686         "test": "run the project's unit tests",
687         "shell": "build the project then start a shell in its chroot",
688         "checkout": "check out the project",
689         "revision": "print the head git revision number"
690     }
691
692     one_of = "Command is one of:\n"
693     summary = ""
694     for k, v in commands.items():
695         one_of += "\t%s\t%s\n" % (k, v)
696         summary += k + " "
697
698     parser = argparse.ArgumentParser()
699     parser.add_argument('command', help=summary)
700     parser.add_argument('-p', '--project', help='project name')
701     parser.add_argument('--minor', help='minor version number bump', action='store_true')
702     parser.add_argument('--micro', help='micro version number bump', action='store_true')
703     parser.add_argument('--major', help='major version to return with latest', type=int)
704     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
705     parser.add_argument('-o', '--output', help='output directory', default='.')
706     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
707     parser.add_argument('-t', '--target', help='target')
708     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
709     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
710     parser.add_argument('-w', '--work', help='override default work directory')
711     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
712     args = parser.parse_args()
713
714     # Override configured stuff
715     if args.git_prefix is not None:
716         config.set('git_prefix', args.git_prefix)
717
718     if args.output.find(':') == -1:
719         # This isn't of the form host:path so make it absolute
720         args.output = os.path.abspath(args.output) + '/'
721     else:
722         if args.output[-1] != ':' and args.output[-1] != '/':
723             args.output += '/'
724
725     # Now, args.output is 'host:', 'host:path/' or 'path/'
726
727     if args.work is not None:
728         args.work = os.path.abspath(args.work)
729
730     if args.project is None and args.command != 'shell':
731         raise Error('you must specify -p or --project')
732
733     globals.quiet = args.quiet
734     globals.command = args.command
735
736     if not globals.command in commands:
737         e = 'command must be one of:\n' + one_of
738         raise Error('command must be one of:\n%s' % one_of)
739
740     if globals.command == 'build':
741         if args.target is None:
742             raise Error('you must specify -t or --target')
743
744         target = target_factory(args.target, args.debug, args.work)
745         tree = globals.trees.get(args.project, args.checkout, target)
746         tree.build_dependencies()
747         tree.build()
748         if not args.keep:
749             target.cleanup()
750
751     elif globals.command == 'package':
752         if args.target is None:
753             raise Error('you must specify -t or --target')
754
755         target = target_factory(args.target, args.debug, args.work)
756         packages, git_commit = target.package(args.project, args.checkout)
757         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
758             packages = [packages]
759
760         if target.platform == 'linux':
761             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
762             try:
763                 makedirs(out)
764             except:
765                 pass
766             for p in packages:
767                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
768         else:
769             try:
770                 makedirs(args.output)
771             except:
772                 pass
773             for p in packages:
774                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
775
776         if not args.keep:
777             target.cleanup()
778
779     elif globals.command == 'release':
780         if args.minor is False and args.micro is False:
781             raise Error('you must specify --minor or --micro')
782
783         target = SourceTarget()
784         tree = globals.trees.get(args.project, args.checkout, target)
785
786         version = tree.version
787         version.to_release()
788         if args.minor:
789             version.bump_minor()
790         else:
791             version.bump_micro()
792
793         set_version_in_wscript(version)
794         append_version_to_changelog(version)
795         append_version_to_debian_changelog(version)
796
797         command('git commit -a -m "Bump version"')
798         command('git tag -m "v%s" v%s' % (version, version))
799
800         version.to_devel()
801         set_version_in_wscript(version)
802         command('git commit -a -m "Bump version"')
803         command('git push')
804         command('git push --tags')
805
806         target.cleanup()
807
808     elif globals.command == 'pot':
809         target = SourceTarget()
810         tree = globals.trees.get(args.project, args.checkout, target)
811
812         pots = tree.call('make_pot')
813         for p in pots:
814             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
815
816         target.cleanup()
817
818     elif globals.command == 'changelog':
819         target = SourceTarget()
820         tree = globals.trees.get(args.project, args.checkout, target)
821
822         with TreeDirectory(tree):
823             text = open('ChangeLog', 'r')
824
825         html = tempfile.NamedTemporaryFile()
826         versions = 8
827
828         last = None
829         changes = []
830
831         while True:
832             l = text.readline()
833             if l == '':
834                 break
835
836             if len(l) > 0 and l[0] == "\t":
837                 s = l.split()
838                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
839                     v = Version(s[2])
840                     if v.micro == 0:
841                         if last is not None and len(changes) > 0:
842                             print("<h2>Changes between version %s and %s</h2>" % (s[2], last), file=html)
843                             print("<ul>", file=html)
844                             for c in changes:
845                                 print("<li>%s" % c, file=html)
846                             print("</ul>", file=html)
847                         last = s[2]
848                         changes = []
849                         versions -= 1
850                         if versions < 0:
851                             break
852                 else:
853                     c = l.strip()
854                     if len(c) > 0:
855                         if c[0] == '*':
856                             changes.append(c[2:])
857                         else:
858                             changes[-1] += " " + c
859
860         copyfile(html.file, '%schangelog.html' % args.output)
861         html.close()
862         target.cleanup()
863
864     elif globals.command == 'manual':
865         target = SourceTarget()
866         tree = globals.trees.get(args.project, args.checkout, target)
867
868         outs = tree.call('make_manual')
869         for o in outs:
870             if os.path.isfile(o):
871                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
872             else:
873                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
874
875         target.cleanup()
876
877     elif globals.command == 'doxygen':
878         target = SourceTarget()
879         tree = globals.trees.get(args.project, args.checkout, target)
880
881         dirs = tree.call('make_doxygen')
882         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
883             dirs = [dirs]
884
885         for d in dirs:
886             copytree(d, args.output)
887
888         target.cleanup()
889
890     elif globals.command == 'latest':
891         target = SourceTarget()
892         tree = globals.trees.get(args.project, args.checkout, target)
893
894         with TreeDirectory(tree):
895             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
896             latest = None
897             while latest is None:
898                 t = f.readline()
899                 m = re.compile(".*\((.*)\).*").match(t)
900                 if m:
901                     tags = m.group(1).split(', ')
902                     for t in tags:
903                         s = t.split()
904                         if len(s) > 1:
905                             t = s[1]
906                         if len(t) > 0 and t[0] == 'v':
907                             v = Version(t[1:])
908                             if args.major is None or v.major == args.major:
909                                 latest = v
910
911         print(latest)
912         target.cleanup()
913
914     elif globals.command == 'test':
915         if args.target is None:
916             raise Error('you must specify -t or --target')
917
918         target = None
919         try:
920             target = target_factory(args.target, args.debug, args.work)
921             tree = globals.trees.get(args.project, args.checkout, target)
922             with TreeDirectory(tree):
923                 target.test(tree)
924         except Error as e:
925             if target is not None:
926                 target.cleanup()
927             raise
928
929         if target is not None:
930             target.cleanup()
931
932     elif globals.command == 'shell':
933         if args.target is None:
934             raise Error('you must specify -t or --target')
935
936         target = target_factory(args.target, args.debug, args.work)
937         target.command('bash')
938
939     elif globals.command == 'revision':
940
941         target = SourceTarget()
942         tree = globals.trees.get(args.project, args.checkout, target)
943         with TreeDirectory(tree):
944             print(command_and_read('git rev-parse HEAD').readline().strip()[:7])
945         target.cleanup()
946
947     elif globals.command == 'checkout':
948
949         if args.output is None:
950             raise Error('you must specify -o or --output')
951
952         target = SourceTarget()
953         tree = globals.trees.get(args.project, args.checkout, target)
954         with TreeDirectory(tree):
955             shutil.copytree('.', args.output)
956         target.cleanup()
957
958     else:
959         raise Error('invalid command %s' % globals.command)
960
961 try:
962     main()
963 except Error as e:
964     print('cdist: %s' % str(e), file=sys.stderr)
965     sys.exit(1)