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