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