Expose some more stuff to OSXTarget.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2015 Carl Hetherington <cth@carlh.net>
4 #
5 #    This program is free software; you can redistribute it and/or modify
6 #    it under the terms of the GNU General Public License as published by
7 #    the Free Software Foundation; either version 2 of the License, or
8 #    (at your option) any later version.
9 #
10 #    This program is distributed in the hope that it will be useful,
11 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #    GNU General Public License for more details.
14
15 #    You should have received a copy of the GNU General Public License
16 #    along with this program; if not, write to the Free Software
17 #    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 import os
20 import sys
21 import shutil
22 import glob
23 import tempfile
24 import argparse
25 import datetime
26 import subprocess
27 import re
28 import copy
29 import inspect
30
31 TEMPORARY_DIRECTORY = '/var/tmp'
32
33 class Error(Exception):
34     def __init__(self, value):
35         self.value = value
36     def __str__(self):
37         return self.value
38     def __repr__(self):
39         return str(self)
40
41 class Trees:
42     """
43     Store for Tree objects which re-uses already-created objects
44     and checks for requests for different versions of the same thing.
45     """
46
47     def __init__(self):
48         self.trees = []
49
50     def get(self, name, specifier, target):
51         for t in self.trees:
52             if t.name == name and t.specifier == specifier and t.target == target:
53                 return t
54             elif t.name == name and t.specifier != specifier:
55                 raise Error('conflicting versions of %s requested (%s and %s)' % (name, specifier, t.specifier))
56
57         nt = Tree(name, specifier, target)
58         self.trees.append(nt)
59         return nt
60
61 class Globals:
62     quiet = False
63     command = None
64     trees = Trees()
65
66 globals = Globals()
67
68
69 #
70 # Configuration
71 #
72
73 class Option(object):
74     def __init__(self, key, default=None):
75         self.key = key
76         self.value = default
77
78     def offer(self, key, value):
79         if key == self.key:
80             self.value = value
81
82 class BoolOption(object):
83     def __init__(self, key):
84         self.key = key
85         self.value = False
86
87     def offer(self, key, value):
88         if key == self.key:
89             self.value = (value == 'yes' or value == '1' or value == 'true')
90
91 class Config:
92     def __init__(self):
93         self.options = [ Option('linux_chroot_prefix'),
94                          Option('windows_environment_prefix'),
95                          Option('mingw_prefix'),
96                          Option('git_prefix'),
97                          Option('osx_build_host'),
98                          Option('osx_environment_prefix'),
99                          Option('osx_sdk_prefix'),
100                          Option('osx_sdk'),
101                          Option('parallel', 4) ]
102
103         try:
104             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
105             while True:
106                 l = f.readline()
107                 if l == '':
108                     break
109
110                 if len(l) > 0 and l[0] == '#':
111                     continue
112
113                 s = l.strip().split()
114                 if len(s) == 2:
115                     for k in self.options:
116                         k.offer(s[0], s[1])
117         except:
118             raise
119
120     def get(self, k):
121         for o in self.options:
122             if o.key == k:
123                 return o.value
124
125         raise Error('Required setting %s not found' % k)
126
127     def set(self, k, v):
128         for o in self.options:
129             o.offer(k, v)
130
131 config = Config()
132
133 #
134 # Utility bits
135 #
136
137 def log(m):
138     if not globals.quiet:
139         print '\x1b[33m* %s\x1b[0m' % m
140
141 def scp_escape(n):
142     s = n.split(':')
143     assert(len(s) == 1 or len(s) == 2)
144     if len(s) == 2:
145         return '%s:"\'%s\'"' % (s[0], s[1])
146     else:
147         return '\"%s\"' % s[0]
148
149 def copytree(a, b):
150     log('copy %s -> %s' % (scp_escape(b), scp_escape(b)))
151     command('scp -r %s %s' % (scp_escape(a), scp_escape(b)))
152
153 def copyfile(a, b):
154     log('copy %s -> %s' % (scp_escape(a), scp_escape(b)))
155     command('scp %s %s' % (scp_escape(a), scp_escape(b)))
156
157 def makedirs(d):
158     if d.find(':') == -1:
159         os.makedirs(d)
160     else:
161         s = d.split(':')
162         command('ssh %s -- mkdir -p %s' % (s[0], s[1]))
163
164 def rmdir(a):
165     log('remove %s' % a)
166     os.rmdir(a)
167
168 def rmtree(a):
169     log('remove %s' % a)
170     shutil.rmtree(a, ignore_errors=True)
171
172 def command(c):
173     log(c)
174     r = os.system(c)
175     if (r >> 8):
176         raise Error('command %s failed' % c)
177
178 def command_and_read(c):
179     log(c)
180     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
181     f = os.fdopen(os.dup(p.stdout.fileno()))
182     return f
183
184 def read_wscript_variable(directory, variable):
185     f = open('%s/wscript' % directory, 'r')
186     while True:
187         l = f.readline()
188         if l == '':
189             break
190
191         s = l.split()
192         if len(s) == 3 and s[0] == variable:
193             f.close()
194             return s[2][1:-1]
195
196     f.close()
197     return None
198
199 def set_version_in_wscript(version):
200     f = open('wscript', 'rw')
201     o = open('wscript.tmp', 'w')
202     while True:
203         l = f.readline()
204         if l == '':
205             break
206
207         s = l.split()
208         if len(s) == 3 and s[0] == "VERSION":
209             print "Writing %s" % version
210             print >>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         self.sdk = config.get('osx_sdk')
459         self.sdk_prefix = config.get('osx_sdk_prefix')
460
461     def command(self, c):
462         command('%s %s' % (self.variables_string(False), c))
463
464
465 class OSXSingleTarget(OSXTarget):
466     def __init__(self, bits, directory=None):
467         super(OSXSingleTarget, self).__init__(directory)
468         self.bits = bits
469
470         if bits == 32:
471             arch = 'i386'
472         else:
473             arch = 'x86_64'
474
475         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (self.sdk_prefix, self.osx_sdk, arch)
476         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
477
478         # Environment variables
479         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
480         self.set('CPPFLAGS', '')
481         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
482         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
483         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
484         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
485         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
486         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
487
488     def package(self, project, checkout):
489         raise Error('cannot package non-universal OS X versions')
490
491
492 class OSXUniversalTarget(OSXTarget):
493     def __init__(self, directory=None):
494         super(OSXUniversalTarget, self).__init__(directory)
495
496     def package(self, project, checkout):
497
498         for b in [32, 64]:
499             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
500             tree = globals.trees.get(project, checkout, target)
501             tree.build_dependencies()
502             tree.build()
503
504         tree = globals.trees.get(project, checkout, self)
505         with TreeDirectory(tree):
506             return tree.call('package', tree.version), tree.git_commit
507
508 class SourceTarget(Target):
509     """Build a source .tar.bz2"""
510     def __init__(self):
511         super(SourceTarget, self).__init__('source')
512
513     def command(self, c):
514         log('host -> %s' % c)
515         command('%s %s' % (self.variables_string(), c))
516
517     def cleanup(self):
518         rmtree(self.directory)
519
520     def package(self, project, checkout):
521         tree = globals.trees.get(project, checkout, self)
522         with TreeDirectory(tree):
523             name = read_wscript_variable(os.getcwd(), 'APPNAME')
524             command('./waf dist')
525             return os.path.abspath('%s-%s.tar.bz2' % (name, tree.version)), tree.git_commit
526
527
528 # @param s Target string:
529 #       windows-{32,64}
530 #    or ubuntu-version-{32,64}
531 #    or debian-version-{32,64}
532 #    or centos-version-{32,64}
533 #    or osx-{32,64}
534 #    or source
535 # @param debug True to build with debugging symbols (where possible)
536 def target_factory(s, debug, work):
537     target = None
538     if s.startswith('windows-'):
539         target = WindowsTarget(int(s.split('-')[1]), work)
540     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
541         p = s.split('-')
542         if len(p) != 3:
543             raise Error("Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s)
544         target = ChrootTarget(p[0], p[1], int(p[2]), work)
545     elif s == 'raspbian':
546         target = ChrootTarget(s, None, None, work)
547     elif s == 'host':
548         try:
549             f = open('/etc/fedora-release', 'r')
550             l = f.readline().strip().split()
551             if command_and_read('uname -m').read().strip() == 'x86_64':
552                 bits = 64
553             else:
554                 bits = 32
555             target = HostTarget("fedora", l[2], bits, work)
556         except Exception as e:
557             raise Error("could not identify distribution for `host' target (%s)" % e)
558     elif s.startswith('osx-'):
559         target = OSXSingleTarget(int(s.split('-')[1]), work)
560     elif s == 'osx':
561         if globals.command == 'build':
562             target = OSXSingleTarget(64, work)
563         else:
564             target = OSXUniversalTarget(work)
565     elif s == 'source':
566         target = SourceTarget()
567
568     if target is None:
569         raise Error("Bad target `%s'" % s)
570
571     target.debug = debug
572     return target
573
574
575 #
576 # Tree
577 #
578
579 class Tree(object):
580     """Description of a tree, which is a checkout of a project,
581        possibly built.  This class is never exposed to cscripts.
582        Attributes:
583            name -- name of git repository (without the .git)
584            specifier -- git tag or revision to use
585            target --- target object that we are using
586            version --- version from the wscript (if one is present)
587            git_commit -- git revision that is actually being used
588            built --- true if the tree has been built yet in this run
589     """
590
591     def __init__(self, name, specifier, target):
592         self.name = name
593         self.specifier = specifier
594         self.target = target
595         self.version = None
596         self.git_commit = None
597         self.built = False
598
599         cwd = os.getcwd()
600
601         flags = ''
602         redirect = ''
603         if globals.quiet:
604             flags = '-q'
605             redirect = '>/dev/null'
606         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
607         os.chdir('%s/src/%s' % (target.directory, self.name))
608
609         spec = self.specifier
610         if spec is None:
611             spec = 'master'
612
613         command('git checkout %s %s %s' % (flags, spec, redirect))
614         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
615         command('git submodule init --quiet')
616         command('git submodule update --quiet')
617
618         proj = '%s/src/%s' % (target.directory, self.name)
619
620         self.cscript = {}
621         execfile('%s/cscript' % proj, self.cscript)
622
623         if os.path.exists('%s/wscript' % proj):
624             v = read_wscript_variable(proj, "VERSION");
625             if v is not None:
626                 self.version = Version(v)
627
628         os.chdir(cwd)
629
630     def call(self, function, *args):
631         with TreeDirectory(self):
632             return self.cscript[function](self.target, *args)
633
634     def build_dependencies(self):
635         if 'dependencies' in self.cscript:
636             for d in self.cscript['dependencies'](self.target):
637                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
638                 dep = globals.trees.get(d[0], d[1], self.target)
639                 dep.build_dependencies()
640
641                 # Make the options to pass in from the option_defaults of the thing
642                 # we are building and any options specified by the parent.
643                 options = {}
644                 if 'option_defaults' in dep.cscript:
645                     options = dep.cscript['option_defaults']()
646                     if len(d) > 2:
647                         for k, v in d[2].iteritems():
648                             options[k] = v
649
650                 dep.build(options)
651
652     def build(self, options=None):
653         if self.built:
654             return
655
656         variables = copy.copy(self.target.variables)
657
658         if len(inspect.getargspec(self.cscript['build']).args) == 2:
659             self.call('build', options)
660         else:
661             self.call('build')
662
663         self.target.variables = variables
664         self.built = True
665
666 #
667 # Command-line parser
668 #
669
670 def main():
671
672     commands = {
673         "build": "build project",
674         "package": "package and build project",
675         "release": "release a project using its next version number (changing wscript and tagging)",
676         "pot": "build the project's .pot files",
677         "changelog": "generate a simple HTML changelog",
678         "manual": "build the project's manual",
679         "doxygen": "build the project's Doxygen documentation",
680         "latest": "print out the latest version",
681         "test": "run the project's unit tests",
682         "shell": "build the project then start a shell in its chroot",
683         "checkout": "check out the project",
684         "revision": "print the head git revision number"
685     }
686
687     one_of = "Command is one of:\n"
688     summary = ""
689     for k, v in commands.iteritems():
690         one_of += "\t%s\t%s\n" % (k, v)
691         summary += k + " "
692
693     parser = argparse.ArgumentParser()
694     parser.add_argument('command', help=summary)
695     parser.add_argument('-p', '--project', help='project name')
696     parser.add_argument('--minor', help='minor version number bump', action='store_true')
697     parser.add_argument('--micro', help='micro version number bump', action='store_true')
698     parser.add_argument('--major', help='major version to return with latest', type=int)
699     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
700     parser.add_argument('-o', '--output', help='output directory', default='.')
701     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
702     parser.add_argument('-t', '--target', help='target')
703     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
704     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
705     parser.add_argument('-w', '--work', help='override default work directory')
706     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
707     args = parser.parse_args()
708
709     # Override configured stuff
710     if args.git_prefix is not None:
711         config.set('git_prefix', args.git_prefix)
712
713     if args.output.find(':') == -1:
714         # This isn't of the form host:path so make it absolute
715         args.output = os.path.abspath(args.output) + '/'
716     else:
717         if args.output[-1] != ':' and args.output[-1] != '/':
718             args.output += '/'
719
720     # Now, args.output is 'host:', 'host:path/' or 'path/'
721
722     if args.work is not None:
723         args.work = os.path.abspath(args.work)
724
725     if args.project is None and args.command != 'shell':
726         raise Error('you must specify -p or --project')
727
728     globals.quiet = args.quiet
729     globals.command = args.command
730
731     if not globals.command in commands:
732         e = 'command must be one of:\n' + one_of
733         raise Error('command must be one of:\n%s' % one_of)
734
735     if globals.command == 'build':
736         if args.target is None:
737             raise Error('you must specify -t or --target')
738
739         target = target_factory(args.target, args.debug, args.work)
740         tree = globals.trees.get(args.project, args.checkout, target)
741         tree.build_dependencies()
742         tree.build()
743         if not args.keep:
744             target.cleanup()
745
746     elif globals.command == 'package':
747         if args.target is None:
748             raise Error('you must specify -t or --target')
749
750         target = target_factory(args.target, args.debug, args.work)
751         packages, git_commit = target.package(args.project, args.checkout)
752         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
753             packages = [packages]
754
755         if target.platform == 'linux':
756             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
757             try:
758                 makedirs(out)
759             except:
760                 pass
761             for p in packages:
762                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
763         else:
764             try:
765                 makedirs(args.output)
766             except:
767                 pass
768             for p in packages:
769                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
770
771         if not args.keep:
772             target.cleanup()
773
774     elif globals.command == 'release':
775         if args.minor is False and args.micro is False:
776             raise Error('you must specify --minor or --micro')
777
778         target = SourceTarget()
779         tree = globals.trees.get(args.project, args.checkout, target)
780
781         version = tree.version
782         version.to_release()
783         if args.minor:
784             version.bump_minor()
785         else:
786             version.bump_micro()
787
788         set_version_in_wscript(version)
789         append_version_to_changelog(version)
790         append_version_to_debian_changelog(version)
791
792         command('git commit -a -m "Bump version"')
793         command('git tag -m "v%s" v%s' % (version, version))
794
795         version.to_devel()
796         set_version_in_wscript(version)
797         command('git commit -a -m "Bump version"')
798         command('git push')
799         command('git push --tags')
800
801         target.cleanup()
802
803     elif globals.command == 'pot':
804         target = SourceTarget()
805         tree = globals.trees.get(args.project, args.checkout, target)
806
807         pots = tree.call('make_pot')
808         for p in pots:
809             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
810
811         target.cleanup()
812
813     elif globals.command == 'changelog':
814         target = SourceTarget()
815         tree = globals.trees.get(args.project, args.checkout, target)
816
817         with TreeDirectory(tree):
818             text = open('ChangeLog', 'r')
819
820         html = tempfile.NamedTemporaryFile()
821         versions = 8
822
823         last = None
824         changes = []
825
826         while True:
827             l = text.readline()
828             if l == '':
829                 break
830
831             if len(l) > 0 and l[0] == "\t":
832                 s = l.split()
833                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
834                     v = Version(s[2])
835                     if v.micro == 0:
836                         if last is not None and len(changes) > 0:
837                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
838                             print >>html,"<ul>"
839                             for c in changes:
840                                 print >>html,"<li>%s" % c
841                             print >>html,"</ul>"
842                         last = s[2]
843                         changes = []
844                         versions -= 1
845                         if versions < 0:
846                             break
847                 else:
848                     c = l.strip()
849                     if len(c) > 0:
850                         if c[0] == '*':
851                             changes.append(c[2:])
852                         else:
853                             changes[-1] += " " + c
854
855         copyfile(html.file, '%schangelog.html' % args.output)
856         html.close()
857         target.cleanup()
858
859     elif globals.command == 'manual':
860         target = SourceTarget()
861         tree = globals.trees.get(args.project, args.checkout, target)
862
863         outs = tree.call('make_manual')
864         for o in outs:
865             if os.path.isfile(o):
866                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
867             else:
868                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
869
870         target.cleanup()
871
872     elif globals.command == 'doxygen':
873         target = SourceTarget()
874         tree = globals.trees.get(args.project, args.checkout, target)
875
876         dirs = tree.call('make_doxygen')
877         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
878             dirs = [dirs]
879
880         for d in dirs:
881             copytree(d, args.output)
882
883         target.cleanup()
884
885     elif globals.command == 'latest':
886         target = SourceTarget()
887         tree = globals.trees.get(args.project, args.checkout, target)
888
889         with TreeDirectory(tree):
890             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
891             latest = None
892             while latest is None:
893                 t = f.readline()
894                 m = re.compile(".*\((.*)\).*").match(t)
895                 if m:
896                     tags = m.group(1).split(', ')
897                     for t in tags:
898                         s = t.split()
899                         if len(s) > 1:
900                             t = s[1]
901                         if len(t) > 0 and t[0] == 'v':
902                             v = Version(t[1:])
903                             if args.major is None or v.major == args.major:
904                                 latest = v
905
906         print latest
907         target.cleanup()
908
909     elif globals.command == 'test':
910         if args.target is None:
911             raise Error('you must specify -t or --target')
912
913         target = None
914         try:
915             target = target_factory(args.target, args.debug, args.work)
916             tree = globals.trees.get(args.project, args.checkout, target)
917             with TreeDirectory(tree):
918                 target.test(tree)
919         except Error as e:
920             if target is not None:
921                 target.cleanup()
922             raise
923
924         if target is not None:
925             target.cleanup()
926
927     elif globals.command == 'shell':
928         if args.target is None:
929             raise Error('you must specify -t or --target')
930
931         target = target_factory(args.target, args.debug, args.work)
932         target.command('bash')
933
934     elif globals.command == 'revision':
935
936         target = SourceTarget()
937         tree = globals.trees.get(args.project, args.checkout, target)
938         with TreeDirectory(tree):
939             print command_and_read('git rev-parse HEAD').readline().strip()[:7]
940         target.cleanup()
941
942     elif globals.command == 'checkout':
943
944         if args.output is None:
945             raise Error('you must specify -o or --output')
946
947         target = SourceTarget()
948         tree = globals.trees.get(args.project, args.checkout, target)
949         with TreeDirectory(tree):
950             shutil.copytree('.', args.output)
951         target.cleanup()
952
953     else:
954         raise Error('invalid command %s' % globals.command)
955
956 try:
957     main()
958 except Error as e:
959     print >>sys.stderr,'cdist: %s' % str(e)
960     sys.exit(1)