cab4ddf25c2a29d46c8ca981bd6438f75854f283
[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                 return o.value
125
126         raise Error('Required setting %s not found' % k)
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     platform -- platform string (e.g. 'windows', 'linux', 'osx')
310     directory -- directory to work in; if None we will use a temporary directory
311     Temporary directories will be removed after use; specified directories will not.
312     """
313     def __init__(self, platform, directory=None):
314         self.platform = platform
315         self.parallel = int(config.get('parallel'))
316
317         # self.directory is the working directory
318         if directory is None:
319             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
320             self.rmdir = True
321         else:
322             self.directory = directory
323             self.rmdir = False
324
325         # Environment variables that we will use when we call cscripts
326         self.variables = {}
327         self.debug = False
328
329     def package(self, project, checkout):
330         tree = globals.trees.get(project, checkout, self)
331         tree.build_dependencies()
332         tree.build(tree)
333         return tree.call('package', tree.version), tree.git_commit
334
335     def test(self, tree, test):
336         """test is the test case to run, or None"""
337         tree.build_dependencies()
338         tree.build()
339         return tree.call('test', test)
340
341     def set(self, a, b):
342         self.variables[a] = b
343
344     def unset(self, a):
345         del(self.variables[a])
346
347     def get(self, a):
348         return self.variables[a]
349
350     def append_with_space(self, k, v):
351         if (not k in self.variables) or len(self.variables[k]) == 0:
352             self.variables[k] = '"%s"' % v
353         else:
354             e = self.variables[k]
355             if e[0] == '"' and e[-1] == '"':
356                 self.variables[k] = '"%s %s"' % (e[1:-1], v)
357             else:
358                 self.variables[k] = '"%s %s"' % (e, v)
359
360     def variables_string(self, escaped_quotes=False):
361         e = ''
362         for k, v in self.variables.items():
363             if escaped_quotes:
364                 v = v.replace('"', '\\"')
365             e += '%s=%s ' % (k, v)
366         return e
367
368     def cleanup(self):
369         if self.rmdir:
370             rmtree(self.directory)
371
372 #
373 # Windows
374 #
375
376 class WindowsTarget(Target):
377     def __init__(self, version, bits, directory=None):
378         super(WindowsTarget, self).__init__('windows', directory)
379         self.version = version
380         self.bits = bits
381
382         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
383         if not os.path.exists(self.windows_prefix):
384             raise Error('windows prefix %s does not exist' % self.windows_prefix)
385
386         if self.bits == 32:
387             self.mingw_name = 'i686'
388         else:
389             self.mingw_name = 'x86_64'
390
391         self.mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
392         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
393
394         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
395         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
396         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, self.mingw_path, os.environ['PATH']))
397         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
398         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
399         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
400         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
401         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
402         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
403         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
404         for p in self.mingw_prefixes:
405             cxx += ' -I%s/include' % p
406             link += ' -L%s/lib' % p
407         self.set('CXXFLAGS', '"%s"' % cxx)
408         self.set('CPPFLAGS', '')
409         self.set('LINKFLAGS', '"%s"' % link)
410         self.set('LDFLAGS', '"%s"' % link)
411
412     def command(self, c):
413         log('host -> %s' % c)
414         command('%s %s' % (self.variables_string(), c))
415
416 class LinuxTarget(Target):
417     """Parent for Linux targets"""
418     def __init__(self, distro, version, bits, directory=None):
419         super(LinuxTarget, self).__init__('linux', directory)
420         self.distro = distro
421         self.version = version
422         self.bits = bits
423
424         self.set('CXXFLAGS', '-I%s/include' % self.directory)
425         self.set('CPPFLAGS', '')
426         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
427         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib64/pkgconfig:/usr/local/lib/pkgconfig' % (self.directory, self.directory))
428         self.set('PATH', '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin')
429
430 class ChrootTarget(LinuxTarget):
431     """Build in a chroot"""
432     def __init__(self, distro, version, bits, directory=None):
433         super(ChrootTarget, self).__init__(distro, version, bits, directory)
434         # e.g. ubuntu-14.04-64
435         if self.version is not None and self.bits is not None:
436             self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
437         else:
438             self.chroot = self.distro
439         # e.g. /home/carl/Environments/ubuntu-14.04-64
440         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
441
442     def command(self, c):
443         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
444
445
446 class HostTarget(LinuxTarget):
447     """Build directly on the host"""
448     def __init__(self, distro, version, bits, directory=None):
449         super(HostTarget, self).__init__(distro, version, bits, directory)
450
451     def command(self, c):
452         command('%s %s' % (self.variables_string(), c))
453
454 #
455 # OS X
456 #
457
458 class OSXTarget(Target):
459     def __init__(self, directory=None):
460         super(OSXTarget, self).__init__('osx', directory)
461         self.sdk = config.get('osx_sdk')
462         self.sdk_prefix = config.get('osx_sdk_prefix')
463
464     def command(self, c):
465         command('%s %s' % (self.variables_string(False), c))
466
467
468 class OSXSingleTarget(OSXTarget):
469     def __init__(self, bits, directory=None):
470         super(OSXSingleTarget, self).__init__(directory)
471         self.bits = bits
472
473         if bits == 32:
474             arch = 'i386'
475         else:
476             arch = 'x86_64'
477
478         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (self.sdk_prefix, self.sdk, arch)
479         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
480
481         # Environment variables
482         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
483         self.set('CPPFLAGS', '')
484         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
485         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
486         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
487         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
488         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
489         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
490
491     def package(self, project, checkout):
492         raise Error('cannot package non-universal OS X versions')
493
494
495 class OSXUniversalTarget(OSXTarget):
496     def __init__(self, directory=None):
497         super(OSXUniversalTarget, self).__init__(directory)
498
499     def package(self, project, checkout):
500
501         for b in [32, 64]:
502             target = OSXSingleTarget(b, os.path.join(self.directory, '%d' % b))
503             tree = globals.trees.get(project, checkout, target)
504             tree.build_dependencies()
505             tree.build()
506
507         tree = globals.trees.get(project, checkout, self)
508         with TreeDirectory(tree):
509             return tree.call('package', tree.version), tree.git_commit
510
511 class SourceTarget(Target):
512     """Build a source .tar.bz2"""
513     def __init__(self):
514         super(SourceTarget, self).__init__('source')
515
516     def command(self, c):
517         log('host -> %s' % c)
518         command('%s %s' % (self.variables_string(), c))
519
520     def cleanup(self):
521         rmtree(self.directory)
522
523     def package(self, project, checkout):
524         tree = globals.trees.get(project, checkout, self)
525         with TreeDirectory(tree):
526             name = read_wscript_variable(os.getcwd(), 'APPNAME')
527             command('./waf dist')
528             return os.path.abspath('%s-%s.tar.bz2' % (name, tree.version)), tree.git_commit
529
530
531 # @param s Target string:
532 #       windows-{32,64}
533 #    or ubuntu-version-{32,64}
534 #    or debian-version-{32,64}
535 #    or centos-version-{32,64}
536 #    or osx-{32,64}
537 #    or source
538 # @param debug True to build with debugging symbols (where possible)
539 def target_factory(s, debug, work):
540     target = None
541     if s.startswith('windows-'):
542         x = s.split('-')
543         if len(x) == 2:
544             target = WindowsTarget(None, int(x[1]), work)
545         elif len(x) == 3:
546             target = WindowsTarget(x[1], int(x[2]), work)
547         else:
548             raise Error("Bad Windows target name `%s'")
549     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
550         p = s.split('-')
551         if len(p) != 3:
552             raise Error("Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s)
553         target = ChrootTarget(p[0], p[1], int(p[2]), work)
554     elif s.startswith('arch-'):
555         p = s.split('-')
556         if len(p) != 2:
557             raise Error("Bad Arch target name `%s'; must be arch-32 or arch-64")
558         target = ChrootTarget(p[0], None, p[1], work)
559     elif s == 'raspbian':
560         target = ChrootTarget(s, None, None, work)
561     elif s == 'host':
562         if command_and_read('uname -m').read().strip() == 'x86_64':
563             bits = 64
564         else:
565             bits = 32
566         try:
567             f = open('/etc/fedora-release', 'r')
568             l = f.readline().strip().split()
569             target = HostTarget("fedora", l[2], bits, work)
570         except Exception as e:
571             if os.path.exists('/etc/arch-release'):
572                 target = HostTarget("arch", None, bits, work)
573             else:
574                 raise Error("could not identify distribution for `host' target (%s)" % e)
575     elif s.startswith('osx-'):
576         target = OSXSingleTarget(int(s.split('-')[1]), work)
577     elif s == 'osx':
578         if globals.command == 'build':
579             target = OSXSingleTarget(64, work)
580         else:
581             target = OSXUniversalTarget(work)
582     elif s == 'source':
583         target = SourceTarget()
584
585     if target is None:
586         raise Error("Bad target `%s'" % s)
587
588     target.debug = debug
589     return target
590
591
592 #
593 # Tree
594 #
595
596 class Tree(object):
597     """Description of a tree, which is a checkout of a project,
598        possibly built.  This class is never exposed to cscripts.
599        Attributes:
600            name -- name of git repository (without the .git)
601            specifier -- git tag or revision to use
602            target --- target object that we are using
603            version --- version from the wscript (if one is present)
604            git_commit -- git revision that is actually being used
605            built --- true if the tree has been built yet in this run
606     """
607
608     def __init__(self, name, specifier, target):
609         self.name = name
610         self.specifier = specifier
611         self.target = target
612         self.version = None
613         self.git_commit = None
614         self.built = False
615
616         cwd = os.getcwd()
617
618         flags = ''
619         redirect = ''
620         if globals.quiet:
621             flags = '-q'
622             redirect = '>/dev/null'
623         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
624         os.chdir('%s/src/%s' % (target.directory, self.name))
625
626         spec = self.specifier
627         if spec is None:
628             spec = 'master'
629
630         command('git checkout %s %s %s' % (flags, spec, redirect))
631         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
632         command('git submodule init --quiet')
633         command('git submodule update --quiet')
634
635         proj = '%s/src/%s' % (target.directory, self.name)
636
637         self.cscript = {}
638         exec(open('%s/cscript' % proj).read(), self.cscript)
639
640         if os.path.exists('%s/wscript' % proj):
641             v = read_wscript_variable(proj, "VERSION");
642             if v is not None:
643                 self.version = Version(v)
644
645         os.chdir(cwd)
646
647     def call(self, function, *args):
648         with TreeDirectory(self):
649             return self.cscript[function](self.target, *args)
650
651     def build_dependencies(self):
652         if 'dependencies' in self.cscript:
653             for d in self.cscript['dependencies'](self.target):
654                 log('Building dependency %s %s of %s' % (d[0], d[1], self.name))
655                 dep = globals.trees.get(d[0], d[1], self.target)
656                 dep.build_dependencies()
657
658                 # Make the options to pass in from the option_defaults of the thing
659                 # we are building and any options specified by the parent.
660                 options = {}
661                 if 'option_defaults' in dep.cscript:
662                     options = dep.cscript['option_defaults']()
663                     if len(d) > 2:
664                         for k, v in d[2].items():
665                             options[k] = v
666
667                 dep.build(options)
668
669     def build(self, options=None):
670         if self.built:
671             return
672
673         variables = copy.copy(self.target.variables)
674
675         if len(inspect.getargspec(self.cscript['build']).args) == 2:
676             self.call('build', options)
677         else:
678             self.call('build')
679
680         self.target.variables = variables
681         self.built = True
682
683 #
684 # Command-line parser
685 #
686
687 def main():
688
689     commands = {
690         "build": "build project",
691         "package": "package and build project",
692         "release": "release a project using its next version number (changing wscript and tagging)",
693         "pot": "build the project's .pot files",
694         "changelog": "generate a simple HTML changelog",
695         "manual": "build the project's manual",
696         "doxygen": "build the project's Doxygen documentation",
697         "latest": "print out the latest version",
698         "test": "run the project's unit tests",
699         "shell": "build the project then start a shell in its chroot",
700         "checkout": "check out the project",
701         "revision": "print the head git revision number"
702     }
703
704     one_of = "Command is one of:\n"
705     summary = ""
706     for k, v in commands.items():
707         one_of += "\t%s\t%s\n" % (k, v)
708         summary += k + " "
709
710     parser = argparse.ArgumentParser()
711     parser.add_argument('command', help=summary)
712     parser.add_argument('-p', '--project', help='project name')
713     parser.add_argument('--minor', help='minor version number bump', action='store_true')
714     parser.add_argument('--micro', help='micro version number bump', action='store_true')
715     parser.add_argument('--major', help='major version to return with latest', type=int)
716     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
717     parser.add_argument('-o', '--output', help='output directory', default='.')
718     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
719     parser.add_argument('-t', '--target', help='target')
720     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
721     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
722     parser.add_argument('-w', '--work', help='override default work directory')
723     parser.add_argument('-g', '--git-prefix', help='override configured git prefix')
724     parser.add_argument('--test', help='name of test to run (with `test''), defaults to all')
725     args = parser.parse_args()
726
727     # Override configured stuff
728     if args.git_prefix is not None:
729         config.set('git_prefix', args.git_prefix)
730
731     if args.output.find(':') == -1:
732         # This isn't of the form host:path so make it absolute
733         args.output = os.path.abspath(args.output) + '/'
734     else:
735         if args.output[-1] != ':' and args.output[-1] != '/':
736             args.output += '/'
737
738     # Now, args.output is 'host:', 'host:path/' or 'path/'
739
740     if args.work is not None:
741         args.work = os.path.abspath(args.work)
742
743     if args.project is None and args.command != 'shell':
744         raise Error('you must specify -p or --project')
745
746     globals.quiet = args.quiet
747     globals.command = args.command
748
749     if not globals.command in commands:
750         e = 'command must be one of:\n' + one_of
751         raise Error('command must be one of:\n%s' % one_of)
752
753     if globals.command == 'build':
754         if args.target is None:
755             raise Error('you must specify -t or --target')
756
757         target = target_factory(args.target, args.debug, args.work)
758         tree = globals.trees.get(args.project, args.checkout, target)
759         tree.build_dependencies()
760         tree.build()
761         if not args.keep:
762             target.cleanup()
763
764     elif globals.command == 'package':
765         if args.target is None:
766             raise Error('you must specify -t or --target')
767
768         target = target_factory(args.target, args.debug, args.work)
769         packages, git_commit = target.package(args.project, args.checkout)
770         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
771             packages = [packages]
772
773         if target.platform == 'linux':
774             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
775             try:
776                 makedirs(out)
777             except:
778                 pass
779             for p in packages:
780                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(git_commit, p))))
781         else:
782             try:
783                 makedirs(args.output)
784             except:
785                 pass
786             for p in packages:
787                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(git_commit, p))))
788
789         if not args.keep:
790             target.cleanup()
791
792     elif globals.command == 'release':
793         if args.minor is False and args.micro is False:
794             raise Error('you must specify --minor or --micro')
795
796         target = SourceTarget()
797         tree = globals.trees.get(args.project, args.checkout, target)
798
799         version = tree.version
800         version.to_release()
801         if args.minor:
802             version.bump_minor()
803         else:
804             version.bump_micro()
805
806         set_version_in_wscript(version)
807         append_version_to_changelog(version)
808         append_version_to_debian_changelog(version)
809
810         command('git commit -a -m "Bump version"')
811         command('git tag -m "v%s" v%s' % (version, version))
812
813         version.to_devel()
814         set_version_in_wscript(version)
815         command('git commit -a -m "Bump version"')
816         command('git push')
817         command('git push --tags')
818
819         target.cleanup()
820
821     elif globals.command == 'pot':
822         target = SourceTarget()
823         tree = globals.trees.get(args.project, args.checkout, target)
824
825         pots = tree.call('make_pot')
826         for p in pots:
827             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
828
829         target.cleanup()
830
831     elif globals.command == 'changelog':
832         target = SourceTarget()
833         tree = globals.trees.get(args.project, args.checkout, target)
834
835         with TreeDirectory(tree):
836             text = open('ChangeLog', 'r')
837
838         html = tempfile.NamedTemporaryFile()
839         versions = 8
840
841         last = None
842         changes = []
843
844         while True:
845             l = text.readline()
846             if l == '':
847                 break
848
849             if len(l) > 0 and l[0] == "\t":
850                 s = l.split()
851                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
852                     v = Version(s[2])
853                     if v.micro == 0:
854                         if last is not None and len(changes) > 0:
855                             print("<h2>Changes between version %s and %s</h2>" % (s[2], last), file=html)
856                             print("<ul>", file=html)
857                             for c in changes:
858                                 print("<li>%s" % c, file=html)
859                             print("</ul>", file=html)
860                         last = s[2]
861                         changes = []
862                         versions -= 1
863                         if versions < 0:
864                             break
865                 else:
866                     c = l.strip()
867                     if len(c) > 0:
868                         if c[0] == '*':
869                             changes.append(c[2:])
870                         else:
871                             changes[-1] += " " + c
872
873         copyfile(html.file, '%schangelog.html' % args.output)
874         html.close()
875         target.cleanup()
876
877     elif globals.command == 'manual':
878         target = SourceTarget()
879         tree = globals.trees.get(args.project, args.checkout, target)
880
881         outs = tree.call('make_manual')
882         for o in outs:
883             if os.path.isfile(o):
884                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
885             else:
886                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
887
888         target.cleanup()
889
890     elif globals.command == 'doxygen':
891         target = SourceTarget()
892         tree = globals.trees.get(args.project, args.checkout, target)
893
894         dirs = tree.call('make_doxygen')
895         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
896             dirs = [dirs]
897
898         for d in dirs:
899             copytree(d, args.output)
900
901         target.cleanup()
902
903     elif globals.command == 'latest':
904         target = SourceTarget()
905         tree = globals.trees.get(args.project, args.checkout, target)
906
907         with TreeDirectory(tree):
908             f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
909             latest = None
910             while latest is None:
911                 t = f.readline()
912                 m = re.compile(".*\((.*)\).*").match(t)
913                 if m:
914                     tags = m.group(1).split(', ')
915                     for t in tags:
916                         s = t.split()
917                         if len(s) > 1:
918                             t = s[1]
919                         if len(t) > 0 and t[0] == 'v':
920                             v = Version(t[1:])
921                             if args.major is None or v.major == args.major:
922                                 latest = v
923
924         print(latest)
925         target.cleanup()
926
927     elif globals.command == 'test':
928         if args.target is None:
929             raise Error('you must specify -t or --target')
930
931         target = None
932         try:
933             target = target_factory(args.target, args.debug, args.work)
934             tree = globals.trees.get(args.project, args.checkout, target)
935             with TreeDirectory(tree):
936                 target.test(tree, args.test)
937         except Error as e:
938             if target is not None:
939                 target.cleanup()
940             raise
941
942         if target is not None:
943             target.cleanup()
944
945     elif globals.command == 'shell':
946         if args.target is None:
947             raise Error('you must specify -t or --target')
948
949         target = target_factory(args.target, args.debug, args.work)
950         target.command('bash')
951
952     elif globals.command == 'revision':
953
954         target = SourceTarget()
955         tree = globals.trees.get(args.project, args.checkout, target)
956         with TreeDirectory(tree):
957             print(command_and_read('git rev-parse HEAD').readline().strip()[:7])
958         target.cleanup()
959
960     elif globals.command == 'checkout':
961
962         if args.output is None:
963             raise Error('you must specify -o or --output')
964
965         target = SourceTarget()
966         tree = globals.trees.get(args.project, args.checkout, target)
967         with TreeDirectory(tree):
968             shutil.copytree('.', args.output)
969         target.cleanup()
970
971     else:
972         raise Error('invalid command %s' % globals.command)
973
974 try:
975     main()
976 except Error as e:
977     print('cdist: %s' % str(e), file=sys.stderr)
978     sys.exit(1)