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