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