Use global for OSX SDK.
[cdist.git] / cdist
1 #!/usr/bin/python
2
3 #    Copyright (C) 2012 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 #
20 # Configuration
21 #
22
23 LINUX_DIR_IN_CHROOT = '/home/carl'
24 LINUX_CHROOT_PREFIX = '/home/carl/Environments'
25 WINDOWS_ENVIRONMENT_PREFIX = '/home/carl/Environments/windows'
26 GIT_PREFIX = 'ssh://houllier/home/carl/git'
27 OSX_BUILD_HOST = 'carl@192.168.1.202'
28 OSX_DIR_IN_HOST = '/Users/carl/cdist'
29 OSX_ENVIRONMENT_PREFIX = '/Users/carl/Environments/osx'
30 OSX_SDK_PREFIX = '/Users/carl/SDK'
31 OSX_SDK = '10.6'
32
33 import os
34 import sys
35 import shutil
36 import glob
37 import tempfile
38 import argparse
39 import datetime
40 import subprocess
41 import re
42
43 #
44 # Utility bits
45
46
47 def log(m):
48     if not args.quiet:
49         print '\x1b[33m* %s\x1b[0m' % m
50
51 def error(e):
52     print '\x1b[31mError: %s\x1b[0m' % e
53     sys.exit(1)
54
55 def copytree(a, b):
56     log('copy %s -> %s' % (a, b))
57     shutil.copytree(a, b)
58
59 def copyfile(a, b):
60     log('copy %s -> %s' % (a, b))
61     shutil.copyfile(a, b)
62
63 def rmdir(a):
64     log('remove %s' % a)
65     os.rmdir(a)
66
67 def rmtree(a):
68     log('remove %s' % a)
69     shutil.rmtree(a, ignore_errors=True)
70
71 def command(c, can_fail=False):
72     log(c)
73     r = os.system(c)
74     if (r >> 8) and not can_fail:
75         error('command %s failed' % c)
76
77 def command_and_read(c):
78     log(c)
79     p = subprocess.Popen(c.split(), stdout=subprocess.PIPE)
80     f = os.fdopen(os.dup(p.stdout.fileno()))
81     return f
82
83
84 #
85 # Version
86 #
87
88 class Version:
89     def __init__(self, s):
90         self.pre = False
91         self.beta = None
92
93         if s.startswith("'"):
94             s = s[1:]
95         if s.endswith("'"):
96             s = s[0:-1]
97         
98         if s.endswith('pre'):
99             s = s[0:-3]
100             self.pre = True
101
102         b = s.find("beta")
103         if b != -1:
104             self.beta = int(s[b+4:])
105             s = s[0:b]
106
107         p = s.split('.')
108         self.major = int(p[0])
109         self.minor = int(p[1])
110
111     def bump(self):
112         self.minor += 1
113         self.pre = False
114         self.beta = None
115
116     def to_pre(self):
117         self.pre = True
118         self.beta = None
119
120     def bump_and_to_pre(self):
121         self.bump()
122         self.pre = True
123         self.beta = None
124
125     def to_release(self):
126         self.pre = False
127         self.beta = None
128
129     def bump_beta(self):
130         if self.pre:
131             self.pre = False
132             self.beta = 1
133         elif self.beta is not None:
134             self.beta += 1
135         elif self.beta is None:
136             self.beta = 1
137
138     def __str__(self):
139         s = '%d.%02d' % (self.major, self.minor)
140         if self.beta is not None:
141             s += 'beta%d' % self.beta
142         elif self.pre:
143             s += 'pre'
144
145         return s
146
147 #
148 # Targets
149 #
150
151 class Target(object):
152     def __init__(self, platform):
153         self.platform = platform
154         # Environment variables that we will use when we call cscripts
155         self.variables = {}
156         # Prefix to which builds should be installed by cscripts
157         self.install_prefix = '.'
158
159     def build_dependencies(self, project):
160         cwd = os.getcwd()
161         if 'dependencies' in project.cscript:
162             print project.cscript['dependencies'](self)
163             for d in project.cscript['dependencies'](self):
164                 log('Building dependency %s %s of %s' % (d[0], d[1], project.name))
165                 dep = Project(d[0], '.', d[1])
166                 dep.checkout(self)
167                 self.build_dependencies(dep)
168                 self.build(dep)
169         os.chdir(cwd)
170
171     def build(self, project):
172         project.cscript['build'](self)
173
174     def set(self, a, b):
175         self.variables[a] = b
176
177     def get(self, a):
178         return self.variables[a]
179
180     def variables_string(self, escaped_quotes=False):
181         e = ''
182         for k, v in self.variables.iteritems():
183             if escaped_quotes:
184                 v = v.replace('"', '\\"')
185             e += '%s=%s ' % (k, v)
186         return e
187
188     def cleanup(self):
189         pass
190
191
192 # Windows
193 #
194
195 class WindowsTarget(Target):
196     def __init__(self, bits, directory = None):
197         super(WindowsTarget, self).__init__('windows')
198         self.bits = bits
199         if directory is None:
200             self.directory = tempfile.mkdtemp()
201             self.rmdir = True
202         else:
203             self.directory = directory
204             self.rmdir = False
205         
206         self.windows_prefix = '%s/%d' % (WINDOWS_ENVIRONMENT_PREFIX, self.bits)
207         if not os.path.exists(self.windows_prefix):
208             error('windows prefix %s does not exist' % target.windows_prefix)
209             
210         if self.bits == 32:
211             mingw_name = 'i686'
212         else:
213             mingw_name = 'x86_64'
214
215         mingw_path = '/mingw/%d/bin' % self.bits
216         mingw_prefixes = ['/mingw/%d' % self.bits, '/mingw/%d/%s-w64-mingw32' % (bits, mingw_name)]
217
218         self.set('PKG_CONFIG_LIBDIR', '%s/lib/pkgconfig' % self.windows_prefix)
219         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % self.work_dir_cscript())
220         self.set('PATH', '%s/bin:%s:%s' % (self.windows_prefix, mingw_path, os.environ['PATH']))
221         self.set('CC', '%s-w64-mingw32-gcc' % mingw_name)
222         self.set('CXX', '%s-w64-mingw32-g++' % mingw_name)
223         self.set('LD', '%s-w64-mingw32-ld' % mingw_name)
224         self.set('RANLIB', '%s-w64-mingw32-ranlib' % mingw_name)
225         self.set('WINRC', '%s-w64-mingw32-windres' % mingw_name)
226         cxx = '-I%s/include -I%s/include' % (self.windows_prefix, self.work_dir_cscript())
227         link = '-L%s/lib -L%s/lib' % (self.windows_prefix, self.work_dir_cscript())
228         for p in mingw_prefixes:
229             cxx += ' -I%s/include' % p
230             link += ' -L%s/lib' % p
231         self.set('CXXFLAGS', '"%s"' % cxx)
232         self.set('LINKFLAGS', '"%s"' % link)
233
234     def work_dir_cdist(self):
235         return self.directory
236
237     def work_dir_cscript(self):
238         return self.directory
239
240     def command(self, c):
241         log('host -> %s' % c)
242         command('%s %s' % (self.variables_string(), c))
243
244     def cleanup(self):
245         if self.rmdir:
246             rmtree(self.directory)
247
248     def package(self, project):
249         project.checkout(self)
250         self.build_dependencies(project)
251         project.cscript['build'](self)
252         return project.cscript['package'](self, project.version)
253
254 #
255 # Linux
256 #
257
258 class LinuxTarget(Target):
259     def __init__(self, distro, version, bits):
260         super(LinuxTarget, self).__init__('linux')
261         self.distro = distro
262         self.version = version
263         self.bits = bits
264         self.chroot = '%s-%s-%d' % (self.distro, self.version, self.bits)
265
266         for g in glob.glob('%s/*' % self.work_dir_cdist()):
267             rmtree(g)
268
269         self.set('CXXFLAGS', '-I%s/include' % self.work_dir_cscript())
270         self.set('LINKFLAGS', '-L%s/lib' % self.work_dir_cscript())
271         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig' % self.work_dir_cscript())
272
273     def work_dir_cdist(self):
274         return '%s/%s%s' % (LINUX_CHROOT_PREFIX, self.chroot, LINUX_DIR_IN_CHROOT)
275
276     def work_dir_cscript(self):
277         return LINUX_DIR_IN_CHROOT
278
279     def command(self, c):
280         # Work out the cwd for the chrooted command
281         cwd = os.getcwd()
282         prefix = '%s/%s' % (LINUX_CHROOT_PREFIX, self.chroot)
283         assert(cwd.startswith(prefix))
284         cwd = cwd[len(prefix):]
285
286         log('schroot [%s] -> %s' % (cwd, c))
287         command('%s schroot -c %s -d %s -p -- %s' % (self.variables_string(), self.chroot, cwd, c))
288
289     def package(self, target):
290         project.checkout(self)
291         self.build_dependencies(project)
292         project.cscript['build'](self)
293         return project.cscript['package'](self, project.version)
294
295
296 #
297 # OS X
298 #
299
300 class OSXTarget(Target):
301     def __init__(self):
302         super(OSXTarget, self).__init__('osx')
303
304         self.host_mount_dir = tempfile.mkdtemp()
305
306         # Mount the remote host on host_mount_dir
307         command('sshfs %s:%s %s' % (OSX_BUILD_HOST, OSX_DIR_IN_HOST, self.host_mount_dir))
308         for g in glob.glob('%s/*' % self.host_mount_dir):
309             rmtree(g)
310
311         flags = '-isysroot %s/MacOSX%s.sdk -arch i386 -arch x86_64' % (OSX_SDK_PREFIX, OSX_SDK)
312         enviro = '%s/%s' % (OSX_ENVIRONMENT_PREFIX, OSX_SDK)
313
314         # Environment variables
315         self.set('CFLAGS', '"-I%s/include -I%s/include %s"' % (OSX_DIR_IN_HOST, enviro, flags))
316         self.set('CXXFLAGS', '"-I%s/include -I%s/include %s"' % (OSX_DIR_IN_HOST, enviro, flags))
317         self.set('LDFLAGS', '"-L%s/lib -L%s/lib %s"' % (OSX_DIR_IN_HOST, enviro, flags))
318         self.set('LINKFLAGS', '"-L%s/lib -L%s/lib %s"' % (OSX_DIR_IN_HOST, enviro, flags))
319         self.set('PKG_CONFIG_PATH', '%s/lib/pkgconfig:%s/lib/pkgconfig' % (OSX_DIR_IN_HOST, enviro))
320         self.set('PATH', '$PATH:/usr/bin:/sbin:/usr/local/bin:%s/bin' % enviro)
321         self.set('MACOSX_DEPLOYMENT_TARGET', OSX_SDK)
322
323     def work_dir_cdist(self):
324         return self.host_mount_dir
325
326     def work_dir_cscript(self):
327         return OSX_DIR_IN_HOST
328
329     def package(self, project):
330         project.checkout(self)
331         self.build_dependencies(target, project)
332         # We have to build 32- and 64-bit versions
333         # and then stick them together to make a universal binary
334         target.bits = 32
335         self.install_prefix = '32'
336         project.cscript['build'](self, target)
337         target.bits = 64
338         self.install_prefix = '64'
339         project.cscript['build'](self, target)
340
341     def command(self, c):
342         # Work out the cwd for the chrooted command
343         cwd = os.getcwd()
344         assert(cwd.startswith(self.host_mount_dir))
345         cwd = cwd[len(self.host_mount_dir):]
346
347         log('ssh [%s] -> %s' % (cwd, c))
348         command('ssh %s -- "cd %s%s; %s %s"' % (OSX_BUILD_HOST, OSX_DIR_IN_HOST, cwd, self.variables_string(True), c))
349
350     def cleanup(self):
351         os.chdir('/')
352         command('fusermount -u %s' % self.host_mount_dir)
353         rmdir(self.host_mount_dir)
354
355 #
356 # Source
357 #
358
359 class SourceTarget(Target):
360     def __init__(self):
361         super(SourceTarget, self).__init__('source')
362         self.directory = tempfile.mkdtemp()
363
364     def work_dir_cdist(self):
365         return self.directory
366
367     def work_dir_cscript(self):
368         return self.directory
369
370     def command(self, c):
371         log('host -> %s' % c)
372         command('%s %s' % (self.variables_string(), c))
373
374     def cleanup(self):
375         rmtree(self.directory)
376
377     def package(self, project):
378         command('./waf dist')
379         if project.directory != '.':
380             return os.path.abspath('%s-%s.tar.bz2' % (project.directory, project.version))
381         return os.path.abspath('%s-%s.tar.bz2' % (project.name, project.version))
382
383
384 # @param s Target string:
385 #       windows-{32,64}
386 #    or ubuntu-version-{32,64}
387 #    or debian-version-{32,64}
388 #    or osx
389 #    or source      
390 def target_factory(s):
391     if s.startswith('windows-'):
392         return WindowsTarget(int(s.split('-')[1]))
393     elif s.startswith('ubuntu-') or s.startswith('debian-'):
394         p = s.split('-')
395         return LinuxTarget(p[0], p[1], int(p[2]))
396     elif s == 'osx':
397         return OSXTarget()
398     elif s == 'source':
399         return SourceTarget()
400
401     return None
402
403
404 #
405 # Project
406 #
407  
408 class Project(object):
409     def __init__(self, name, directory, specifier=None):
410         self.name = name
411         self.directory = directory
412         self.version = None
413         self.specifier = specifier
414         if self.specifier is None:
415             self.specifier = 'master'
416
417     def checkout(self, target):
418         flags = ''
419         redirect = ''
420         if args.quiet:
421             flags = '-q'
422             redirect = '>/dev/null'
423         command('git clone --depth 0 %s %s/%s.git %s/src/%s' % (flags, GIT_PREFIX, self.name, target.work_dir_cdist(), self.name))
424         os.chdir('%s/src/%s' % (target.work_dir_cdist(), self.name))
425         command('git checkout %s %s %s' % (flags, self.specifier, redirect))
426         command('git submodule init')
427         command('git submodule update')
428         os.chdir(self.directory)
429
430         proj = '%s/src/%s/%s' % (target.work_dir_cdist(), self.name, self.directory)
431
432         self.read_cscript('%s/cscript' % proj)
433         
434         if os.path.exists('%s/wscript' % proj):
435             f = open('%s/wscript' % proj, 'r')
436             version = None
437             while 1:
438                 l = f.readline()
439                 if l == '':
440                     break
441
442                 s = l.split()
443                 if len(s) == 3 and s[0] == "VERSION":
444                     self.version = Version(s[2])
445
446             f.close()
447
448     def read_cscript(self, s):
449         self.cscript = {}
450         execfile(s, self.cscript)
451
452 def set_version_in_wscript(version):
453     f = open('wscript', 'rw')
454     o = open('wscript.tmp', 'w')
455     while 1:
456         l = f.readline()
457         if l == '':
458             break
459
460         s = l.split()
461         if len(s) == 3 and s[0] == "VERSION":
462             print "Writing %s" % version
463             print >>o,"VERSION = '%s'" % version
464         else:
465             print >>o,l,
466     f.close()
467     o.close()
468
469     os.rename('wscript.tmp', 'wscript')
470
471 def append_version_to_changelog(version):
472     try:
473         f = open('ChangeLog', 'r')
474     except:
475         log('Could not open ChangeLog')
476         return
477
478     c = f.read()
479     f.close()
480
481     f = open('ChangeLog', 'w')
482     now = datetime.datetime.now()
483     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))
484     f.write(c)
485
486 def append_version_to_debian_changelog(version):
487     if not os.path.exists('debian'):
488         log('Could not find debian directory')
489         return
490
491     command('dch -b -v %s-1 "New upstream release."' % version)
492
493 #
494 # Command-line parser
495 #
496
497 parser = argparse.ArgumentParser()
498 parser.add_argument('command')
499 parser.add_argument('-p', '--project', help='project name', required=True)
500 parser.add_argument('-d', '--directory', help='directory within project repo', default='.')
501 parser.add_argument('--beta', help='beta release', action='store_true')
502 parser.add_argument('--full', help='full release', action='store_true')
503 parser.add_argument('-c', '--checkout', help='string to pass to git for checkout')
504 parser.add_argument('-o', '--output', help='output directory', default='.')
505 parser.add_argument('-q', '--quiet', help='be quiet', action='store_true')
506 parser.add_argument('-t', '--target', help='target')
507 parser.add_argument('-k', '--keep', help='keep working tree', action='store_true')
508 args = parser.parse_args()
509
510 args.output = os.path.abspath(args.output)
511
512 if args.project is None:
513     error('you must specify -p or --project')
514
515 project = Project(args.project, args.directory, args.checkout)
516
517 if args.command == 'build':
518     if args.target is None:
519         error('you must specify -t or --target')
520
521     target = target_factory(args.target)
522     project.checkout(target)
523     target.build_dependencies(project)
524     target.build(project)
525     target.cleanup()
526
527 elif args.command == 'package':
528     if args.target is None:
529         error('you must specify -t or --target')
530         
531     target = target_factory(args.target)
532
533     packages = target.package(project)
534     if hasattr(packages, 'strip') or (not hasattr(packages, '__getitem__') and not hasattr(packages, '__iter__')):
535         packages = [packages]
536
537     if target.platform == 'linux':
538         out = '%s/%s-%d' % (args.output, target.version, target.bits)
539         try:
540             os.makedirs(out)
541         except:
542             pass
543         for p in packages:
544             copyfile(p, '%s/%s' % (out, os.path.basename(p)))
545     else:
546         for p in packages:
547             copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
548
549     target.cleanup()
550
551 elif args.command == 'release':
552     if args.full is False and args.beta is False:
553         error('you must specify --full or --beta')
554
555     target = SourceTarget()
556     project.checkout(target)
557
558     version = project.version
559     if args.full:
560         version.to_release()
561     else:
562         version.bump_beta()
563
564     set_version_in_wscript(version)
565     append_version_to_changelog(version)
566     append_version_to_debian_changelog(version)
567
568     command('git commit -a -m "Bump version"')
569     command('git tag -m "v%s" v%s' % (version, version))
570
571     if args.full:
572         version.bump_and_to_pre()
573         set_version_in_wscript(version)
574         command('git commit -a -m "Bump version"')
575
576     command('git push')
577     command('git push --tags')
578
579     target.cleanup()
580
581 elif args.command == 'pot':
582     target = SourceTarget()
583     project.checkout(target)
584
585     pots = project.cscript['make_pot'](target)
586     for p in pots:
587         copyfile(p, '%s/%s' % (args.output, os.path.basename(p)))
588
589     target.cleanup()
590
591 elif args.command == 'changelog':
592     target = SourceTarget()
593     project.checkout(target)
594
595     text = open('ChangeLog', 'r')
596     html = open('%s/changelog.html' % args.output, 'w')
597     versions = 8
598     
599     last = None
600     changes = []
601     
602     while 1:
603         l = text.readline()
604         if l == '':
605             break
606     
607         if len(l) > 0 and l[0] == "\t":
608             s = l.split()
609             if len(s) == 4 and s[1] == "Version" and s[3] == "released.":
610                 if not "beta" in s[2]:
611                     if last is not None and len(changes) > 0:
612                         print >>html,"<h2>Changes between version %s and %s</h2>" % (s[2], last)
613                         print >>html,"<ul>"
614                         for c in changes:
615                             print >>html,"<li>%s" % c
616                         print >>html,"</ul>"
617                     last = s[2]
618                     changes = []
619                     versions -= 1
620                     if versions < 0:
621                         break
622             else:
623                 c = l.strip()
624                 if len(c) > 0:
625                     if c[0] == '*':
626                         changes.append(c[2:])
627                     else:
628                         changes[-1] += " " + c
629
630     target.cleanup()
631
632 elif args.command == 'manual':
633     target = SourceTarget()
634     project.checkout(target)
635
636     dirs = project.cscript['make_manual'](target)
637     for d in dirs:
638         copytree(d, '%s/%s' % (args.output, os.path.basename(d)))
639
640     target.cleanup()
641
642 elif args.command == 'doxygen':
643     target = SourceTarget()
644     project.checkout(target)
645
646     dirs = project.cscript['make_doxygen'](target)
647     if hasattr(dirs, 'strip') or (not hasattr(dirs, '__getitem__') and not hasattr(dirs, '__iter__')):
648         dirs = [dirs]
649
650     for d in dirs:
651         copytree(d, '%s/%s' % (args.output, 'doc'))
652
653     target.cleanup()
654
655 elif args.command == 'latest':
656     target = SourceTarget()
657     project.checkout(target)
658
659     f = command_and_read('git log --tags --simplify-by-decoration --pretty="%d"')
660     t = f.readline()
661     m = re.compile(".*\((.*)\).*").match(t)
662     latest = None
663     if m:
664         tags = m.group(1).split(', ')
665         for t in tags:
666             if len(t) > 0 and t[0] == 'v':
667                 latest = t[1:]
668
669     print latest
670     target.cleanup()
671
672 elif args.command == 'test':
673     if args.target is None:
674         error('you must specify -t or --target')
675
676     target = Target(args.target)
677     project.read_cscript('cscript')
678     target.build(project)
679
680 else:
681     error('invalid command %s' % args.command)