Try to fix command() failure.
[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     def command(self, cmd):
527         target = '%s-%s-%s' % (self.distro, self.version, self.bits)
528         container = command_and_read('%s run -itd %s /bin/bash' % (config.docker(), target)).read().strip()
529         command('%s exec -t %s /bin/bash -c "%s"' % (config.docker(), container, cmd))
530
531 class DirectTarget(LinuxTarget):
532     """Build directly in the current environment"""
533     def __init__(self, distro, version, bits, directory=None):
534         super(DirectTarget, self).__init__(distro, version, bits, directory)
535
536     def command(self, c):
537         command('%s %s' % (self.variables_string(), c))
538
539
540 class OSXTarget(Target):
541     def __init__(self, directory=None):
542         super(OSXTarget, self).__init__('osx', directory)
543         self.sdk = config.get('osx_sdk')
544         self.sdk_prefix = config.get('osx_sdk_prefix')
545         self.environment_prefix = config.get('osx_environment_prefix')
546
547     def command(self, c):
548         command('%s %s' % (self.variables_string(False), c))
549
550
551 class OSXSingleTarget(OSXTarget):
552     def __init__(self, bits, directory=None):
553         super(OSXSingleTarget, self).__init__(directory)
554         self.bits = bits
555
556         if bits == 32:
557             arch = 'i386'
558         else:
559             arch = 'x86_64'
560
561         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (self.sdk_prefix, self.sdk, arch)
562         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
563
564         # Environment variables
565         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
566         self.set('CPPFLAGS', '')
567         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
568         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
569         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
570         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
571         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
572         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
573
574     def package(self, project, checkout, output_dir):
575         raise Error('cannot package non-universal OS X versions')
576
577
578 class OSXUniversalTarget(OSXTarget):
579     def __init__(self, directory=None):
580         super(OSXUniversalTarget, self).__init__(directory)
581
582     def package(self, project, checkout, output_dir):
583
584         for b in [32, 64]:
585             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
586             tree = globals.trees.get(project, checkout, target)
587             tree.build_dependencies()
588             tree.build()
589
590         tree = globals.trees.get(project, checkout, self)
591         with TreeDirectory(tree):
592             for p in tree.call('package', tree.version):
593                 copyfile(p, os.path.join(output_dir, os.path.basename(devel_to_git(tree.git_commit, p))))
594
595 class SourceTarget(Target):
596     """Build a source .tar.bz2"""
597     def __init__(self):
598         super(SourceTarget, self).__init__('source')
599
600     def command(self, c):
601         log('host -> %s' % c)
602         command('%s %s' % (self.variables_string(), c))
603
604     def cleanup(self):
605         rmtree(self.directory)
606
607     def package(self, project, checkout, output_dir):
608         tree = globals.trees.get(project, checkout, self)
609         with TreeDirectory(tree):
610             name = read_wscript_variable(os.getcwd(), 'APPNAME')
611             command('./waf dist')
612             p = os.path.abspath('%s-%s.tar.bz2' % (name, tree.version))
613             copyfile(p, os.path.join(output_dir, os.path.basename(devel_to_git(tree.git_commit, p))))
614
615 # @param s Target string:
616 #       windows-{32,64}
617 #    or ubuntu-version-{32,64}
618 #    or debian-version-{32,64}
619 #    or centos-version-{32,64}
620 #    or fedora-version-{32,64}
621 #    or osx-{32,64}
622 #    or source
623 # @param debug True to build with debugging symbols (where possible)
624 def target_factory(s, direct, debug, work):
625     target = None
626     if s.startswith('windows-'):
627         x = s.split('-')
628         if len(x) == 2:
629             target = WindowsTarget(None, int(x[1]), work)
630         elif len(x) == 3:
631             target = WindowsTarget(x[1], int(x[2]), work)
632         else:
633             raise Error("Bad Windows target name `%s'")
634     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-') or s.startswith('fedora'):
635         p = s.split('-')
636         if len(p) != 3:
637             raise Error("Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s)
638         if direct:
639             target = DirectTarget(p[0], p[1], int(p[2]), work)
640         else:
641             target = DockerTarget(p[0], p[1], int(p[2]), work)
642     elif s.startswith('arch-'):
643         p = s.split('-')
644         if len(p) != 2:
645             raise Error("Bad Arch target name `%s'; must be arch-32 or arch-64")
646         target = DockerTarget(p[0], None, p[1], work)
647     elif s == 'raspbian':
648         target = DockerTarget(s, None, None, work)
649     elif s.startswith('osx-'):
650         target = OSXSingleTarget(int(s.split('-')[1]), work)
651     elif s == 'osx':
652         if globals.command == 'build':
653             target = OSXSingleTarget(64, work)
654         else:
655             target = OSXUniversalTarget(work)
656     elif s == 'source':
657         target = SourceTarget()
658     elif s == 'docker':
659         target = DockerTarget()
660
661     if target is None:
662         raise Error("Bad target `%s'" % s)
663
664     target.debug = debug
665     return target
666
667
668 #
669 # Tree
670 #
671
672 class Tree(object):
673     """Description of a tree, which is a checkout of a project,
674        possibly built.  This class is never exposed to cscripts.
675        Attributes:
676            name -- name of git repository (without the .git)
677            specifier -- git tag or revision to use
678            target --- target object that we are using
679            version --- version from the wscript (if one is present)
680            git_commit -- git revision that is actually being used
681            built --- true if the tree has been built yet in this run
682     """
683
684     def __init__(self, name, specifier, target):
685         self.name = name
686         self.specifier = specifier
687         self.target = target
688         self.version = None
689         self.git_commit = None
690         self.built = False
691
692         cwd = os.getcwd()
693
694         flags = ''
695         redirect = ''
696         if globals.quiet:
697             flags = '-q'
698             redirect = '>/dev/null'
699         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
700         os.chdir('%s/src/%s' % (target.directory, self.name))
701
702         spec = self.specifier
703         if spec is None:
704             spec = 'master'
705
706         command('git checkout %s %s %s' % (flags, spec, redirect))
707         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
708         command('git submodule init --quiet')
709         command('git submodule update --quiet')
710
711         proj = '%s/src/%s' % (target.directory, self.name)
712
713         self.cscript = {}
714         exec(open('%s/cscript' % proj).read(), self.cscript)
715
716         if os.path.exists('%s/wscript' % proj):
717             v = read_wscript_variable(proj, "VERSION");
718             if v is not None:
719                 self.version = Version(v)
720
721         os.chdir(cwd)
722
723     def call(self, function, *args):
724         with TreeDirectory(self):
725             return self.cscript[function](self.target, *args)
726
727     def build_dependencies(self, options=None):
728
729         if not 'dependencies' in self.cscript:
730             return
731
732         if len(inspect.getargspec(self.cscript['dependencies']).args) == 2:
733             deps = self.call('dependencies', options)
734         else:
735             log("Deprecated cscipt dependencies() method with no parameter")
736             deps = self.call('dependencies')
737
738         for d in deps:
739             dep = globals.trees.get(d[0], d[1], self.target)
740
741             options = dict()
742             # Make the options to pass in from the option_defaults of the thing
743             # we are building and any options specified by the parent.
744             if 'option_defaults' in dep.cscript:
745                 for k, v in dep.cscript['option_defaults']().items():
746                     options[k] = v
747
748             if len(d) > 2:
749                 for k, v in d[2].items():
750                     options[k] = v
751
752             msg = 'Building dependency %s %s of %s' % (d[0], d[1], self.name)
753             if len(options) > 0:
754                 msg += ' with options %s' % options
755             log(msg)
756
757             dep.build_dependencies(options)
758             dep.build(options)
759
760     def build(self, options=None):
761         if self.built:
762             return
763
764         variables = copy.copy(self.target.variables)
765
766         if not globals.dry_run:
767             if len(inspect.getargspec(self.cscript['build']).args) == 2:
768                 self.call('build', options)
769             else:
770                 self.call('build')
771
772         self.target.variables = variables
773         self.built = True
774
775 #
776 # Command-line parser
777 #
778
779 def main():
780
781     commands = {
782         "build": "build project",
783         "package": "package and build project",
784         "release": "release a project using its next version number (changing wscript and tagging)",
785         "pot": "build the project's .pot files",
786         "changelog": "generate a simple HTML changelog",
787         "manual": "build the project's manual",
788         "doxygen": "build the project's Doxygen documentation",
789         "latest": "print out the latest version",
790         "test": "run the project's unit tests",
791         "shell": "build the project then start a shell",
792         "checkout": "check out the project",
793         "revision": "print the head git revision number"
794     }
795
796     one_of = "Command is one of:\n"
797     summary = ""
798     for k, v in commands.items():
799         one_of += "\t%s\t%s\n" % (k, v)
800         summary += k + " "
801
802     parser = argparse.ArgumentParser()
803     parser.add_argument('command', help=summary)
804     parser.add_argument('-p', '--project', help='project name')
805     parser.add_argument('--minor', help='minor version number bump', action='store_true')
806     parser.add_argument('--micro', help='micro version number bump', action='store_true')
807     parser.add_argument('--major', help='major version to return with latest', type=int)
808     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
809     parser.add_argument('-o', '--output', help='output directory', default='.')
810     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
811     parser.add_argument('-t', '--target', help='target')
812     parser.add_argument('-d', '--direct', help='build in the current environment', action='store_true')
813     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
814     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
815     parser.add_argument('-w', '--work', help='override default work directory')
816     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
817     parser.add_argument('--test', help='name of test to run (with `test''), defaults to all')
818     parser.add_argument('-n', '--dry-run', help='run the process without building anything', action='store_true')
819     args = parser.parse_args()
820
821     # Override configured stuff
822     if args.git_prefix is not None:
823         config.set('git_prefix', args.git_prefix)
824
825     if args.output.find(':') == -1:
826         # This isn't of the form host:path so make it absolute
827         args.output = os.path.abspath(args.output) + '/'
828     else:
829         if args.output[-1] != ':' and args.output[-1] != '/':
830             args.output += '/'
831
832     # Now, args.output is 'host:', 'host:path/' or 'path/'
833
834     if args.work is not None:
835         args.work = os.path.abspath(args.work)
836
837     if args.project is None and args.command != 'shell':
838         raise Error('you must specify -p or --project')
839
840     globals.quiet = args.quiet
841     globals.command = args.command
842     globals.dry_run = args.dry_run
843
844     if not globals.command in commands:
845         e = 'command must be one of:\n' + one_of
846         raise Error('command must be one of:\n%s' % one_of)
847
848     if globals.command == 'build':
849         if args.target is None:
850             raise Error('you must specify -t or --target')
851
852         target = target_factory(args.target, args.direct, args.debug, args.work)
853         target.build(args.project, args.checkout)
854         if not args.keep:
855             target.cleanup()
856
857     elif globals.command == 'package':
858         if args.target is None:
859             raise Error('you must specify -t or --target')
860
861         target = target_factory(args.target, args.direct, args.debug, args.work)
862
863         if target.platform == 'linux':
864             output_dir = os.path.join(args.output, '%s-%s-%d' % (target.distro, target.version, target.bits))
865         else:
866             output_dir = args.output
867
868         makedirs(output_dir)
869         target.package(args.project, args.checkout, output_dir)
870
871         if not args.keep:
872             target.cleanup()
873
874     elif globals.command == 'release':
875         if args.minor is False and args.micro is False:
876             raise Error('you must specify --minor or --micro')
877
878         target = SourceTarget()
879         tree = globals.trees.get(args.project, args.checkout, target)
880
881         version = tree.version
882         version.to_release()
883         if args.minor:
884             version.bump_minor()
885         else:
886             version.bump_micro()
887
888         with TreeDirectory(tree):
889             set_version_in_wscript(version)
890             append_version_to_changelog(version)
891             append_version_to_debian_changelog(version)
892
893             command('git commit -a -m "Bump version"')
894             command('git tag -m "v%s" v%s' % (version, version))
895
896             version.to_devel()
897             set_version_in_wscript(version)
898             command('git commit -a -m "Bump version"')
899             command('git push')
900             command('git push --tags')
901
902         target.cleanup()
903
904     elif globals.command == 'pot':
905         target = SourceTarget()
906         tree = globals.trees.get(args.project, args.checkout, target)
907
908         pots = tree.call('make_pot')
909         for p in pots:
910             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
911
912         target.cleanup()
913
914     elif globals.command == 'changelog':
915         target = SourceTarget()
916         tree = globals.trees.get(args.project, args.checkout, target)
917
918         with TreeDirectory(tree):
919             text = open('ChangeLog', 'r')
920
921         html = tempfile.NamedTemporaryFile()
922         versions = 8
923
924         last = None
925         changes = []
926
927         while True:
928             l = text.readline()
929             if l == '':
930                 break
931
932             if len(l) > 0 and l[0] == "\t":
933                 s = l.split()
934                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
935                     v = Version(s[2])
936                     if v.micro == 0:
937                         if last is not None and len(changes) > 0:
938                             print("<h2>Changes between version %s and %s</h2>" % (s[2], last), file=html)
939                             print("<ul>", file=html)
940                             for c in changes:
941                                 print("<li>%s" % c, file=html)
942                             print("</ul>", file=html)
943                         last = s[2]
944                         changes = []
945                         versions -= 1
946                         if versions < 0:
947                             break
948                 else:
949                     c = l.strip()
950                     if len(c) > 0:
951                         if c[0] == '*':
952                             changes.append(c[2:])
953                         else:
954                             changes[-1] += " " + c
955
956         copyfile(html.file, '%schangelog.html' % args.output)
957         html.close()
958         target.cleanup()
959
960     elif globals.command == 'manual':
961         target = SourceTarget()
962         tree = globals.trees.get(args.project, args.checkout, target)
963
964         outs = tree.call('make_manual')
965         for o in outs:
966             if os.path.isfile(o):
967                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
968             else:
969                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
970
971         target.cleanup()
972
973     elif globals.command == 'doxygen':
974         target = SourceTarget()
975         tree = globals.trees.get(args.project, args.checkout, target)
976
977         dirs = tree.call('make_doxygen')
978         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
979             dirs = [dirs]
980
981         for d in dirs:
982             copytree(d, args.output)
983
984         target.cleanup()
985
986     elif globals.command == 'latest':
987         target = SourceTarget()
988         tree = globals.trees.get(args.project, args.checkout, target)
989
990         with TreeDirectory(tree):
991             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
992             latest = None
993             while latest is None:
994                 t = f.readline()
995                 m = re.compile(".*\((.*)\).*").match(t)
996                 if m:
997                     tags = m.group(1).split(', ')
998                     for t in tags:
999                         s = t.split()
1000                         if len(s) > 1:
1001                             t = s[1]
1002                         if len(t) > 0 and t[0] == 'v':
1003                             v = Version(t[1:])
1004                             if args.major is None or v.major == args.major:
1005                                 latest = v
1006
1007         print(latest)
1008         target.cleanup()
1009
1010     elif globals.command == 'test':
1011         if args.target is None:
1012             raise Error('you must specify -t or --target')
1013
1014         target = None
1015         try:
1016             target = target_factory(args.target, args.direct, args.debug, args.work)
1017             tree = globals.trees.get(args.project, args.checkout, target)
1018             with TreeDirectory(tree):
1019                 target.test(tree, args.test)
1020         except Error as e:
1021             if target is not None:
1022                 target.cleanup()
1023             raise
1024
1025         if target is not None:
1026             target.cleanup()
1027
1028     elif globals.command == 'shell':
1029         if args.target is None:
1030             raise Error('you must specify -t or --target')
1031
1032         target = target_factory(args.target, args.direct, args.debug, args.work)
1033         target.command('bash')
1034
1035     elif globals.command == 'revision':
1036
1037         target = SourceTarget()
1038         tree = globals.trees.get(args.project, args.checkout, target)
1039         with TreeDirectory(tree):
1040             print(command_and_read('git rev-parse HEAD').readline().strip()[:7])
1041         target.cleanup()
1042
1043     elif globals.command == 'checkout':
1044
1045         if args.output is None:
1046             raise Error('you must specify -o or --output')
1047
1048         target = SourceTarget()
1049         tree = globals.trees.get(args.project, args.checkout, target)
1050         with TreeDirectory(tree):
1051             shutil.copytree('.', args.output)
1052         target.cleanup()
1053
1054     else:
1055         raise Error('invalid command %s' % globals.command)
1056
1057 try:
1058     main()
1059 except Error as e:
1060     print('cdist: %s' % str(e), file=sys.stderr)
1061     sys.exit(1)