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