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