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