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