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