Support subdirectories.
[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, directory=None):
209         super(HostEnvironment, self).__init__()
210         if directory is None:
211             self.directory = tempfile.mkdtemp()
212             self.rmdir = True
213         else:
214             self.directory = directory
215             self.rmdir = False
216
217     def work_dir_cdist(self):
218         return self.directory
219
220     def work_dir_cscript(self):
221         return self.directory
222
223     def command(self, c):
224         log('host -> %s' % c)
225         command('%s %s' % (self.variables_string(), c))
226
227     def cleanup(self):
228         if self.rmdir:
229             rmtree(self.directory)
230
231
232 def prepare_for_windows(env, bits):
233     env.windows_prefix = '/home/carl/Environments/windows/%d' % bits
234     if not os.path.exists(env.windows_prefix):
235         error('windows prefix %s does not exist' % env.windows_prefix)
236
237     if bits == 32:
238         mingw_name = 'i686'
239     else:
240         mingw_name = 'x86_64'
241
242     mingw_path = '/mingw/bin'
243     mingw_prefixes = ['/mingw', '/mingw/%s-w64-mingw32' % mingw_name]
244
245     env.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % env.windows_prefix)
246     env.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % env.work_dir_cscript())
247     env.set('PATH', '%s/bin:%s:%s' % (env.windows_prefix, mingw_path, os.environ['PATH']))
248     env.set('CC', '%s-w64-mingw32-gcc' % mingw_name)
249     env.set('CXX', '%s-w64-mingw32-g++' % mingw_name)
250     env.set('LD', '%s-w64-mingw32-ld' % mingw_name)
251     env.set('RANLIB', '%s-w64-mingw32-ranlib' % mingw_name)
252     env.set('WINRC', '%s-w64-mingw32-windres' % mingw_name)
253     cxx = '-I%s/include -I%s/include' % (env.windows_prefix, env.work_dir_cscript())
254     link = '-L%s/lib -L%s/lib' % (env.windows_prefix, env.work_dir_cscript())
255     for p in mingw_prefixes:
256         cxx += ' -I%s/include' % p
257         link += ' -L%s/lib' % p
258     env.set('CXXFLAGS', cxx)
259     env.set('LINKFLAGS', link)
260
261
262 #
263 # Target
264 #
265
266 class Target:
267     def __init__(self, name):
268         self.name = 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         elif name.startswith('windows-'):
274             self.platform = 'windows'
275             self.bits = int(name.split('-')[1])
276         elif name == 'source':
277             self.platform = 'source'
278
279 def environment_for_target(target, directory):
280     if target.platform == 'linux':
281         return ChrootEnvironment(target.name)
282     elif target.platform == 'windows':
283         env = HostEnvironment(directory)
284         prepare_for_windows(env, target.bits)
285         return env
286     elif target.platform == 'source':
287         return HostEnvironment()
288
289     return None
290
291 #
292 # Project
293 #
294  
295 class Project(object):
296     def __init__(self, name, directory, specifier=None):
297         self.name = name
298         self.directory = directory
299         self.git_dir = 'ssh://houllier/home/carl/git'
300         self.version = None
301         self.specifier = specifier
302         if self.specifier is None:
303             self.specifier = 'master'
304
305     def checkout(self, env):
306         flags = ''
307         redirect = ''
308         if args.quiet:
309             flags = '-q'
310             redirect = '>/dev/null'
311         command('git clone --depth 0 %s %s/%s.git %s/src/%s' % (flags, self.git_dir, self.name, env.work_dir_cdist(), self.name))
312         os.chdir('%s/src/%s' % (env.work_dir_cdist(), self.name))
313         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
314         command('git submodule init')
315         command('git submodule update')
316         os.chdir(self.directory)
317
318         proj = '%s/src/%s/%s' % (env.work_dir_cdist(), self.name, self.directory)
319
320         self.read_cscript('%s/cscript' % proj)
321         
322         if os.path.exists('%s/wscript' % proj):
323             f = open('%s/wscript' % proj, 'r')
324             version = None
325             while 1:
326                 l = f.readline()
327                 if l == '':
328                     break
329
330                 s = l.split()
331                 if len(s) == 3 and s[0] == "VERSION":
332                     self.version = Version(s[2])
333
334             f.close()
335
336     def read_cscript(self, s):
337         self.cscript = {}
338         execfile(s, self.cscript)
339
340 def set_version_in_wscript(version):
341     f = open('wscript', 'rw')
342     o = open('wscript.tmp', 'w')
343     while 1:
344         l = f.readline()
345         if l == '':
346             break
347
348         s = l.split()
349         if len(s) == 3 and s[0] == "VERSION":
350             print "Writing %s" % version
351             print >>o,"VERSION = '%s'" % version
352         else:
353             print >>o,l,
354     f.close()
355     o.close()
356
357     os.rename('wscript.tmp', 'wscript')
358
359 def append_version_to_changelog(version):
360     try:
361         f = open('ChangeLog', 'r')
362     except:
363         log('Could not open ChangeLog')
364         return
365
366     c = f.read()
367     f.close()
368
369     f = open('ChangeLog', 'w')
370     now = datetime.datetime.now()
371     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))
372     f.write(c)
373
374 def append_version_to_debian_changelog(version):
375     if not os.path.exists('debian'):
376         log('Could not find debian directory')
377         return
378
379     command('dch -b -v %s-1 "New upstream release."' % version)
380
381 #
382 # Command-line parser
383 #
384
385 parser = argparse.ArgumentParser()
386 parser.add_argument('command')
387 parser.add_argument('-p', '--project', help='project name', required=True)
388 parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
389 parser.add_argument('--beta', help='beta release', action='store_true')
390 parser.add_argument('--full', help='full release', action='store_true')
391 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
392 parser.add_argument('-o', '--output', help='output directory', default='.')
393 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
394 parser.add_argument('-t', '--target', help='target')
395 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
396 args = parser.parse_args()
397
398 args.output = os.path.abspath(args.output)
399
400 if args.project is None:
401     error('you must specify -p or --project')
402
403 project = Project(args.project, args.directory, args.checkout)
404
405 if args.command == 'build':
406     if args.target is None:
407         error('you must specify -t or --target')
408
409     target = Target(args.target)
410     env = environment_for_target(target)
411     project.checkout(env)
412     env.build_dependencies(target, project)
413     env.build(target, project)
414
415     env.cleanup()
416
417 elif args.command == 'package':
418     if args.target is None:
419         error('you must specify -t or --target')
420         
421     target = Target(args.target)
422     env = environment_for_target(target, None)
423
424     packages = env.package(target, project)
425     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
426         packages = [packages]
427
428     if target.platform == 'linux':
429         out = '%s/%s-%d' % (args.output, target.version, target.bits)
430         try:
431             os.makedirs(out)
432         except:
433             pass
434         for p in packages:
435             copyfile(p, '%s/%s' % (out, os.path.basename(p)))
436     else:
437         for p in packages:
438             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
439
440     env.cleanup()
441
442 elif args.command == 'release':
443     if args.full is False and args.beta is False:
444         error('you must specify --full or --beta')
445
446     env = HostEnvironment()
447     project.checkout(env)
448
449     version = project.version
450     if args.full:
451         version.to_release()
452     else:
453         version.bump_beta()
454
455     set_version_in_wscript(version)
456     append_version_to_changelog(version)
457     append_version_to_debian_changelog(version)
458
459     command('git commit -a -m "Bump version"')
460     command('git tag -m "v%s" v%s' % (version, version))
461
462     if args.full:
463         version.bump_and_to_pre()
464         set_version_in_wscript(version)
465         command('git commit -a -m "Bump version"')
466
467     command('git push')
468     command('git push --tags')
469
470     env.cleanup()
471
472 elif args.command == 'pot':
473     env = HostEnvironment()
474     project.checkout(env)
475
476     pots = project.cscript['make_pot'](env)
477     for p in pots:
478         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
479
480     env.cleanup()
481
482 elif args.command == 'changelog':
483     env = HostEnvironment()
484     project.checkout(env)
485
486     text = open('ChangeLog', 'r')
487     html = open('%s/changelog.html' % args.output, 'w')
488     versions = 8
489     
490     last = None
491     changes = []
492     
493     while 1:
494         l = text.readline()
495         if l == '':
496             break
497     
498         if len(l) > 0 and l[0] == "\t":
499             s = l.split()
500             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
501                 if not "beta" in s[2]:
502                     if last is not None and len(changes) > 0:
503                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
504                         print >>html,"<ul>"
505                         for c in changes:
506                             print >>html,"<li>%s" % c
507                         print >>html,"</ul>"
508                     last = s[2]
509                     changes = []
510                     versions -= 1
511                     if versions < 0:
512                         break
513             else:
514                 c = l.strip()
515                 if len(c) > 0:
516                     if c[0] == '*':
517                         changes.append(c[2:])
518                     else:
519                         changes[-1] += " " + c
520
521     env.cleanup()
522
523 elif args.command == 'manual':
524     env = HostEnvironment()
525     project.checkout(env)
526
527     dirs = project.cscript['make_manual'](env)
528     for d in dirs:
529         copytree(d, '%s/%s' % (args.output, os.path.basename(d)))
530
531     env.cleanup()
532
533 elif args.command == 'doxygen':
534     env = HostEnvironment()
535     project.checkout(env)
536
537     dirs = project.cscript['make_doxygen'](env)
538     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
539         dirs = [dirs]
540
541     for d in dirs:
542         copytree(d, '%s/%s' % (args.output, 'doc'))
543
544     env.cleanup()
545
546 elif args.command == 'latest':
547     env = HostEnvironment()
548     project.checkout(env)
549
550     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
551     t = f.readline()
552     m = re.compile(".*\((.*)\).*").match(t)
553     latest = None
554     if m:
555         tags = m.group(1).split(', ')
556         for t in tags:
557             if len(t) > 0 and t[0] == 'v':
558                 latest = t[1:]
559
560     print latest
561     env.cleanup()
562
563 elif args.command == 'test':
564     if args.target is None:
565         error('you must specify -t or --target')
566
567     target = Target(args.target)
568     env = environment_for_target(target, '.')
569     project.read_cscript('cscript')
570     env.build(target, project)
571
572 else:
573     error('invalid command %s' % args.command)