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