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