tools/deploy-mac.py (view raw)
1#!/usr/bin/env python
2from __future__ import print_function
3import argparse
4import errno
5import os
6import re
7import shutil
8import subprocess
9
10qtPath = None
11
12def splitPath(path):
13 folders = []
14 while True:
15 path, folder = os.path.split(path)
16 if folder != '':
17 folders.append(folder)
18 else:
19 if path != '':
20 folders.append(path)
21 break
22 folders.reverse()
23 return folders
24
25def joinPath(path):
26 return reduce(os.path.join, path, '')
27
28def findFramework(path):
29 child = []
30 while path and not path[-1].endswith('.framework'):
31 child.append(path.pop())
32 child.reverse()
33 return path, child
34
35def findQtPath(path):
36 parent, child = findFramework(splitPath(path))
37 return joinPath(parent[:-2])
38
39def makedirs(path):
40 split = splitPath(path)
41 accum = []
42 split.reverse()
43 while split:
44 accum.append(split.pop())
45 newPath = joinPath(accum)
46 try:
47 os.mkdir(newPath)
48 except OSError as e:
49 if e.errno != errno.EEXIST:
50 raise
51
52
53def parseOtoolLine(line, execPath, root):
54 if not line.startswith('\t'):
55 return None, None, None, None
56 line = line[1:]
57 match = re.match('([@/].*) \(compatibility version.*\)', line)
58 path = match.group(1)
59 split = splitPath(path)
60 newExecPath = ['@executable_path', '..', 'Frameworks']
61 newPath = execPath[:-1]
62 newPath.append('Frameworks')
63 if split[:3] == ['/', 'usr', 'lib'] or split[:2] == ['/', 'System']:
64 return None, None, None, None
65 if split[0] == '@executable_path':
66 split[:1] = execPath
67 if split[0] == '/' and not os.access(joinPath(split), os.F_OK):
68 split[:1] = root
69 isFramework = False
70 if not split[-1].endswith('.dylib'):
71 isFramework = True
72 split, framework = findFramework(split)
73 newPath.append(split[-1])
74 newExecPath.append(split[-1])
75 if isFramework:
76 newPath.extend(framework)
77 newExecPath.extend(framework)
78 split.extend(framework)
79 newPath = joinPath(newPath)
80 newExecPath = joinPath(newExecPath)
81 return joinPath(split), newPath, path, newExecPath
82
83def updateMachO(bin, execPath, root):
84 otoolOutput = subprocess.check_output([otool, '-L', bin])
85 toUpdate = []
86 for line in otoolOutput.split('\n'):
87 oldPath, newPath, oldExecPath, newExecPath = parseOtoolLine(line, execPath, root)
88 if not newPath:
89 continue
90 if os.access(newPath, os.F_OK):
91 print('Skipping copying {}, already done.'.format(oldPath))
92 elif os.path.abspath(oldPath) != os.path.abspath(newPath):
93 print('Copying {} to {}...'.format(oldPath, newPath))
94 parent, child = os.path.split(newPath)
95 makedirs(parent)
96 shutil.copy2(oldPath, newPath)
97 os.chmod(newPath, 0o644)
98 toUpdate.append((newPath, oldExecPath, newExecPath))
99 if not qtPath and 'Qt' in oldPath:
100 global qtPath
101 qtPath = findQtPath(oldPath)
102 print('Found Qt path at {}.'.format(qtPath))
103 for path, oldExecPath, newExecPath in toUpdate:
104 if path != bin:
105 updateMachO(path, execPath, root)
106 print('Updating Mach-O load from {} to {}...'.format(oldExecPath, newExecPath))
107 subprocess.check_call([installNameTool, '-change', oldExecPath, newExecPath, bin])
108 else:
109 print('Updating Mach-O id from {} to {}...'.format(oldExecPath, newExecPath))
110 subprocess.check_call([installNameTool, '-id', newExecPath, bin])
111
112if __name__ == '__main__':
113 parser = argparse.ArgumentParser()
114 parser.add_argument('-R', '--root', metavar='ROOT', default='/', help='root directory to search')
115 parser.add_argument('-I', '--install-name-tool', metavar='INSTALL_NAME_TOOL', default='install_name_tool', help='path to install_name_tool')
116 parser.add_argument('-O', '--otool', metavar='OTOOL', default='otool', help='path to otool')
117 parser.add_argument('-p', '--qt-plugins', metavar='PLUGINS', default='', help='Qt plugins to include (comma-separated)')
118 parser.add_argument('bundle', help='application bundle to deploy')
119 args = parser.parse_args()
120
121 otool = args.otool
122 installNameTool = args.install_name_tool
123
124 try:
125 shutil.rmtree(os.path.join(args.bundle, 'Contents/Frameworks/'))
126 except OSError as e:
127 if e.errno != errno.ENOENT:
128 raise
129
130 for executable in os.listdir(os.path.join(args.bundle, 'Contents/MacOS')):
131 if executable.endswith('.dSYM'):
132 continue
133 fullPath = os.path.join(args.bundle, 'Contents/MacOS/', executable)
134 updateMachO(fullPath, splitPath(os.path.join(args.bundle, 'Contents/MacOS')), splitPath(args.root))
135 if args.qt_plugins:
136 try:
137 shutil.rmtree(os.path.join(args.bundle, 'Contents/PlugIns/'))
138 except OSError as e:
139 if e.errno != errno.ENOENT:
140 raise
141 makedirs(os.path.join(args.bundle, 'Contents/PlugIns'))
142 makedirs(os.path.join(args.bundle, 'Contents/Resources'))
143 with open(os.path.join(args.bundle, 'Contents/Resources/qt.conf'), 'w') as conf:
144 conf.write('[Paths]\nPlugins = PlugIns\n')
145 plugins = args.qt_plugins.split(',')
146 for plugin in plugins:
147 plugin = plugin.strip()
148 kind, plug = os.path.split(plugin)
149 newDir = os.path.join(args.bundle, 'Contents/PlugIns/', kind)
150 makedirs(newDir)
151 newPath = os.path.join(newDir, plug)
152 shutil.copy2(os.path.join(qtPath, 'plugins', plugin), newPath)
153 updateMachO(newPath, splitPath(os.path.join(args.bundle, 'Contents/MacOS')), splitPath(args.root))