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