Basic --keep option.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 import os
4 import sys
5 import shutil
6 import glob
7 import tempfile
8 import argparse
9 import datetime
10 import subprocess
11 import re
12
13 #
14 # Utility bits
15
16
17 def log(m):
18     if not args.quiet:
19         print '\x1b[33m* %s\x1b[0m' % m
20
21 def error(e):
22     print '\x1b[31mError: %s\x1b[0m' % e
23     sys.exit(1)
24
25 def copytree(a, b):
26     log('copy %s -> %s' % (a, b))
27     shutil.copytree(a, b)
28
29 def copyfile(a, b):
30     log('copy %s -> %s' % (a, b))
31     shutil.copyfile(a, b)
32
33 def rmtree(a):
34     log('remove %s' % a)
35     shutil.rmtree(a, ignore_errors=True)
36
37 def command(c, can_fail=False):
38     log(c)
39     r = os.system(c)
40     if (r >> 8) and not can_fail:
41         error('command failed')
42
43 def command_and_read(c):
44     log(c)
45     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
46     f = os.fdopen(os.dup(p.stdout.fileno()))
47     return f
48
49
50 #
51 # Version
52 #
53
54 class Version:
55     def __init__(self, s):
56         self.pre = False
57         self.beta = None
58
59         if s.startswith("'"):
60             s = s[1:]
61         if s.endswith("'"):
62             s = s[0:-1]
63         
64         if s.endswith('pre'):
65             s = s[0:-3]
66             self.pre = True
67
68         b = s.find("beta")
69         if b != -1:
70             self.beta = int(s[b+4:])
71             s = s[0:b]
72
73         p = s.split('.')
74         self.major = int(p[0])
75         self.minor = int(p[1])
76
77     def bump(self):
78         self.minor += 1
79         self.pre = False
80         self.beta = None
81
82     def to_pre(self):
83         self.pre = True
84         self.beta = None
85
86     def bump_and_to_pre(self):
87         self.bump()
88         self.pre = True
89         self.beta = None
90
91     def to_release(self):
92         self.pre = False
93         self.beta = None
94
95     def bump_beta(self):
96         if self.pre:
97             self.pre = False
98             self.beta = 1
99         elif self.beta is not None:
100             self.beta += 1
101         elif self.beta is None:
102             self.beta = 1
103
104     def __str__(self):
105         s = '%d.%02d' % (self.major, self.minor)
106         if self.beta is not None:
107             s += 'beta%d' % self.beta
108         elif self.pre:
109             s += 'pre'
110
111         return s
112
113
114 #
115 # Environment
116 #
117
118 class Environment(object):
119     def __init__(self):
120         self.variables = {}
121
122     def set(self, a, b):
123         self.variables[a] = b
124
125     def get(self, a):
126         return self.variables[a]
127
128     def variables_string(self):
129         e = ''
130         for k, v in self.variables.iteritems():
131             e += '%s="%s" ' % (k, v)
132         return e
133
134     def work_dir_cdist(self, sub):
135         assert(false)
136
137     def work_dir_cscript(self):
138         assert(false)
139
140     def build_dependencies(self, target, project):
141         cwd = os.getcwd()
142         if 'dependencies' in project.cscript:
143             for d in project.cscript['dependencies'](target):
144                 dep = Project(d[0], d[1])
145                 dep.checkout(self)
146                 self.build(target, dep)
147         os.chdir(cwd)
148
149     def build(self, target, project):
150         project.cscript['build'](self, target)
151
152     def package(self, target, project):
153         project.checkout(self)
154         if target.platform != 'source':
155             self.build_dependencies(target, project)
156         if target.platform == 'source':
157             command('./waf dist')
158             return os.path.abspath('%s-%s.tar.bz2' % (project.name, project.version))
159         else:
160             project.cscript['build'](self, target)
161             return project.cscript['package'](self, target, project.version)
162
163     def cleanup(self):
164         pass
165
166 #
167 # ChrootEnvironment
168 #
169
170 class ChrootEnvironment(Environment):
171     def __init__(self, chroot):
172         super(ChrootEnvironment, self).__init__()
173         self.chroot = chroot
174         self.dir_in_chroot = '/home/carl'
175         self.chroot_dir = '/home/carl/Environments'
176
177         # ChrootEnvironments work in dir_in_chroot, and clear
178         # it out before use
179         for g in glob.glob('%s/*' % self.work_dir_cdist()):
180             rmtree(g)
181
182         # Environment variables
183         self.set('CXXFLAGS', '-I%s/include' % self.work_dir_cscript())
184         self.set('LINKFLAGS', '-L%s/lib' % self.work_dir_cscript())
185         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % self.work_dir_cscript())
186
187     def work_dir_cdist(self):
188         return '%s/%s%s' % (self.chroot_dir, self.chroot, self.dir_in_chroot)
189
190     def work_dir_cscript(self):
191         return self.dir_in_chroot
192
193     def command(self, c):
194         # Work out the cwd for the chrooted command
195         cwd = os.getcwd()
196         prefix = '%s/%s' % (self.chroot_dir, self.chroot)
197         assert(cwd.startswith(prefix))
198         cwd = cwd[len(prefix):]
199
200         log('schroot [%s] -> %s' % (cwd, c))
201         command('%s schroot -c %s -d %s -p -- %s' % (self.variables_string(), self.chroot, cwd, c))
202
203 #
204 # HostEnvironment
205 #
206
207 class HostEnvironment(Environment):
208     def __init__(self):
209         super(HostEnvironment, self).__init__()
210         self.temp = tempfile.mkdtemp()
211
212     def work_dir_cdist(self):
213         return self.temp
214
215     def work_dir_cscript(self):
216         return self.temp
217
218     def command(self, c):
219         log('host -> %s' % c)
220         command('%s %s' % (self.variables_string(), c))
221
222     def cleanup(self):
223         if args.keep:
224             shutil.copytree('%s/src/%s' % (self.temp, args.project), '%s/%s' % (args.output, args.project))
225         rmtree(self.temp)
226
227
228 #
229 # WindowsEnvironment
230 #
231
232 class WindowsEnvironment(HostEnvironment):
233     def __init__(self, bits):
234         super(WindowsEnvironment, self).__init__()
235
236         self._windows_prefix = '/home/carl/Environments/windows/%d' % bits
237         if not os.path.exists(self._windows_prefix):
238             error('windows prefix %s does not exist' % self._windows_prefix)
239
240         if bits == 32:
241             self.mingw_name = 'i686'
242         else:
243             self.mingw_name = 'x86_64'
244
245         self.mingw_path = '/mingw/bin'
246         self.mingw_prefix = '/mingw/%s-w64-mingw32' % self.mingw_name
247
248         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self._windows_prefix)
249         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % self.work_dir_cscript())
250         self.set('PATH', '%s/bin:%s:%s' % (self._windows_prefix, self.mingw_path, os.environ['PATH']))
251         self.set('CC', '%s-w64-mingw32-gcc' % self.mingw_name)
252         self.set('CXX', '%s-w64-mingw32-g++' % self.mingw_name)
253         self.set('LD', '%s-w64-mingw32-ld' % self.mingw_name)
254         self.set('RANLIB', '%s-w64-mingw32-ranlib' % self.mingw_name)
255         self.set('WINRC', '%s-w64-mingw32-windres' % self.mingw_name)
256         self.set('CXXFLAGS', '-I%s/include -I%s/include -I%s/include' % (self._windows_prefix, self.mingw_prefix, self.work_dir_cscript()))
257         self.set('LINKFLAGS', '-L%s/lib -L%s/lib -L%s/lib' % (self._windows_prefix, self.mingw_prefix, self.work_dir_cscript()))
258
259     def windows_prefix(self):
260         return self._windows_prefix
261
262
263 #
264 # Target
265 #
266
267 class Target:
268     def __init__(self, name):
269         if name.startswith('ubuntu-') or name.startswith('debian-'):
270             self.platform = 'linux'
271             self.version = name.split('-')[1]
272             self.bits = int(name.split('-')[2])
273             self.environment = ChrootEnvironment(name)
274         elif name.startswith('windows-'):
275             self.platform = 'windows'
276             self.bits = int(name.split('-')[1])
277             self.environment = WindowsEnvironment(self.bits)
278             if self.bits == 32:
279                 self.host = 'i686-w64-mingw32'
280             else:
281                 self.hots = 'x86_64-w64-mingw32'
282         elif name == 'source':
283             self.platform = 'source'
284             self.environment = HostEnvironment()
285
286 #
287 # Project
288 #
289  
290 class Project(object):
291     def __init__(self, name, specifier=None):
292         self.name = name
293         self.git_dir = 'ssh://houllier/home/carl/git'
294         self.version = None
295         self.specifier = specifier
296         if self.specifier is None:
297             self.specifier = 'master'
298
299     def checkout(self, env):
300         flags = ''
301         redirect = ''
302         if args.quiet:
303             flags = '-q'
304             redirect = '>/dev/null'
305         command('git clone --depth 0 %s %s/%s.git %s/src/%s' % (flags, self.git_dir, self.name, env.work_dir_cdist(), self.name))
306         os.chdir('%s/src/%s' % (env.work_dir_cdist(), self.name))
307         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
308         command('git submodule init')
309         command('git submodule update')
310
311         self.cscript = {}
312         execfile('%s/src/%s/cscript' % (env.work_dir_cdist(), self.name), self.cscript)
313         
314         if os.path.exists('%s/src/%s/wscript' % (env.work_dir_cdist(), self.name)):
315             f = open('%s/src/%s/wscript' % (env.work_dir_cdist(), self.name), 'r')
316             version = None
317             while 1:
318                 l = f.readline()
319                 if l == '':
320                     break
321
322                 s = l.split()
323                 if len(s) == 3 and s[0] == "VERSION":
324                     self.version = Version(s[2])
325
326             f.close()
327
328
329 def set_version_in_wscript(version):
330     f = open('wscript', 'rw')
331     o = open('wscript.tmp', 'w')
332     while 1:
333         l = f.readline()
334         if l == '':
335             break
336
337         s = l.split()
338         if len(s) == 3 and s[0] == "VERSION":
339             print "Writing %s" % version
340             print >>o,"VERSION = '%s'" % version
341         else:
342             print >>o,l,
343     f.close()
344     o.close()
345
346     os.rename('wscript.tmp', 'wscript')
347
348 def append_version_to_changelog(version):
349     try:
350         f = open('ChangeLog', 'r')
351     except:
352         log('Could not open ChangeLog')
353         return
354
355     c = f.read()
356     f.close()
357
358     f = open('ChangeLog', 'w')
359     now = datetime.datetime.now()
360     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))
361     f.write(c)
362
363 def append_version_to_debian_changelog(version):
364     if not os.path.exists('debian'):
365         log('Could not find debian directory')
366         return
367
368     command('dch -b -v %s-1 "New upstream release."' % version)
369
370 #
371 # Command-line parser
372 #
373
374 parser = argparse.ArgumentParser()
375 parser.add_argument('command')
376 parser.add_argument('-p', '--project', help='project name', required=True)
377 parser.add_argument('--beta', help='beta release', action='store_true')
378 parser.add_argument('--full', help='full release', action='store_true')
379 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
380 parser.add_argument('-o', '--output', help='output directory', default='.')
381 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
382 parser.add_argument('-t', '--target', help='target')
383 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
384 args = parser.parse_args()
385
386 args.output = os.path.abspath(args.output)
387
388 if args.project is None:
389     error('you must specify -p or --project')
390
391 project = Project(args.project, args.checkout)
392
393 if args.command == 'build':
394     if args.target is None:
395         error('you must specify -t or --target')
396
397     target = Target(args.target)
398     env = target.environment
399     project.checkout(env)
400     env.build_dependencies(target, project)
401     env.build(target, project)
402
403     env.cleanup()
404
405 elif args.command == 'package':
406     if args.target is None:
407         error('you must specify -t or --target')
408         
409     target = Target(args.target)
410     env = target.environment
411
412     packages = env.package(target, project)
413     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
414         packages = [packages]
415
416     if target.platform == 'linux':
417         out = '%s/%s-%d' % (args.output, target.version, target.bits)
418         try:
419             os.makedirs(out)
420         except:
421             pass
422         for p in packages:
423             copyfile(p, '%s/%s' % (out, os.path.basename(p)))
424     else:
425         for p in packages:
426             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
427
428     env.cleanup()
429
430 elif args.command == 'release':
431     if args.full is False and args.beta is False:
432         error('you must specify --full or --beta')
433
434     env = HostEnvironment()
435     project.checkout(env)
436
437     version = project.version
438     if args.full:
439         version.to_release()
440     else:
441         version.bump_beta()
442
443     set_version_in_wscript(version)
444     append_version_to_changelog(version)
445     append_version_to_debian_changelog(version)
446
447     command('git commit -a -m "Bump version"')
448     command('git tag -m "v%s" v%s' % (version, version))
449
450     if args.full:
451         version.bump_and_to_pre()
452         set_version_in_wscript(version)
453         command('git commit -a -m "Bump version"')
454
455     command('git push')
456     command('git push --tags')
457
458     env.cleanup()
459
460 elif args.command == 'pot':
461     env = HostEnvironment()
462     project.checkout(env)
463
464     pots = project.cscript['make_pot'](env)
465     for p in pots:
466         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
467
468     env.cleanup()
469
470 elif args.command == 'changelog':
471     env = HostEnvironment()
472     project.checkout(env)
473
474     text = open('ChangeLog', 'r')
475     html = open('%s/changelog.html' % args.output, 'w')
476     versions = 8
477     
478     last = None
479     changes = []
480     
481     while 1:
482         l = text.readline()
483         if l == '':
484             break
485     
486         if len(l) > 0 and l[0] == "\t":
487             s = l.split()
488             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
489                 if not "beta" in s[2]:
490                     if last is not None and len(changes) > 0:
491                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
492                         print >>html,"<ul>"
493                         for c in changes:
494                             print >>html,"<li>%s" % c
495                         print >>html,"</ul>"
496                     last = s[2]
497                     changes = []
498                     versions -= 1
499                     if versions < 0:
500                         break
501             else:
502                 c = l.strip()
503                 if len(c) > 0:
504                     if c[0] == '*':
505                         changes.append(c[2:])
506                     else:
507                         changes[-1] += " " + c
508
509     env.cleanup()
510
511 elif args.command == 'manual':
512     env = HostEnvironment()
513     project.checkout(env)
514
515     dirs = project.cscript['make_manual'](env)
516     for d in dirs:
517         copytree(d, '%s/%s' % (args.output, os.path.basename(d)))
518
519     env.cleanup()
520
521 elif args.command == 'doxygen':
522     env = HostEnvironment()
523     project.checkout(env)
524
525     dirs = project.cscript['make_doxygen'](env)
526     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
527         dirs = [dirs]
528
529     for d in dirs:
530         copytree(d, '%s/%s' % (args.output, 'doc'))
531
532     env.cleanup()
533
534 elif args.command == 'latest':
535     env = HostEnvironment()
536     project.checkout(env)
537
538     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
539     t = f.readline()
540     m = re.compile(".*\((.*)\).*").match(t)
541     latest = None
542     if m:
543         tags = m.group(1).split(', ')
544         for t in tags:
545             if len(t) > 0 and t[0] == 'v':
546                 latest = t[1:]
547
548     print latest
549     env.cleanup()
550
551 else:
552     error('invalid command %s' % args.command)