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