Return errors when os.system fails.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012-2014 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 import os
20 import sys
21 import shutil
22 import glob
23 import tempfile
24 import argparse
25 import datetime
26 import subprocess
27 import re
28 import copy
29 import inspect
30
31 TEMPORARY_DIRECTORY = '/tmp'
32
33 class Globals:
34     quiet = False
35     command = None
36
37 globals = Globals()
38
39 class Error(Exception):
40     def __init__(self, value):
41         self.value = value
42     def __str__(self):
43         return self.value
44     def __repr__(self):
45         return str(self)
46
47 #
48 # Configuration
49 #
50
51 class Option(object):
52     def __init__(self, key, default=None):
53         self.key = key
54         self.value = default
55
56     def offer(self, key, value):
57         if key == self.key:
58             self.value = value
59
60 class BoolOption(object):
61     def __init__(self, key):
62         self.key = key
63         self.value = False
64
65     def offer(self, key, value):
66         if key == self.key:
67             self.value = (value == 'yes' or value == '1' or value == 'true')
68
69 class Config:
70     def __init__(self):
71         self.options = [ Option('linux_chroot_prefix'),
72                          Option('windows_environment_prefix'),
73                          Option('mingw_prefix'),
74                          Option('git_prefix'),
75                          Option('osx_build_host'),
76                          Option('osx_environment_prefix'),
77                          Option('osx_sdk_prefix'),
78                          Option('osx_sdk'),
79                          Option('parallel', 4) ]
80
81         try:
82             f = open('%s/.config/cdist' % os.path.expanduser('~'), 'r')
83             while True:
84                 l = f.readline()
85                 if l == '':
86                     break
87
88                 if len(l) > 0 and l[0] == '#':
89                     continue
90
91                 s = l.strip().split()
92                 if len(s) == 2:
93                     for k in self.options:
94                         k.offer(s[0], s[1])
95         except:
96             raise
97
98     def get(self, k):
99         for o in self.options:
100             if o.key == k:
101                 return o.value
102
103         raise Error('Required setting %s not found' % k)
104
105 config = Config()
106
107 #
108 # Utility bits
109
110
111 def log(m):
112     if not globals.quiet:
113         print '\x1b[33m* %s\x1b[0m' % m
114
115 def scp_escape(n):
116     s = n.split(':')
117     assert(len(s) == 1 or len(s) == 2)
118     if len(s) == 2:
119         return '%s:"\'%s\'"' % (s[0], s[1])
120     else:
121         return '\"%s\"' % s[0]
122
123 def command(c):
124     if os.system(c) != 0:
125         raise Error('%s failed' % c)
126
127 def copytree(a, b):
128     log('copy %s -> %s' % (scp_escape(b), scp_escape(b)))
129     command('scp -r %s %s' % (scp_escape(a), scp_escape(b)))
130
131 def copyfile(a, b):
132     log('copy %s -> %s' % (scp_escape(a), scp_escape(b)))
133     command('scp %s %s' % (scp_escape(a), scp_escape(b)))
134
135 def makedirs(d):
136     if d.find(':') == -1:
137         os.makedirs(d)
138     else:
139         s = d.split(':')
140         command('ssh %s -- mkdir -p %s' % (s[0], s[1]))
141
142 def rmdir(a):
143     log('remove %s' % a)
144     os.rmdir(a)
145
146 def rmtree(a):
147     log('remove %s' % a)
148     shutil.rmtree(a, ignore_errors=True)
149
150 def command(c, can_fail=False):
151     log(c)
152     r = os.system(c)
153     if (r >> 8) and not can_fail:
154         raise Error('command %s failed' % c)
155
156 def command_and_read(c):
157     log(c)
158     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
159     f = os.fdopen(os.dup(p.stdout.fileno()))
160     return f
161
162 def read_wscript_variable(directory, variable):
163     f = open('%s/wscript' % directory, 'r')
164     while True:
165         l = f.readline()
166         if l == '':
167             break
168         
169         s = l.split()
170         if len(s) == 3 and s[0] == variable:
171             f.close()
172             return s[2][1:-1]
173
174     f.close()
175     return None
176
177 #
178 # Version
179 #
180
181 class Version:
182     def __init__(self, s):
183         self.devel = False
184
185         if s.startswith("'"):
186             s = s[1:]
187         if s.endswith("'"):
188             s = s[0:-1]
189         
190         if s.endswith('devel'):
191             s = s[0:-5]
192             self.devel = True
193
194         if s.endswith('pre'):
195             s = s[0:-3]
196
197         p = s.split('.')
198         self.major = int(p[0])
199         self.minor = int(p[1])
200         if len(p) == 3:
201             self.micro = int(p[2])
202         else:
203             self.micro = 0
204
205     def bump_minor(self):
206         self.minor += 1
207         self.micro = 0
208
209     def bump_micro(self):
210         self.micro += 1
211
212     def to_devel(self):
213         self.devel = True
214
215     def to_release(self):
216         self.devel = False
217
218     def __str__(self):
219         s = '%d.%d.%d' % (self.major, self.minor, self.micro)
220         if self.devel:
221             s += 'devel'
222
223         return s
224
225 #
226 # Targets
227 #
228
229 class Target(object):
230     # @param directory directory to work in; if None we will use a temporary directory
231     # Temporary directories will be removed after use; specified directories will not
232     def __init__(self, platform, directory=None):
233         self.platform = platform
234         self.parallel = int(config.get('parallel'))
235
236         if directory is None:
237             self.directory = tempfile.mkdtemp('', 'tmp', TEMPORARY_DIRECTORY)
238             self.rmdir = True
239         else:
240             self.directory = directory
241             self.rmdir = False
242
243         # Environment variables that we will use when we call cscripts
244         self.variables = {}
245         self.debug = False
246
247     def build_dependencies(self, project):
248         cwd = os.getcwd()
249         if 'dependencies' in project.cscript:
250             for d in project.cscript['dependencies'](self):
251                 log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
252                 dep = Project(d[0], '.', d[1])
253                 dep.checkout(self)
254                 self.build_dependencies(dep)
255
256                 # Make the options to pass in from the option_defaults of the thing
257                 # we are building and any options specified by the parent.
258                 options = {}
259                 if 'option_defaults' in dep.cscript:
260                     options = dep.cscript['option_defaults']()
261                     if len(d) > 2:
262                         for k, v in d[2].iteritems():
263                             options[k] = v
264
265                 self.build(dep, options)
266
267         os.chdir(cwd)
268
269     def build(self, project, options=None):
270         variables = copy.copy(self.variables)
271         if len(inspect.getargspec(project.cscript['build']).args) == 2:
272             project.cscript['build'](self, options)
273         else:
274             project.cscript['build'](self)
275         self.variables = variables
276
277     def package(self, project):
278         project.checkout(self)
279         self.build_dependencies(project)
280         self.build(project)
281         return project.cscript['package'](self, project.version)
282
283     def test(self, project):
284         project.checkout(self)
285         self.build_dependencies(project)
286         self.build(project)
287         project.cscript['test'](self)
288
289     def set(self, a, b):
290         self.variables[a] = b
291
292     def unset(self, a):
293         del(self.variables[a])
294
295     def get(self, a):
296         return self.variables[a]
297
298     def append_with_space(self, k, v):
299         if not k in self.variables:
300             self.variables[k] = v
301         else:
302             self.variables[k] = '%s %s' % (self.variables[k], v)
303
304     def variables_string(self, escaped_quotes=False):
305         e = ''
306         for k, v in self.variables.iteritems():
307             if escaped_quotes:
308                 v = v.replace('"', '\\"')
309             e += '%s=%s ' % (k, v)
310         return e
311
312     def cleanup(self):
313         if self.rmdir:
314             rmtree(self.directory)
315
316
317 # Windows
318 #
319
320 class WindowsTarget(Target):
321     def __init__(self, bits, directory=None):
322         super(WindowsTarget, self).__init__('windows', directory)
323         self.bits = bits
324
325         self.windows_prefix = '%s/%d' % (config.get('windows_environment_prefix'), self.bits)
326         if not os.path.exists(self.windows_prefix):
327             raise Error('windows prefix %s does not exist' % self.windows_prefix)
328             
329         if self.bits == 32:
330             self.mingw_name = 'i686'
331         else:
332             self.mingw_name = 'x86_64'
333
334         mingw_path = '%s/%d/bin' % (config.get('mingw_prefix'), self.bits)
335         self.mingw_prefixes = ['/%s/%d' % (config.get('mingw_prefix'), self.bits), '%s/%d/%s-w64-mingw32' % (config.get('mingw_prefix'), bits, self.mingw_name)]
336
337         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
338         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/bin/pkgconfig' % (self.directory, self.directory))
339         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
340         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
341         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
342         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
343         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
344         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
345         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.directory)
346         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.directory)
347         for p in self.mingw_prefixes:
348             cxx += ' -I%s/include' % p
349             link += ' -L%s/lib' % p
350         self.set('CXXFLAGS', '"%s"' % cxx)
351         self.set('CPPFLAGS', '')
352         self.set('LINKFLAGS', '"%s"' % link)
353
354     def command(self, c):
355         log('host -> %s' % c)
356         command('%s %s' % (self.variables_string(), c))
357
358 #
359 # Linux
360 #
361
362 class LinuxTarget(Target):
363     def __init__(self, distro, version, bits, directory=None):
364         super(LinuxTarget, self).__init__('linux', directory)
365         self.distro = distro
366         self.version = version
367         self.bits = bits
368         # e.g. ubuntu-14.04-64
369         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
370         # e.g. /home/carl/Environments/ubuntu-14.04-64
371         self.chroot_prefix = '%s/%s' % (config.get('linux_chroot_prefix'), self.chroot)
372
373         self.set('CXXFLAGS', '-I%s/include' % self.directory)
374         self.set('CPPFLAGS', '')
375         self.set('LINKFLAGS', '-L%s/lib' % self.directory)
376         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:/usr/local/lib/pkgconfig' % self.directory)
377         self.set('PATH', '%s:/usr/local/bin' % (os.environ['PATH']))
378
379     def command(self, c):
380         command('%s schroot -c %s -p -- %s' % (self.variables_string(), self.chroot, c))
381
382 #
383 # OS X
384 #
385
386 class OSXTarget(Target):
387     def __init__(self, directory=None):
388         super(OSXTarget, self).__init__('osx', directory)
389
390     def command(self, c):
391         command('%s %s' % (self.variables_string(False), c))
392
393
394 class OSXSingleTarget(OSXTarget):
395     def __init__(self, bits, directory=None):
396         super(OSXSingleTarget, self).__init__(directory)
397         self.bits = bits
398
399         if bits == 32:
400             arch = 'i386'
401         else:
402             arch = 'x86_64'
403
404         flags = '-isysroot %s/MacOSX%s.sdk -arch %s' % (config.get('osx_sdk_prefix'), config.get('osx_sdk'), arch)
405         enviro = '%s/%d' % (config.get('osx_environment_prefix'), bits)
406
407         # Environment variables
408         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
409         self.set('CPPFLAGS', '')
410         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (self.directory, enviro, flags))
411         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
412         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (self.directory, enviro, flags))
413         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig:/usr/lib/pkgconfig' % (self.directory, enviro))
414         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
415         self.set('MACOSX_DEPLOYMENT_TARGET', config.get('osx_sdk'))
416
417     def package(self, project):
418         raise Error('cannot package non-universal OS X versions')
419
420
421 class OSXUniversalTarget(OSXTarget):
422     def __init__(self, directory=None):
423         super(OSXUniversalTarget, self).__init__(directory)
424         self.parts = []
425         self.parts.append(OSXSingleTarget(32, os.path.join(self.directory, '32')))
426         self.parts.append(OSXSingleTarget(64, os.path.join(self.directory, '64')))
427
428     def package(self, project):
429         for p in self.parts:
430             project.checkout(p)
431             p.build_dependencies(project)
432             p.build(project)
433
434         return project.cscript['package'](self, project.version)
435     
436
437 #
438 # Source
439 #
440
441 class SourceTarget(Target):
442     def __init__(self):
443         super(SourceTarget, self).__init__('source')
444
445     def command(self, c):
446         log('host -> %s' % c)
447         command('%s %s' % (self.variables_string(), c))
448
449     def cleanup(self):
450         rmtree(self.directory)
451
452     def package(self, project):
453         project.checkout(self)
454         name = read_wscript_variable(os.getcwd(), 'APPNAME')
455         command('./waf dist')
456         return os.path.abspath('%s-%s.tar.bz2' % (name, project.version))
457
458
459 # @param s Target string:
460 #       windows-{32,64}
461 #    or ubuntu-version-{32,64}
462 #    or debian-version-{32,64}
463 #    or centos-version-{32,64}
464 #    or osx-{32,64}
465 #    or source      
466 # @param debug True to build with debugging symbols (where possible)
467 def target_factory(s, debug, work):
468     target = None
469     if s.startswith('windows-'):
470         target = WindowsTarget(int(s.split('-')[1]), work)
471     elif s.startswith('ubuntu-') or s.startswith('debian-') or s.startswith('centos-'):
472         p = s.split('-')
473         if len(p) != 3:
474             print >>sys.stderr,"Bad Linux target name `%s'; must be something like ubuntu-12.04-32 (i.e. distro-version-bits)" % s
475             sys.exit(1)
476         target = LinuxTarget(p[0], p[1], int(p[2]), work)
477     elif s.startswith('osx-'):
478         target = OSXSingleTarget(int(s.split('-')[1]), work)
479     elif s == 'osx':
480         if globals.command == 'build':
481             target = OSXSingleTarget(64, work)
482         else:
483             target = OSXUniversalTarget(work)
484     elif s == 'source':
485         target = SourceTarget()
486
487     if target is not None:
488         target.debug = debug
489
490     return target
491
492
493 #
494 # Project
495 #
496  
497 class Project(object):
498     def __init__(self, name, directory, specifier=None):
499         self.name = name
500         self.directory = directory
501         self.version = None
502         self.specifier = specifier
503         self.git_commit = None
504         if self.specifier is None:
505             self.specifier = 'master'
506
507     def checkout(self, target):
508         flags = ''
509         redirect = ''
510         if globals.quiet:
511             flags = '-q'
512             redirect = '>/dev/null'
513         command('git clone %s %s/%s.git %s/src/%s' % (flags, config.get('git_prefix'), self.name, target.directory, self.name))
514         os.chdir('%s/src/%s' % (target.directory, self.name))
515         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
516         self.git_commit = command_and_read('git rev-parse --short=7 HEAD').readline().strip()
517         command('git submodule init --quiet')
518         command('git submodule update --quiet')
519         os.chdir(self.directory)
520
521         proj = '%s/src/%s/%s' % (target.directory, self.name, self.directory)
522
523         self.read_cscript('%s/cscript' % proj)
524         
525         if os.path.exists('%s/wscript' % proj):
526             v = read_wscript_variable(proj, "VERSION");
527             if v is not None:
528                 self.version = Version(v)
529
530     def read_cscript(self, s):
531         self.cscript = {}
532         execfile(s, self.cscript)
533
534 def set_version_in_wscript(version):
535     f = open('wscript', 'rw')
536     o = open('wscript.tmp', 'w')
537     while True:
538         l = f.readline()
539         if l == '':
540             break
541
542         s = l.split()
543         if len(s) == 3 and s[0] == "VERSION":
544             print "Writing %s" % version
545             print >>o,"VERSION = '%s'" % version
546         else:
547             print >>o,l,
548     f.close()
549     o.close()
550
551     os.rename('wscript.tmp', 'wscript')
552
553 def append_version_to_changelog(version):
554     try:
555         f = open('ChangeLog', 'r')
556     except:
557         log('Could not open ChangeLog')
558         return
559
560     c = f.read()
561     f.close()
562
563     f = open('ChangeLog', 'w')
564     now = datetime.datetime.now()
565     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))
566     f.write(c)
567
568 def append_version_to_debian_changelog(version):
569     if not os.path.exists('debian'):
570         log('Could not find debian directory')
571         return
572
573     command('dch -b -v %s-1 "New upstream release."' % version)
574
575 def devel_to_git(project, filename):
576     if project.git_commit is not None:
577         filename = filename.replace('devel', '-%s' % project.git_commit)
578     return filename
579
580
581 #
582 # Command-line parser
583 #
584
585 def main():
586
587     commands = {
588         "build": "build project",
589         "package": "package and build project",
590         "release": "release a project using its next version number (changing wscript and tagging)",
591         "pot": "build the project's .pot files",
592         "changelog": "generate a simple HTML changelog",
593         "manual": "build the project's manual",
594         "doxygen": "build the project's Doxygen documentation",
595         "latest": "print out the latest version",
596         "test": "run the project's unit tests",
597         "shell": "build the project then start a shell in its chroot",
598         "revision": "print the head git revision number"
599     }
600
601     one_of = "Command is one of:\n"
602     summary = ""
603     for k, v in commands.iteritems():
604         one_of += "\t%s\t%s\n" % (k, v)
605         summary += k + " "
606
607     parser = argparse.ArgumentParser()
608     parser.add_argument('command', help=summary)
609     parser.add_argument('-p', '--project', help='project name')
610     parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
611     parser.add_argument('--minor', help='minor version number bump', action='store_true')
612     parser.add_argument('--micro', help='micro version number bump', action='store_true')
613     parser.add_argument('--major', help='major version to return with latest', type=int)
614     parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
615     parser.add_argument('-o', '--output', help='output directory', default='.')
616     parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
617     parser.add_argument('-t', '--target', help='target')
618     parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
619     parser.add_argument('--debug', help='build with debugging symbols where possible', action='store_true')
620     parser.add_argument('-w', '--work', help='override default work directory')
621     args = parser.parse_args()
622
623     if args.output.find(':') == -1:
624         # This isn't of the form host:path so make it absolute
625         args.output = os.path.abspath(args.output) + '/'
626     else:
627         if args.output[-1] != ':' and args.output[-1] != '/':
628             args.output += '/'
629
630     # Now, args.output is 'host:', 'host:path/' or 'path/'
631
632     if args.work is not None:
633         args.work = os.path.abspath(args.work)
634
635     if args.project is None and args.command != 'shell':
636         raise Error('you must specify -p or --project')
637         
638     globals.quiet = args.quiet
639     globals.command = args.command
640
641     project = Project(args.project, args.directory, args.checkout)
642
643     if not globals.command in commands:
644         e = 'command must be one of:\n' + one_of
645         raise Error('command must be one of:\n%s' % one_of)
646
647     if globals.command == 'build':
648         if args.target is None:
649             raise Error('you must specify -t or --target')
650
651         target = target_factory(args.target, args.debug, args.work)
652         project.checkout(target)
653         target.build_dependencies(project)
654         target.build(project)
655         if not args.keep:
656             target.cleanup()
657
658     elif globals.command == 'package':
659         if args.target is None:
660             raise Error('you must specify -t or --target')
661
662         target = target_factory(args.target, args.debug, args.work)
663
664         packages = target.package(project)
665         if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
666             packages = [packages]
667
668         if target.platform == 'linux':
669             out = '%s%s-%s-%d' % (args.output, target.distro, target.version, target.bits)
670             try:
671                 makedirs(out)
672             except:
673                 pass
674             for p in packages:
675                 copyfile(p, '%s/%s' % (out, os.path.basename(devel_to_git(project, p))))
676         else:
677             for p in packages:
678                 copyfile(p, '%s%s' % (args.output, os.path.basename(devel_to_git(project, p))))
679
680         if not args.keep:
681             target.cleanup()
682
683     elif globals.command == 'release':
684         if args.minor is False and args.micro is False:
685             raise Error('you must specify --minor or --micro')
686
687         target = SourceTarget()
688         project.checkout(target)
689
690         version = project.version
691         version.to_release()
692         if args.minor:
693             version.bump_minor()
694         else:
695             version.bump_micro()
696
697         set_version_in_wscript(version)
698         append_version_to_changelog(version)
699         append_version_to_debian_changelog(version)
700
701         command('git commit -a -m "Bump version"')
702         command('git tag -m "v%s" v%s' % (version, version))
703
704         version.to_devel()
705         set_version_in_wscript(version)
706         command('git commit -a -m "Bump version"')
707         command('git push')
708         command('git push --tags')
709
710         target.cleanup()
711
712     elif globals.command == 'pot':
713         target = SourceTarget()
714         project.checkout(target)
715
716         pots = project.cscript['make_pot'](target)
717         for p in pots:
718             copyfile(p, '%s%s' % (args.output, os.path.basename(p)))
719
720         target.cleanup()
721
722     elif globals.command == 'changelog':
723         target = SourceTarget()
724         project.checkout(target)
725
726         text = open('ChangeLog', 'r')
727         html = tempfile.NamedTemporaryFile()
728         versions = 8
729
730         last = None
731         changes = []
732
733         while True:
734             l = text.readline()
735             if l == '':
736                 break
737
738             if len(l) > 0 and l[0] == "\t":
739                 s = l.split()
740                 if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
741                     v = Version(s[2])
742                     if v.micro == 0:
743                         if last is not None and len(changes) > 0:
744                             print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
745                             print >>html,"<ul>"
746                             for c in changes:
747                                 print >>html,"<li>%s" % c
748                             print >>html,"</ul>"
749                         last = s[2]
750                         changes = []
751                         versions -= 1
752                         if versions < 0:
753                             break
754                 else:
755                     c = l.strip()
756                     if len(c) > 0:
757                         if c[0] == '*':
758                             changes.append(c[2:])
759                         else:
760                             changes[-1] += " " + c
761
762         copyfile(html.file, '%schangelog.html' % args.output)
763         html.close()
764         target.cleanup()
765
766     elif globals.command == 'manual':
767         target = SourceTarget()
768         project.checkout(target)
769
770         outs = project.cscript['make_manual'](target)
771         for o in outs:
772             if os.path.isfile(o):
773                 copyfile(o, '%s%s' % (args.output, os.path.basename(o)))
774             else:
775                 copytree(o, '%s%s' % (args.output, os.path.basename(o)))
776
777         target.cleanup()
778
779     elif globals.command == 'doxygen':
780         target = SourceTarget()
781         project.checkout(target)
782
783         dirs = project.cscript['make_doxygen'](target)
784         if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
785             dirs = [dirs]
786
787         for d in dirs:
788             copytree(d, '%s%s' % (args.output, 'doc'))
789
790         target.cleanup()
791
792     elif globals.command == 'latest':
793         target = SourceTarget()
794         project.checkout(target)
795
796         f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
797         latest = None
798         while latest is None:
799             t = f.readline()
800             m = re.compile(".*\((.*)\).*").match(t)
801             if m:
802                 tags = m.group(1).split(', ')
803                 for t in tags:
804                     s = t.split()
805                     if len(s) > 1:
806                         t = s[1]
807                     if len(t) > 0 and t[0] == 'v':
808                         v = Version(t[1:])
809                         if args.major is None or v.major == args.major:
810                             latest = v
811
812         print latest
813         target.cleanup()
814
815     elif globals.command == 'test':
816         if args.target is None:
817             raise Error('you must specify -t or --target')
818
819         target = None
820         try:
821             target = target_factory(args.target, args.debug, args.work)
822             target.test(project)
823         except Error as e:
824             if target is not None:
825                 target.cleanup()
826             raise
827
828         if target is not None:
829             target.cleanup()
830
831     elif globals.command == 'shell':
832         if args.target is None:
833             raise Error('you must specify -t or --target')
834
835         target = target_factory(args.target, args.debug, args.work)
836         target.command('bash')
837
838     elif globals.command == 'revision':
839
840         target = SourceTarget()
841         project.checkout(target)
842         print command_and_read('git rev-parse HEAD').readline().strip()[:7]
843         target.cleanup()
844
845     else:
846         raise Error('invalid command %s' % globals.command)
847
848 try:
849     main()
850 except Error as e:
851     print >>sys.stderr,'cdist: %s' % str(e)
852     sys.exit(1)