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