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