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