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