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