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