blob: 300f36e5924e9dbf37f4b9ef8249522924baafd0 [file] [log] [blame]
Alexander Afanasyevcf18c802016-03-20 22:48:15 -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 Afanasyev38d74ec2016-11-14 10:22:48 +090013os.environ['PATH'] += ":/usr/local/opt/qt5/bin:/opt/qt5/5.8/clang_64/bin"
14
Alexander Afanasyevcf18c802016-03-20 22:48:15 -070015import platform
16
17if platform.system () != 'Darwin':
18 print "This script is indended to be run only on OSX platform"
19 exit (1)
20
21MIN_SUPPORTED_VERSION="10.10"
22
23current_version = tuple(int(i) for i in platform.mac_ver()[0].split('.')[0:2])
24min_supported_version = tuple(int(i) for i in MIN_SUPPORTED_VERSION.split('.')[0:2])
25
26if current_version < min_supported_version:
27 print "This script is indended to be run only on OSX >= %s platform" % MIN_SUPPORTED_VERSION
28 exit (1)
29
30options = None
31
32def gitrev():
33 return os.popen('git describe').read()[:-1]
34
35def codesign(path):
36 '''Call the codesign executable.'''
37
38 if hasattr(path, 'isalpha'):
39 path = (path,)
40
41 for p in path:
42 p = Popen(('codesign', '-vvvv', '--deep', '--force', '--sign', options.codesign, p))
43 retval = p.wait()
44 if retval != 0:
45 return retval
46 return 0
47
48class AppBundle(object):
49
50 def __init__(self, bundle, version, binary):
51 shutil.copytree (src = binary, dst = bundle, symlinks = True)
52
53 self.framework_path = ''
54 self.handled_libs = {}
55 self.bundle = bundle
56 self.version = version
57 self.infopath = os.path.join(os.path.abspath(bundle), 'Contents', 'Info.plist')
58 self.infoplist = plistlib.readPlist(self.infopath)
59 self.binary = os.path.join(os.path.abspath(bundle), 'Contents', 'MacOS', self.infoplist['CFBundleExecutable'])
60 print ' * Preparing AppBundle'
61
62 def is_system_lib(self, lib):
63 '''
64 Is the library a system library, meaning that we should not include it in our bundle?
65 '''
66 if lib.startswith('/System/Library/'):
67 return True
68 if lib.startswith('/usr/lib/'):
69 return True
70
71 return False
72
73 def is_dylib(self, lib):
74 '''
75 Is the library a dylib?
76 '''
77 return lib.endswith('.dylib')
78
79 def get_framework_base(self, fw):
80 '''
81 Extracts the base .framework bundle path from a library in an abitrary place in a framework.
82 '''
83 paths = fw.split('/')
84 for i, str in enumerate(paths):
85 if str.endswith('.framework'):
86 return '/'.join(paths[:i+1])
87 return None
88
89 def is_framework(self, lib):
90 '''
91 Is the library a framework?
92 '''
93 return bool(self.get_framework_base(lib))
94
95 def get_binary_libs(self, path):
96 '''
97 Get a list of libraries that we depend on.
98 '''
99 m = re.compile('^\t(.*)\ \(.*$')
100 libs = Popen(['otool', '-L', path], stdout=PIPE).communicate()[0]
101 libs = string.split(libs, '\n')
102 ret = []
103 bn = os.path.basename(path)
104 for line in libs:
105 g = m.match(line)
106 if g is not None:
107 lib = g.groups()[0]
108 if lib != bn:
109 ret.append(lib)
110 return ret
111
112 def handle_libs(self):
113 '''
114 Copy non-system libraries that we depend on into our bundle, and fix linker
115 paths so they are relative to our bundle.
116 '''
117 print ' * Taking care of libraries'
118
119 # Does our fwpath exist?
120 fwpath = os.path.join(os.path.abspath(self.bundle), 'Contents', 'Frameworks')
121 if not os.path.exists(fwpath):
122 os.mkdir(fwpath)
123
124 self.handle_binary_libs()
125
126 def handle_binary_libs(self, macho=None, loader_path=None):
127 '''
128 Fix up dylib depends for a specific binary.
129 '''
130 # Does our fwpath exist already? If not, create it.
131 if not self.framework_path:
132 self.framework_path = self.bundle + '/Contents/Frameworks'
133 if not os.path.exists(self.framework_path):
134 os.mkdir(self.framework_path)
135 else:
136 shutil.rmtree(self.framework_path)
137 os.mkdir(self.framework_path)
138
139 # If we weren't explicitly told which binary to operate on, pick the
140 # bundle's default executable from its property list.
141 if macho is None:
142 macho = os.path.abspath(self.binary)
143 else:
144 macho = os.path.abspath(macho)
145
146 print "Processing [%s]" % 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 print "COPY ", fw_path, dst
168 shutil.copytree(fw_path, dst, symlinks=True)
169 if name.startswith('Qt'):
170 os.remove(dst + '/' + name + '.prl')
171 os.remove(dst + '/Headers')
172 shutil.rmtree(dst + '/Versions/Current/Headers')
173
174 os.chmod(abs, 0755)
175 os.system('install_name_tool -id "@executable_path/../Frameworks/%s" "%s"' % (rel, abs))
176 self.handled_libs[basename] = True
177 self.handle_binary_libs(abs)
178
179 os.chmod(macho, 0755)
Alexander Afanasyev2fda1262016-03-20 23:26:23 -0700180 # print 'install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700181 os.system('install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho))
182
183 # Regular dylibs
184 else:
185 basename = os.path.basename(lib)
186 rel = basename
187
188 if not basename in self.handled_libs:
189 if lib.startswith('@loader_path'):
190 copypath = lib.replace('@loader_path', loader_path)
191 else:
192 copypath = lib
193
194 print "COPY ", copypath
195 shutil.copy(copypath, self.framework_path + '/' + basename)
196
197 abs = self.framework_path + '/' + rel
198 os.chmod(abs, 0755)
199 os.system('install_name_tool -id "@executable_path/../Frameworks/%s" "%s"' % (rel, abs))
200 self.handled_libs[basename] = True
201 self.handle_binary_libs(abs, loader_path=os.path.dirname(lib) if loader_path is None else loader_path)
202
Alexander Afanasyev2fda1262016-03-20 23:26:23 -0700203 # print 'install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700204 os.chmod(macho, 0755)
205 os.system('install_name_tool -change "%s" "@executable_path/../Frameworks/%s" "%s"' % (lib, rel, macho))
206
207 def copy_resources(self, rsrcs):
208 '''
209 Copy needed resources into our bundle.
210 '''
211 print ' * Copying needed resources'
212 rsrcpath = os.path.join(self.bundle, 'Contents', 'Resources')
213 if not os.path.exists(rsrcpath):
214 os.mkdir(rsrcpath)
215
216 # Copy resources already in the bundle
217 for rsrc in rsrcs:
218 b = os.path.basename(rsrc)
219 if os.path.isdir(rsrc):
220 shutil.copytree(rsrc, os.path.join(rsrcpath, b), symlinks=True)
221 elif os.path.isfile(rsrc):
222 shutil.copy(rsrc, os.path.join(rsrcpath, b))
223
224 return
225
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700226 def copy_etc(self, rsrcs):
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700227 '''
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700228 Copy needed config files into our bundle.
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700229 '''
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700230 print ' * Copying needed config files'
Alexander Afanasyev964feb92016-03-22 13:03:11 -0700231 rsrcpath = os.path.join(self.bundle, 'Contents', 'etc', 'ndn')
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700232 if not os.path.exists(rsrcpath):
Alexander Afanasyev964feb92016-03-22 13:03:11 -0700233 os.makedirs(rsrcpath)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700234
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700235 # Copy resources already in the bundle
236 for rsrc in rsrcs:
237 b = os.path.basename(rsrc)
238 if os.path.isdir(rsrc):
239 shutil.copytree(rsrc, os.path.join(rsrcpath, b), symlinks=True)
240 elif os.path.isfile(rsrc):
241 shutil.copy(rsrc, os.path.join(rsrcpath, b))
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700242
Alexander Afanasyeve1236c72016-03-21 15:52:09 -0700243 return
244
245 def copy_framework(self, framework):
246 '''
247 Copy frameworks
248 '''
249 print ' * Copying framework'
250 rsrcpath = os.path.join(self.bundle, 'Contents', 'Frameworks', os.path.basename(framework))
251
252 shutil.copytree(framework, rsrcpath, symlinks = True)
253
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700254 # def copy_qt_plugins(self):
255 # '''
256 # Copy over any needed Qt plugins.
257 # '''
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700258
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700259 # print ' * Copying Qt and preparing plugins'
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700260
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700261 # src = os.popen('qmake -query QT_INSTALL_PLUGINS').read().strip()
262 # dst = os.path.join(self.bundle, 'Contents', 'QtPlugins')
263 # shutil.copytree(src, dst, symlinks=False)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700264
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700265 # top = dst
266 # files = {}
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700267
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700268 # def cb(arg, dirname, fnames):
269 # if dirname == top:
270 # return
271 # files[os.path.basename(dirname)] = fnames
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700272
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700273 # os.path.walk(top, cb, None)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700274
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700275 # exclude = ( 'phonon_backend', 'designer', 'script' )
276
277 # for dir, files in files.items():
278 # absdir = dst + '/' + dir
279 # if dir in exclude:
280 # shutil.rmtree(absdir)
281 # continue
282 # for file in files:
283 # abs = absdir + '/' + file
284 # if file.endswith('_debug.dylib'):
285 # os.remove(abs)
286 # else:
287 # os.system('install_name_tool -id "%s" "%s"' % (file, abs))
288 # self.handle_binary_libs(abs)
289
290 def macdeployqt(self):
291 Popen(['macdeployqt', self.bundle, '-qmldir=src', '-executable=%s' % self.binary]).communicate()
292
Alexander Afanasyevb2cf5c02016-03-21 11:04:28 -0700293 def copy_ndn_deps(self, path):
294 '''
295 Copy over NDN dependencies (NFD and related apps)
296 '''
297 print ' * Copying NDN dependencies'
298
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700299 src = os.path.join(path, 'bin')
300 dst = os.path.join(self.bundle, 'Contents', 'Platform')
Alexander Afanasyevb2cf5c02016-03-21 11:04:28 -0700301 shutil.copytree(src, dst, symlinks=False)
302
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700303 for subdir, dirs, files in os.walk(dst):
Alexander Afanasyevb2cf5c02016-03-21 11:04:28 -0700304 for file in files:
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700305 abs = subdir + "/" + file
306 self.handle_binary_libs(abs)
Alexander Afanasyevb2cf5c02016-03-21 11:04:28 -0700307
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700308 # top = dst
309 # files = {}
310
311 # def cb(arg, dirname, fnames):
312 # if dirname == top:
313 # return
314 # files[dirname] = fnames
315
316 # os.path.walk(top, cb, None)
317
318 # # Cleanup debug folders stuff
319 # excludeDirs = ['include', 'pkgconfig', 'lib'] # lib already processed
320 # excludeFiles = ['libndn-cxx.dylib', 'nfd-start', 'nfd-stop']
321
322 # for dir, files in files.items():
323 # basename = os.path.basename(dir)
324 # if basename in excludeDirs:
325 # shutil.rmtree(dir)
326 # continue
327 # for file in files:
328 # if file in excludeFiles:
329 # abs = dir + '/' + file
330 # os.remove(abs)
331
332 # top = dst
333 # files = {}
334
335 # os.path.walk(top, cb, None)
336
337 # for dir, files in files.items():
338 # for file in files:
339 # abs = dir + '/' + file
340 # type = Popen(['file', '-b', abs], stdout=PIPE).communicate()[0].strip()
341 # if type.startswith('Mach-O'):
342 # self.handle_binary_libs(abs)
Alexander Afanasyevb2cf5c02016-03-21 11:04:28 -0700343
344
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700345 def set_min_macosx_version(self, version):
346 '''
347 Set the minimum version of Mac OS X version that this App will run on.
348 '''
349 print ' * Setting minimum Mac OS X version to: %s' % (version)
350 self.infoplist['LSMinimumSystemVersion'] = version
351
352 def done(self):
353 plistlib.writePlist(self.infoplist, self.infopath)
354 print ' * Done!'
355 print ''
356
357class FolderObject(object):
358 class Exception(exceptions.Exception):
359 pass
360
361 def __init__(self):
362 self.tmp = tempfile.mkdtemp()
363
364 def copy(self, src, dst='/'):
365 '''
366 Copy a file or directory into foler
367 '''
368 asrc = os.path.abspath(src)
369
370 if dst[0] != '/':
371 raise self.Exception
372
373 # Determine destination
374 if dst[-1] == '/':
375 adst = os.path.abspath(self.tmp + '/' + dst + os.path.basename(src))
376 else:
377 adst = os.path.abspath(self.tmp + '/' + dst)
378
379 if os.path.isdir(asrc):
380 print ' * Copying directory: %s' % os.path.basename(asrc)
381 shutil.copytree(asrc, adst, symlinks=True)
382 elif os.path.isfile(asrc):
383 print ' * Copying file: %s' % os.path.basename(asrc)
384 shutil.copy(asrc, adst)
385
386 def symlink(self, src, dst):
387 '''
388 Create a symlink inside the folder
389 '''
390 asrc = os.path.abspath(src)
391 adst = self.tmp + '/' + dst
392 print " * Creating symlink %s" % os.path.basename(asrc)
393 os.symlink(asrc, adst)
394
395 def mkdir(self, name):
396 '''
397 Create a directory inside the folder.
398 '''
399 print ' * Creating directory %s' % os.path.basename(name)
400 adst = self.tmp + '/' + name
401 os.makedirs(adst)
402
403class DiskImage(FolderObject):
404
405 def __init__(self, filename, volname):
406 FolderObject.__init__(self)
407 print ' * Preparing to create diskimage'
408 self.filename = filename
409 self.volname = volname
410
411 def create(self):
412 '''
413 Create the disk image
414 '''
415 print ' * Creating disk image. Please wait...'
416 if os.path.exists(self.filename):
Alexander Afanasyev2fda1262016-03-20 23:26:23 -0700417 os.remove(self.filename)
418 shutil.rmtree(self.filename, ignore_errors=True)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700419 p = Popen(['hdiutil', 'create',
420 '-srcfolder', self.tmp,
421 '-format', 'UDBZ',
422 '-volname', self.volname,
423 self.filename])
424
425 retval = p.wait()
426 print ' * Removing temporary directory.'
427 shutil.rmtree(self.tmp)
428 print ' * Done!'
429
430
431if __name__ == '__main__':
432 parser = OptionParser()
433 parser.add_option('-r', '--release', dest='release', help='Build a release. This determines the version number of the release.')
434 parser.add_option('-s', '--snapshot', dest='snapshot', help='Build a snapshot release. This determines the \'snapshot version\'.')
435 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)
436 parser.add_option('--codesign', dest='codesign', help='Identity to use for code signing. (If not set, no code signing will occur)')
437
438 options, args = parser.parse_args()
439
440 # Release
441 if options.release:
442 ver = options.release
443 # Snapshot
444 elif options.snapshot or options.git:
445 if not options.git:
446 ver = options.snapshot
447 else:
448 ver = gitrev()
449 else:
450 print 'ERROR: Neither snapshot or release selected. Bailing.'
451 parser.print_help ()
452 sys.exit(1)
453
454 # Do the finishing touches to our Application bundle before release
455 shutil.rmtree('build/%s/NDN.app' % (MIN_SUPPORTED_VERSION), ignore_errors=True)
456 a = AppBundle('build/%s/NDN.app' % (MIN_SUPPORTED_VERSION), ver, 'build/NFD Control Center.app')
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700457 # a.copy_qt_plugins()
458 # a.handle_libs()
Alexander Afanasyevb2cf5c02016-03-21 11:04:28 -0700459 a.copy_ndn_deps("build/deps")
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700460 # a.copy_resources(['qt.conf'])
461 a.copy_etc(['nfd.conf'])
Alexander Afanasyev11ae34d2016-03-21 11:55:16 -0700462 a.set_min_macosx_version('%s.0' % MIN_SUPPORTED_VERSION)
Alexander Afanasyev8e986f82016-03-21 14:19:15 -0700463 a.macdeployqt()
Alexander Afanasyeve1236c72016-03-21 15:52:09 -0700464 a.copy_framework("build/Sparkle.framework")
Alexander Afanasyev11ae34d2016-03-21 11:55:16 -0700465 a.done()
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700466
Alexander Afanasyev11ae34d2016-03-21 11:55:16 -0700467 # Sign our binaries, etc.
468 if options.codesign:
469 print ' * Signing binaries with identity `%s\'' % options.codesign
Alexander Afanasyeve1236c72016-03-21 15:52:09 -0700470 binaries = (a.bundle)
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700471
Alexander Afanasyev11ae34d2016-03-21 11:55:16 -0700472 codesign(binaries)
473 print ''
Alexander Afanasyevcf18c802016-03-20 22:48:15 -0700474
Alexander Afanasyev11ae34d2016-03-21 11:55:16 -0700475 # Create diskimage
476 title = "NDN-%s-%s" % (ver, MIN_SUPPORTED_VERSION)
477 fn = "build/%s.dmg" % title
478 d = DiskImage(fn, title)
479 d.symlink('/Applications', '/Applications')
480 d.copy('build/%s/NDN.app' % MIN_SUPPORTED_VERSION, '/NDN.app')
481 d.create()