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