Add environment_prefix to OSX target and rename the Windows equivalent.
[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                 if o.value is None:
125                     raise Error('Required setting %s not found' % k)
126                 return o.value
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     Class representing the target that we are building for.  This is exposed to cscripts,
310     though not all of it is guaranteed 'API'.  cscripts may expect:
311
312     platform: platform string (e.g. 'windows', 'linux', 'osx')
313     parallel: number of parallel jobs to run
314     directory: directory to work in
315     variables: dict of environment variables
316     debug: True to build a debug version, otherwise False
317     set(a, b): set the value of variable 'a' to 'b'
318     unset(a): unset the value of variable 'a'
319     command(c): run the command 'c' in the build environment
320
321     """
322
323     def __init__(self, platform, directory=None):
324         """
325         platform -- platform string (e.g. 'windows', 'linux', 'osx')
326         directory -- directory to work in; if None we will use a temporary directory
327         Temporary directories will be removed after use; specified directories will not.
328         """
329         self.platform = platform
330         self.parallel = int(config.get('parallel'))
331
332         # self.directory is the working directory
333         if directory is None:
334             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
335             self.rmdir = True
336         else:
337             self.directory = directory
338             self.rmdir = False
339
340         # Environment variables that we will use when we call cscripts
341         self.variables = {}
342         self.debug = False
343
344     def package(self, project, checkout):
345         tree = globals.trees.get(project, checkout, self)
346         tree.build_dependencies()
347         tree.build(tree)
348         return tree.call('package', tree.version), tree.git_commit
349
350     def test(self, tree, test):
351         """test is the test case to run, or None"""
352         tree.build_dependencies()
353         tree.build()
354         return tree.call('test', test)
355
356     def set(self, a, b):
357         self.variables[a] = b
358
359     def unset(self, a):
360         del(self.variables[a])
361
362     def get(self, a):
363         return self.variables[a]
364
365     def append_with_space(self, k, v):
366         if (not k in self.variables) or len(self.variables[k]) == 0:
367             self.variables[k] = '"%s"' % v
368         else:
369             e = self.variables[k]
370             if e[0] == '"' and e[-1] == '"':
371                 self.variables[k] = '"%s %s"' % (e[1:-1], v)
372             else:
373                 self.variables[k] = '"%s %s"' % (e, v)
374
375     def variables_string(self, escaped_quotes=False):
376         e = ''
377         for k, v in self.variables.items():
378             if escaped_quotes:
379                 v = v.replace('"', '\\"')
380             e += '%s=%s ' % (k, v)
381         return e
382
383     def cleanup(self):
384         if self.rmdir:
385             rmtree(self.directory)
386
387
388 class WindowsTarget(Target):
389     """
390     This target exposes the following additional API:
391
392     version: Windows version ('xp' or None)
393     bits: bitness of Windows (32 or 64)
394     environment_prefix: path to Windows environment
395     mingw_path: path to mingw binaries
396     """
397     def __init__(self, version, bits, directory=None):
398         super(WindowsTarget, self).__init__('windows', directory)
399         self.version = version
400         self.bits = bits
401
402         self.environment_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
403         if not os.path.exists(self.environment_prefix):
404             raise Error('environment prefix %s does not exist' % self.environment_prefix)
405
406         if self.bits == 32:
407             self.mingw_name = 'i686'
408         else:
409             self.mingw_name = 'x86_64'
410
411         self.mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
412         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
413
414         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.environment_prefix)
415         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
416         self.set('PATH', '%s/bin:%s:%s' % (self.environment_prefix, self.mingw_path, os.environ['PATH']))
417         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
418         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
419         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
420         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
421         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
422         cxx = '-I%s/include -I%s/include' % (self.environment_prefix, self.directory)
423         link = '-L%s/lib -L%s/lib' % (self.environment_prefix, self.directory)
424         for p in self.mingw_prefixes:
425             cxx += ' -I%s/include' % p
426             link += ' -L%s/lib' % p
427         self.set('CXXFLAGS', '"%s"' % cxx)
428         self.set('CPPFLAGS', '')
429         self.set('LINKFLAGS', '"%s"' % link)
430         self.set('LDFLAGS', '"%s"' % link)
431
432         # This is for backwards-compatibility
433         self.windows_prefix = self.environment_prefix
434
435     def command(self, c):
436         log('host -> %s' % c)
437         command('%s %s' % (self.variables_string(), c))
438
439 class LinuxTarget(Target):
440     """
441     Parent for Linux targets.
442     This target exposes the following additional API:
443
444     distro: distribution ('debian', 'ubuntu', 'centos' or 'fedora')
445     version: distribution version (e.g. '12.04', '8', '6.5')
446     bits: bitness of the distribution (32 or 64)
447     """
448
449     def __init__(self, distro, version, bits, directory=None):
450         super(LinuxTarget, self).__init__('linux', directory)
451         self.distro = distro
452         self.version = version
453         self.bits = bits
454
455         self.set('CXXFLAGS', '-I%s/include' % self.directory)
456         self.set('CPPFLAGS', '')
457         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
458         self.set('PKG_CONFIG_PATH',
459                  '%s/lib/pkgconfig:%s/lib64/pkgconfig:/usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig' % (self.directory, self.directory))
460         self.set('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin')
461
462 class ChrootTarget(LinuxTarget):
463     """Build in a chroot"""
464     def __init__(self, distro, version, bits, directory=None):
465         super(ChrootTarget, self).__init__(distro, version, bits, directory)
466         # e.g. ubuntu-14.04-64
467         if self.version is not None and self.bits is not None:
468             self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
469         else:
470             self.chroot = self.distro
471         # e.g. /home/carl/Environments/ubuntu-14.04-64
472         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
473
474     def command(self, c):
475         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
476
477
478 class HostTarget(LinuxTarget):
479     """Build directly on the host"""
480     def __init__(self, distro, version, bits, directory=None):
481         super(HostTarget, self).__init__(distro, version, bits, directory)
482
483     def command(self, c):
484         command('%s %s' % (self.variables_string(), c))
485
486
487 class DockerTarget(Target):
488     """
489     Build a Docker image.
490
491     This target exposes the following additional API:
492
493     deb: path to Debian 8 .deb
494     """
495     def __init__(self, directory=None):
496         super(DockerTarget, self).__init__('docker', directory)
497         self.debian = ChrootTarget('debian', '8', 64, directory)
498
499     def command(self, c):
500         log('host -> %s' % c)
501         command('%s %s' % (self.variables_string(), c))
502
503     def package(self, project, checkout):
504         self.deb = self.debian.package(project, checkout)
505         return globals.trees.get(project, checkout, self).call('package', tree.version), tree.git_commit
506
507
508 class OSXTarget(Target):
509     def __init__(self, directory=None):
510         super(OSXTarget, self).__init__('osx', directory)
511         self.sdk = config.get('osx_sdk')
512         self.sdk_prefix = config.get('osx_sdk_prefix')
513         self.environment_prefix = config.get('osx_environment_prefix')
514
515     def command(self, c):
516         command('%s %s' % (self.variables_string(False), c))
517
518
519 class OSXSingleTarget(OSXTarget):
520     def __init__(self, bits, directory=None):
521         super(OSXSingleTarget, self).__init__(directory)
522         self.bits = bits
523
524         if bits == 32:
525             arch = 'i386'
526         else:
527             arch = 'x86_64'
528
529         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (self.sdk_prefix, self.sdk, arch)
530         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
531
532         # Environment variables
533         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
534         self.set('CPPFLAGS', '')
535         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
536         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
537         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
538         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
539         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
540         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
541
542     def package(self, project, checkout):
543         raise Error('cannot package non-universal OS X versions')
544
545
546 class OSXUniversalTarget(OSXTarget):
547     def __init__(self, directory=None):
548         super(OSXUniversalTarget, self).__init__(directory)
549
550     def package(self, project, checkout):
551
552         for b in [32, 64]:
553             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
554             tree = globals.trees.get(project, checkout, target)
555             tree.build_dependencies()
556             tree.build()
557
558         tree = globals.trees.get(project, checkout, self)
559         with TreeDirectory(tree):
560             return tree.call('package', tree.version), tree.git_commit
561
562 class SourceTarget(Target):
563     """Build a source .tar.bz2"""
564     def __init__(self):
565         super(SourceTarget, self).__init__('source')
566
567     def command(self, c):
568         log('host -> %s' % c)
569         command('%s %s' % (self.variables_string(), c))
570
571     def cleanup(self):
572         rmtree(self.directory)
573
574     def package(self, project, checkout):
575         tree = globals.trees.get(project, checkout, self)
576         with TreeDirectory(tree):
577             name = read_wscript_variable(os.getcwd(), 'APPNAME')
578             command('./waf dist')
579             return os.path.abspath('%s-%s.tar.bz2' % (name, tree.version)), tree.git_commit
580
581
582 # @param s Target string:
583 #       windows-{32,64}
584 #    or ubuntu-version-{32,64}
585 #    or debian-version-{32,64}
586 #    or centos-version-{32,64}
587 #    or osx-{32,64}
588 #    or source
589 # @param debug True to build with debugging symbols (where possible)
590 def target_factory(s, debug, work):
591     target = None
592     if s.startswith('windows-'):
593         x = s.split('-')
594         if len(x) == 2:
595             target = WindowsTarget(None, int(x[1]), work)
596         elif len(x) == 3:
597             target = WindowsTarget(x[1], int(x[2]), work)
598         else:
599             raise Error("Bad Windows target name `%s'")
600     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
601         p = s.split('-')
602         if len(p) != 3:
603             raise Error("Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s)
604         target = ChrootTarget(p[0], p[1], int(p[2]), work)
605     elif s.startswith('arch-'):
606         p = s.split('-')
607         if len(p) != 2:
608             raise Error("Bad Arch target name `%s'; must be arch-32 or arch-64")
609         target = ChrootTarget(p[0], None, p[1], work)
610     elif s == 'raspbian':
611         target = ChrootTarget(s, None, None, work)
612     elif s == 'host':
613         if command_and_read('uname -m').read().strip() == 'x86_64':
614             bits = 64
615         else:
616             bits = 32
617         try:
618             f = open('/etc/fedora-release', 'r')
619             l = f.readline().strip().split()
620             target = HostTarget("fedora", l[2], bits, work)
621         except Exception as e:
622             if os.path.exists('/etc/arch-release'):
623                 target = HostTarget("arch", None, bits, work)
624             else:
625                 raise Error("could not identify distribution for `host' target (%s)" % e)
626     elif s.startswith('osx-'):
627         target = OSXSingleTarget(int(s.split('-')[1]), work)
628     elif s == 'osx':
629         if globals.command == 'build':
630             target = OSXSingleTarget(64, work)
631         else:
632             target = OSXUniversalTarget(work)
633     elif s == 'source':
634         target = SourceTarget()
635     elif s == 'docker':
636         target = DockerTarget()
637
638     if target is None:
639         raise Error("Bad target `%s'" % s)
640
641     target.debug = debug
642     return target
643
644
645 #
646 # Tree
647 #
648
649 class Tree(object):
650     """Description of a tree, which is a checkout of a project,
651        possibly built.  This class is never exposed to cscripts.
652        Attributes:
653            name -- name of git repository (without the .git)
654            specifier -- git tag or revision to use
655            target --- target object that we are using
656            version --- version from the wscript (if one is present)
657            git_commit -- git revision that is actually being used
658            built --- true if the tree has been built yet in this run
659     """
660
661     def __init__(self, name, specifier, target):
662         self.name = name
663         self.specifier = specifier
664         self.target = target
665         self.version = None
666         self.git_commit = None
667         self.built = False
668
669         cwd = os.getcwd()
670
671         flags = ''
672         redirect = ''
673         if globals.quiet:
674             flags = '-q'
675             redirect = '>/dev/null'
676         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
677         os.chdir('%s/src/%s' % (target.directory, self.name))
678
679         spec = self.specifier
680         if spec is None:
681             spec = 'master'
682
683         command('git checkout %s %s %s' % (flags, spec, redirect))
684         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
685         command('git submodule init --quiet')
686         command('git submodule update --quiet')
687
688         proj = '%s/src/%s' % (target.directory, self.name)
689
690         self.cscript = {}
691         exec(open('%s/cscript' % proj).read(), self.cscript)
692
693         if os.path.exists('%s/wscript' % proj):
694             v = read_wscript_variable(proj, "VERSION");
695             if v is not None:
696                 self.version = Version(v)
697
698         os.chdir(cwd)
699
700     def call(self, function, *args):
701         with TreeDirectory(self):
702             return self.cscript[function](self.target, *args)
703
704     def build_dependencies(self):
705         if 'dependencies' in self.cscript:
706             for d in self.cscript['dependencies'](self.target):
707                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
708                 dep = globals.trees.get(d[0], d[1], self.target)
709                 dep.build_dependencies()
710
711                 # Make the options to pass in from the option_defaults of the thing
712                 # we are building and any options specified by the parent.
713                 options = {}
714                 if 'option_defaults' in dep.cscript:
715                     options = dep.cscript['option_defaults']()
716                     if len(d) > 2:
717                         for k, v in d[2].items():
718                             options[k] = v
719
720                 dep.build(options)
721
722     def build(self, options=None):
723         if self.built:
724             return
725
726         variables = copy.copy(self.target.variables)
727
728         if len(inspect.getargspec(self.cscript['build']).args) == 2:
729             self.call('build', options)
730         else:
731             self.call('build')
732
733         self.target.variables = variables
734         self.built = True
735
736 #
737 # Command-line parser
738 #
739
740 def main():
741
742     commands = {
743         "build": "build project",
744         "package": "package and build project",
745         "release": "release a project using its next version number (changing wscript and tagging)",
746         "pot": "build the project's .pot files",
747         "changelog": "generate a simple HTML changelog",
748         "manual": "build the project's manual",
749         "doxygen": "build the project's Doxygen documentation",
750         "latest": "print out the latest version",
751         "test": "run the project's unit tests",
752         "shell": "build the project then start a shell in its chroot",
753         "checkout": "check out the project",
754         "revision": "print the head git revision number"
755     }
756
757     one_of = "Command is one of:\n"
758     summary = ""
759     for k, v in commands.items():
760         one_of += "\t%s\t%s\n" % (k, v)
761         summary += k + " "
762
763     parser = argparse.ArgumentParser()
764     parser.add_argument('command', help=summary)
765     parser.add_argument('-p', '--project', help='project name')
766     parser.add_argument('--minor', help='minor version number bump', action='store_true')
767     parser.add_argument('--micro', help='micro version number bump', action='store_true')
768     parser.add_argument('--major', help='major version to return with latest', type=int)
769     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
770     parser.add_argument('-o', '--output', help='output directory', default='.')
771     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
772     parser.add_argument('-t', '--target', help='target')
773     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
774     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
775     parser.add_argument('-w', '--work', help='override default work directory')
776     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
777     parser.add_argument('--test', help='name of test to run (with `test''), defaults to all')
778     args = parser.parse_args()
779
780     # Override configured stuff
781     if args.git_prefix is not None:
782         config.set('git_prefix', args.git_prefix)
783
784     if args.output.find(':') == -1:
785         # This isn't of the form host:path so make it absolute
786         args.output = os.path.abspath(args.output) + '/'
787     else:
788         if args.output[-1] != ':' and args.output[-1] != '/':
789             args.output += '/'
790
791     # Now, args.output is 'host:', 'host:path/' or 'path/'
792
793     if args.work is not None:
794         args.work = os.path.abspath(args.work)
795
796     if args.project is None and args.command != 'shell':
797         raise Error('you must specify -p or --project')
798
799     globals.quiet = args.quiet
800     globals.command = args.command
801
802     if not globals.command in commands:
803         e = 'command must be one of:\n' + one_of
804         raise Error('command must be one of:\n%s' % one_of)
805
806     if globals.command == 'build':
807         if args.target is None:
808             raise Error('you must specify -t or --target')
809
810         target = target_factory(args.target, args.debug, args.work)
811         tree = globals.trees.get(args.project, args.checkout, target)
812         tree.build_dependencies()
813         tree.build()
814         if not args.keep:
815             target.cleanup()
816
817     elif globals.command == 'package':
818         if args.target is None:
819             raise Error('you must specify -t or --target')
820
821         target = target_factory(args.target, args.debug, args.work)
822         packages, git_commit = target.package(args.project, args.checkout)
823         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
824             packages = [packages]
825
826         if target.platform == 'linux':
827             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
828             try:
829                 makedirs(out)
830             except:
831                 pass
832             for p in packages:
833                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
834         else:
835             try:
836                 makedirs(args.output)
837             except:
838                 pass
839             for p in packages:
840                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
841
842         if not args.keep:
843             target.cleanup()
844
845     elif globals.command == 'release':
846         if args.minor is False and args.micro is False:
847             raise Error('you must specify --minor or --micro')
848
849         target = SourceTarget()
850         tree = globals.trees.get(args.project, args.checkout, target)
851
852         version = tree.version
853         version.to_release()
854         if args.minor:
855             version.bump_minor()
856         else:
857             version.bump_micro()
858
859         set_version_in_wscript(version)
860         append_version_to_changelog(version)
861         append_version_to_debian_changelog(version)
862
863         command('git commit -a -m "Bump version"')
864         command('git tag -m "v%s" v%s' % (version, version))
865
866         version.to_devel()
867         set_version_in_wscript(version)
868         command('git commit -a -m "Bump version"')
869         command('git push')
870         command('git push --tags')
871
872         target.cleanup()
873
874     elif globals.command == 'pot':
875         target = SourceTarget()
876         tree = globals.trees.get(args.project, args.checkout, target)
877
878         pots = tree.call('make_pot')
879         for p in pots:
880             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
881
882         target.cleanup()
883
884     elif globals.command == 'changelog':
885         target = SourceTarget()
886         tree = globals.trees.get(args.project, args.checkout, target)
887
888         with TreeDirectory(tree):
889             text = open('ChangeLog', 'r')
890
891         html = tempfile.NamedTemporaryFile()
892         versions = 8
893
894         last = None
895         changes = []
896
897         while True:
898             l = text.readline()
899             if l == '':
900                 break
901
902             if len(l) > 0 and l[0] == "\t":
903                 s = l.split()
904                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
905                     v = Version(s[2])
906                     if v.micro == 0:
907                         if last is not None and len(changes) > 0:
908                             print("<h2>Changes between version %s and %s</h2>" % (s[2], last), file=html)
909                             print("<ul>", file=html)
910                             for c in changes:
911                                 print("<li>%s" % c, file=html)
912                             print("</ul>", file=html)
913                         last = s[2]
914                         changes = []
915                         versions -= 1
916                         if versions < 0:
917                             break
918                 else:
919                     c = l.strip()
920                     if len(c) > 0:
921                         if c[0] == '*':
922                             changes.append(c[2:])
923                         else:
924                             changes[-1] += " " + c
925
926         copyfile(html.file, '%schangelog.html' % args.output)
927         html.close()
928         target.cleanup()
929
930     elif globals.command == 'manual':
931         target = SourceTarget()
932         tree = globals.trees.get(args.project, args.checkout, target)
933
934         outs = tree.call('make_manual')
935         for o in outs:
936             if os.path.isfile(o):
937                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
938             else:
939                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
940
941         target.cleanup()
942
943     elif globals.command == 'doxygen':
944         target = SourceTarget()
945         tree = globals.trees.get(args.project, args.checkout, target)
946
947         dirs = tree.call('make_doxygen')
948         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
949             dirs = [dirs]
950
951         for d in dirs:
952             copytree(d, args.output)
953
954         target.cleanup()
955
956     elif globals.command == 'latest':
957         target = SourceTarget()
958         tree = globals.trees.get(args.project, args.checkout, target)
959
960         with TreeDirectory(tree):
961             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
962             latest = None
963             while latest is None:
964                 t = f.readline()
965                 m = re.compile(".*\((.*)\).*").match(t)
966                 if m:
967                     tags = m.group(1).split(', ')
968                     for t in tags:
969                         s = t.split()
970                         if len(s) > 1:
971                             t = s[1]
972                         if len(t) > 0 and t[0] == 'v':
973                             v = Version(t[1:])
974                             if args.major is None or v.major == args.major:
975                                 latest = v
976
977         print(latest)
978         target.cleanup()
979
980     elif globals.command == 'test':
981         if args.target is None:
982             raise Error('you must specify -t or --target')
983
984         target = None
985         try:
986             target = target_factory(args.target, args.debug, args.work)
987             tree = globals.trees.get(args.project, args.checkout, target)
988             with TreeDirectory(tree):
989                 target.test(tree, args.test)
990         except Error as e:
991             if target is not None:
992                 target.cleanup()
993             raise
994
995         if target is not None:
996             target.cleanup()
997
998     elif globals.command == 'shell':
999         if args.target is None:
1000             raise Error('you must specify -t or --target')
1001
1002         target = target_factory(args.target, args.debug, args.work)
1003         target.command('bash')
1004
1005     elif globals.command == 'revision':
1006
1007         target = SourceTarget()
1008         tree = globals.trees.get(args.project, args.checkout, target)
1009         with TreeDirectory(tree):
1010             print(command_and_read('git rev-parse HEAD').readline().strip()[:7])
1011         target.cleanup()
1012
1013     elif globals.command == 'checkout':
1014
1015         if args.output is None:
1016             raise Error('you must specify -o or --output')
1017
1018         target = SourceTarget()
1019         tree = globals.trees.get(args.project, args.checkout, target)
1020         with TreeDirectory(tree):
1021             shutil.copytree('.', args.output)
1022         target.cleanup()
1023
1024     else:
1025         raise Error('invalid command %s' % globals.command)
1026
1027 try:
1028     main()
1029 except Error as e:
1030     print('cdist: %s' % str(e), file=sys.stderr)
1031     sys.exit(1)