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