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