blob: 8925b5ca78cd2bebcf97515c41b886fa2298871f [file] [log] [blame]
Zhenkai Zhu54ed4692013-02-24 19:25:04 -08001#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4#
5# Loosely based on original bash-version by Sebastian Schlingmann (based, again, on a OSX application bundler
6# by Thomas Keller).
7#
8
9import sys, os, string, re, shutil, plistlib, tempfile, exceptions, datetime, tarfile
10from subprocess import Popen, PIPE
11from optparse import OptionParser
12
13options = None
14
15def gitrev():
16 return os.popen('git describe').read()[:-1]
17
18def codesign(path):
19 '''Call the codesign executable.'''
20
21 if hasattr(path, 'isalpha'):
22 path = (path,)
23 for p in path:
Zhenkai Zhu9fdf3cd2013-02-28 12:36:06 -080024 #p = Popen(('codesign', '--keychain', options.codesign_keychain, '--signature-size', '6400', '-vvvv', '-s', options.codesign, p))
25 p = Popen(('codesign', '-vvvv', '-s', options.codesign, p))
Zhenkai Zhu54ed4692013-02-24 19:25:04 -080026 retval = p.wait()
27 if retval != 0:
28 return retval
29 return 0
30
31class AppBundle(object):
32
33 def is_system_lib(self, lib):
34 '''
35 Is the library a system library, meaning that we should not include it in our bundle?
36 '''
37 if lib.startswith('/System/Library/'):
38 return True
39 if lib.startswith('/usr/lib/'):
40 return True
41
42 return False
43
44 def is_dylib(self, lib):
45 '''
46 Is the library a dylib?
47 '''
48 return lib.endswith('.dylib')
49
50 def get_framework_base(self, fw):
51 '''
52 Extracts the base .framework bundle path from a library in an abitrary place in a framework.
53 '''
54 paths = fw.split('/')
55 for i, str in enumerate(paths):
56 if str.endswith('.framework'):
57 return '/'.join(paths[:i+1])
58 return None
59
60 def is_framework(self, lib):
61 '''
62 Is the library a framework?
63 '''
64 return bool(self.get_framework_base(lib))
65
66 def get_binary_libs(self, path):
67 '''
68 Get a list of libraries that we depend on.
69 '''
70 m = re.compile('^\t(.*)\ \(.*$')
71 libs = Popen(['otool', '-L', path], stdout=PIPE).communicate()[0]
72 libs = string.split(libs, '\n')
73 ret = []
74 bn = os.path.basename(path)
75 for line in libs:
76 g = m.match(line)
77 if g is not None:
78 lib = g.groups()[0]
79 if lib != bn:
80 ret.append(lib)
81 return ret
82
83 def handle_libs(self):
84 '''
85 Copy non-system libraries that we depend on into our bundle, and fix linker
86 paths so they are relative to our bundle.
87 '''
88 print ' * Taking care of libraries'
89
90 # Does our fwpath exist?
91 fwpath = os.path.join(os.path.abspath(self.bundle), 'Contents', 'Frameworks')
92 if not os.path.exists(fwpath):
93 os.mkdir(fwpath)
94
95 self.handle_binary_libs()
96
97 #actd = os.path.join(os.path.abspath(self.bundle), 'Contents', 'MacOS', 'actd')
98 #if os.path.exists(actd):
99 # self.handle_binary_libs(actd)
100
101 def handle_binary_libs(self, macho=None):
102 '''
103 Fix up dylib depends for a specific binary.
104 '''
105 print "macho is ", macho
106 # Does our fwpath exist already? If not, create it.
107 if not self.framework_path:
108 self.framework_path = self.bundle + '/Contents/Frameworks'
109 if not os.path.exists(self.framework_path):
110 os.mkdir(self.framework_path)
111 else:
Zhenkai Zhudafbbff2013-03-04 13:26:15 -0800112 # shutil.rmtree(self.framework_path)
113 # os.mkdir(self.framework_path)
114 # do not remove /Contents/Framework; we have Sparkle.framework there
115 pass
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800116
117 # If we weren't explicitly told which binary to operate on, pick the
118 # bundle's default executable from its property list.
119 if macho is None:
120 macho = os.path.abspath(self.binary)
121 else:
122 macho = os.path.abspath(macho)
123
124 libs = self.get_binary_libs(macho)
125
126 for lib in libs:
127
128 # Skip system libraries
129 if self.is_system_lib(lib):
130 continue
131
132 # Frameworks are 'special'.
133 if self.is_framework(lib):
134 fw_path = self.get_framework_base(lib)
135 basename = os.path.basename(fw_path)
136 name = basename.split('.framework')[0]
137 rel = basename + '/' + name
138
Zhenkai Zhu9fdf3cd2013-02-28 12:36:06 -0800139 if fw_path.startswith('@loader_path') or fw_path.startswith('@executable_path'):
140 continue
141
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800142 abs = self.framework_path + '/' + rel
143
144 if not basename in self.handled_libs:
145 dst = self.framework_path + '/' + basename
146 shutil.copytree(fw_path, dst, symlinks=True)
147 if name.startswith('Qt'):
148 try:
149 os.remove(dst + '/Headers')
150 os.remove(dst + '/' + name + '.prl')
151 os.remove(dst + '/' + name + '_debug')
152 os.remove(dst + '/' + name + '_debug.prl')
153 shutil.rmtree(dst + '/Versions/4/Headers')
154 os.remove(dst + '/Versions/4/' + name + '_debug')
155 except OSError:
156 pass
157 os.chmod(abs, 0755)
158 os.system('install_name_tool -id @executable_path/../Frameworks/%s %s' % (rel, abs))
159 self.handled_libs[basename] = True
160 self.handle_binary_libs(abs)
161 os.chmod(macho, 0755)
162 os.system('install_name_tool -change %s @executable_path/../Frameworks/%s %s' % (lib, rel, macho))
163
164 # Regular dylibs
165 else:
166 basename = os.path.basename(lib)
167 rel = basename
168
169 if not basename in self.handled_libs:
170 print lib
171 print self.framework_path + '/' + basename
172 try:
173 shutil.copy(lib, self.framework_path + '/' + basename)
174 except IOError:
175 print "IOError!" + self.framework_path + '/' + basename + "does not exist\n"
176 continue
177
178 abs = self.framework_path + '/' + rel
179 os.chmod(abs, 0755)
180 os.system('install_name_tool -id @executable_path/../Frameworks/%s %s' % (rel, abs))
181 self.handled_libs[basename] = True
182 self.handle_binary_libs(abs)
183 os.chmod(macho, 0755)
184 os.system('install_name_tool -change %s @executable_path/../Frameworks/%s %s' % (lib, rel, macho))
185
186 def copy_resources(self, rsrcs):
187 '''
188 Copy needed resources into our bundle.
189 '''
190 print ' * Copying needed resources'
191 rsrcpath = os.path.join(self.bundle, 'Contents', 'Resources')
192 if not os.path.exists(rsrcpath):
193 os.mkdir(rsrcpath)
194
195 # Copy resources already in the bundle
196 for rsrc in rsrcs:
197 b = os.path.basename(rsrc)
198 if os.path.isdir(rsrc):
199 shutil.copytree(rsrc, os.path.join(rsrcpath, b), symlinks=True)
200 elif os.path.isfile(rsrc):
201 shutil.copy(rsrc, os.path.join(rsrcpath, b))
202
203 return
204
205 def copy_qt_plugins(self):
206 '''
207 Copy over any needed Qt plugins.
208 '''
209
210 print ' * Copying Qt and preparing plugins'
211
212 src = os.popen('qmake -query QT_INSTALL_PLUGINS').read().strip()
213 dst = os.path.join(self.bundle, 'Contents', 'QtPlugins')
214 shutil.copytree(src, dst, symlinks=False)
215
216 top = dst
217 files = {}
218
219 def cb(arg, dirname, fnames):
220 if dirname == top:
221 return
222 files[os.path.basename(dirname)] = fnames
223
224 os.path.walk(top, cb, None)
225
226 exclude = ( 'phonon_backend', 'designer', 'script' )
227
228 for dir, files in files.items():
229 absdir = dst + '/' + dir
230 if dir in exclude:
231 shutil.rmtree(absdir)
232 continue
233 for file in files:
234 abs = absdir + '/' + file
235 if file.endswith('_debug.dylib'):
236 os.remove(abs)
237 else:
238 os.system('install_name_tool -id %s %s' % (file, abs))
239 self.handle_binary_libs(abs)
240
241 def update_plist(self):
242 '''
243 Modify our bundle's Info.plist to make it ready for release.
244 '''
245 if self.version is not None:
246 print ' * Changing version in Info.plist'
247 p = self.infoplist
248 p['CFBundleVersion'] = self.version
249 p['CFBundleExecutable'] = "ChronoShare"
250 plistlib.writePlist(p, self.infopath)
251
252
253 def set_min_macosx_version(self, version):
254 '''
255 Set the minimum version of Mac OS X version that this App will run on.
256 '''
257 print ' * Setting minimum Mac OS X version to: %s' % (version)
258 self.infoplist['LSMinimumSystemVersion'] = version
259
260 def done(self):
261 plistlib.writePlist(self.infoplist, self.infopath)
262 print ' * Done!'
263 print ''
264
265 def __init__(self, bundle, version=None):
266 self.framework_path = ''
267 self.handled_libs = {}
268 self.bundle = bundle
269 self.version = version
270 self.infopath = os.path.join(os.path.abspath(bundle), 'Contents', 'Info.plist')
271 self.infoplist = plistlib.readPlist(self.infopath)
272 self.binary = os.path.join(os.path.abspath(bundle), 'Contents', 'MacOS', self.infoplist['CFBundleExecutable'])
273 print ' * Preparing AppBundle'
274
275class FolderObject(object):
276 class Exception(exceptions.Exception):
277 pass
278
279 def __init__(self):
280 self.tmp = tempfile.mkdtemp()
281
282 def copy(self, src, dst='/'):
283 '''
284 Copy a file or directory into foler
285 '''
286 asrc = os.path.abspath(src)
287
288 if dst[0] != '/':
289 raise self.Exception
290
291 # Determine destination
292 if dst[-1] == '/':
293 adst = os.path.abspath(self.tmp + '/' + dst + os.path.basename(src))
294 else:
295 adst = os.path.abspath(self.tmp + '/' + dst)
296
297 if os.path.isdir(asrc):
298 print ' * Copying directory: %s' % os.path.basename(asrc)
299 shutil.copytree(asrc, adst, symlinks=True)
300 elif os.path.isfile(asrc):
301 print ' * Copying file: %s' % os.path.basename(asrc)
302 shutil.copy(asrc, adst)
303
304 def symlink(self, src, dst):
305 '''
306 Create a symlink inside the folder
307 '''
308 asrc = os.path.abspath(src)
309 adst = self.tmp + '/' + dst
310 print " * Creating symlink %s" % os.path.basename(asrc)
311 os.symlink(asrc, adst)
312
313 def mkdir(self, name):
314 '''
315 Create a directory inside the folder.
316 '''
317 print ' * Creating directory %s' % os.path.basename(name)
318 adst = self.tmp + '/' + name
319 os.makedirs(adst)
320
321class DiskImage(FolderObject):
322
323 def __init__(self, filename, volname):
324 FolderObject.__init__(self)
325 print ' * Preparing to create diskimage'
326 self.filename = filename
327 self.volname = volname
328
329 def create(self):
330 '''
331 Create the disk image
332 '''
333 print ' * Creating disk image. Please wait...'
334 if os.path.exists(self.filename):
335 shutil.rmtree(self.filename)
336 p = Popen(['hdiutil', 'create',
337 '-srcfolder', self.tmp,
338 '-format', 'UDBZ',
339 '-volname', self.volname,
340 self.filename])
341
342 retval = p.wait()
343 print ' * Removing temporary directory.'
344 shutil.rmtree(self.tmp)
345 print ' * Done!'
346
347
348if __name__ == '__main__':
349 parser = OptionParser()
350 parser.add_option('', '--release', dest='release', help='Build a release. This determines the version number of the release.')
351 parser.add_option('', '--snapshot', dest='snapshot', help='Build a snapshot release. This determines the \'snapshot version\'.')
352 parser.add_option('', '--git', dest='git', help='Build a snapshot release. Use the git revision number as the \'snapshot version\'.', action='store_true', default=False)
353 parser.add_option('', '--codesign', dest='codesign', help='Identity to use for code signing. (If not set, no code signing will occur)')
354 parser.add_option('', '--codesign-keychain', dest='codesign_keychain', help='The keychain to use when invoking the codesign utility.')
Zhenkai Zhu9fdf3cd2013-02-28 12:36:06 -0800355 parser.add_option('', '--build-dir', dest='build_dir', help='specify build directory', default='build')
356 parser.add_option('', '--dmg', dest='dmg', help='create dmg image', action='store_true', default=False)
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800357
358 options, args = parser.parse_args()
359
360 # Release
361 if options.release:
362 ver = options.release
363 # Snapshot
364 elif options.snapshot or options.git:
365 if not options.git:
366 ver = options.snapshot
367 else:
368 ver = gitrev()
369 #ver = "0.0.1"
370 else:
371 print 'Neither snapshot or release selected. Bailing.'
372 sys.exit(1)
373
Zhenkai Zhu9fdf3cd2013-02-28 12:36:06 -0800374 if options.build_dir:
375 os.chdir(options.build_dir)
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800376
377 # Do the finishing touches to our Application bundle before release
378 a = AppBundle('ChronoShare.app', ver)
379 a.copy_qt_plugins()
380 a.handle_libs()
Zhenkai Zhud5d311a2013-03-04 15:12:46 -0800381 a.copy_resources(['../chronoshare.icns', '../gui/images', '../gui/html', '../osx/qt.conf', '../osx/dsa_pub.pem'])
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800382 a.update_plist()
Zhenkai Zhud5d311a2013-03-04 15:12:46 -0800383 a.set_min_macosx_version('10.8.0')
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800384 a.done()
385
386 # Sign our binaries, etc.
387 if options.codesign:
388 print ' * Signing binaries with identity `%s\'' % options.codesign
389 binaries = (
390 'ChronoShare.app',
391 )
392
393 codesign(binaries)
394 print ''
395
396 # Create diskimage
Zhenkai Zhu9fdf3cd2013-02-28 12:36:06 -0800397 if options.dmg:
398 title = "ChronoShare-%s" % ver
399 fn = "%s.dmg" % title
400 d = DiskImage(fn, title)
401 d.symlink('/Applications', '/Applications')
402 d.copy('ChronoShare.app', '/ChronoShare.app')
403 d.create()
Zhenkai Zhu54ed4692013-02-24 19:25:04 -0800404