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