macdeployqt.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. #!/usr/bin/python
  2. # This file is part of ownCloud.
  3. # It was inspired in large part by the macdeploy script in Clementine
  4. # and Tomahawk
  5. #
  6. # ownCloud is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # ownCLoud is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with ownCloud. If not, see <http://www.gnu.org/licenses/>.
  18. import os
  19. import re
  20. import subprocess
  21. import commands
  22. import sys
  23. from glob import glob
  24. def QueryQMake(attrib):
  25. return subprocess.check_output([qmake_path, '-query', attrib]).rstrip('\n')
  26. FRAMEWORK_SEARCH_PATH=[
  27. '/Library/Frameworks',
  28. os.path.join(os.environ['HOME'], 'Library/Frameworks')
  29. ]
  30. LIBRARY_SEARCH_PATH=['/usr/local/lib', '.']
  31. QT_PLUGINS = [
  32. 'sqldrivers/libqsqlite.dylib',
  33. 'platforms/libqcocoa.dylib',
  34. 'imageformats/libqgif.dylib',
  35. 'imageformats/libqico.dylib',
  36. 'imageformats/libqjpeg.dylib',
  37. 'imageformats/libqsvg.dylib',
  38. ]
  39. QT_PLUGINS_SEARCH_PATH=[
  40. # os.path.join(os.environ['QTDIR'], 'plugins'),
  41. '/usr/local/Cellar/qt/5.2.1/plugins',
  42. ]
  43. class Error(Exception):
  44. pass
  45. class CouldNotFindQtPluginErrorFindFrameworkError(Error):
  46. pass
  47. class InstallNameToolError(Error):
  48. pass
  49. class CouldNotFindQtPluginError(Error):
  50. pass
  51. class CouldNotFindScriptPluginError(Error):
  52. pass
  53. class CouldNotFindFrameworkError(Error):
  54. pass
  55. if len(sys.argv) < 3:
  56. print 'Usage: %s <bundle.app> <path-to-qmake>' % sys.argv[0]
  57. exit()
  58. def is_exe(fpath):
  59. return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
  60. bundle_dir = sys.argv[1]
  61. qmake_path = sys.argv[2]
  62. bundle_name = os.path.basename(bundle_dir).split('.')[0]
  63. commands = []
  64. binary_dir = os.path.join(bundle_dir, 'Contents', 'MacOS')
  65. frameworks_dir = os.path.join(bundle_dir, 'Contents', 'Frameworks')
  66. commands.append(['mkdir', '-p', frameworks_dir])
  67. resources_dir = os.path.join(bundle_dir, 'Contents', 'Resources')
  68. commands.append(['mkdir', '-p', resources_dir])
  69. plugins_dir = os.path.join(bundle_dir, 'Contents', 'PlugIns')
  70. binaries = [i for i in glob(os.path.join(bundle_dir, 'Contents', 'MacOS', "*")) if is_exe(i)];
  71. fixed_libraries = []
  72. fixed_frameworks = []
  73. def WriteQtConf():
  74. print "Writing qt.conf..."
  75. with open(os.path.join(resources_dir, 'qt.conf'), 'w') as f:
  76. f.write("[Paths]\nPlugins = PlugIns\n");
  77. f.close()
  78. def GetBrokenLibraries(binary):
  79. #print "Checking libs for binary: %s" % binary
  80. output = subprocess.Popen(['otool', '-L', binary], stdout=subprocess.PIPE).communicate()[0]
  81. broken_libs = {
  82. 'frameworks': [],
  83. 'libs': []}
  84. for line in [x.split(' ')[0].lstrip() for x in output.split('\n')[1:]]:
  85. #print "Checking line: %s" % line
  86. if not line: # skip empty lines
  87. continue
  88. if os.path.basename(binary) == os.path.basename(line):
  89. #print "mnope %s-%s" % (os.path.basename(binary), os.path.basename(line))
  90. continue
  91. if re.match(r'^\s*/System/', line):
  92. continue # System framework
  93. elif re.match(r'^\s*/usr/lib/', line):
  94. #print "unix style system lib"
  95. continue # unix style system library
  96. elif re.match(r'Breakpad', line):
  97. continue # Manually added by cmake.
  98. elif re.match(r'^\s*@executable_path', line) or re.match(r'^\s*@loader_path', line):
  99. # Potentially already fixed library
  100. if '.framework' in line:
  101. relative_path = os.path.join(*line.split('/')[3:])
  102. if not os.path.exists(os.path.join(frameworks_dir, relative_path)):
  103. broken_libs['frameworks'].append(relative_path)
  104. else:
  105. relative_path = os.path.join(*line.split('/')[1:])
  106. #print "RELPATH %s %s" % (relative_path, os.path.join(binary_dir, relative_path))
  107. if not os.path.exists(os.path.join(binary_dir, relative_path)):
  108. broken_libs['libs'].append(relative_path)
  109. elif re.search(r'\w+\.framework', line):
  110. broken_libs['frameworks'].append(line)
  111. else:
  112. broken_libs['libs'].append(line)
  113. return broken_libs
  114. def FindFramework(path):
  115. search_pathes = FRAMEWORK_SEARCH_PATH
  116. search_pathes.insert(0, QueryQMake('QT_INSTALL_LIBS'))
  117. for search_path in search_pathes:
  118. # The following two lines are needed for a custom built Qt from version 5.5 on, possibly not for the one from the Qt SDK.
  119. # Looks like the upstream macdeployqt also had an issue there https://bugreports.qt.io/browse/QTBUG-47868
  120. if path.find( "\@rpath/"):
  121. path = path.replace("@rpath/", "")
  122. abs_path = os.path.join(search_path, path)
  123. if os.path.exists(abs_path):
  124. return abs_path
  125. raise CouldNotFindFrameworkError(path)
  126. def FindLibrary(path):
  127. if os.path.exists(path):
  128. return path
  129. search_pathes = LIBRARY_SEARCH_PATH
  130. search_pathes.insert(0, QueryQMake('QT_INSTALL_LIBS'))
  131. for search_path in search_pathes:
  132. abs_path = os.path.join(search_path, path)
  133. if os.path.exists(abs_path):
  134. return abs_path
  135. else: # try harder---look for lib name in library folders
  136. newpath = os.path.join(search_path,os.path.basename(path))
  137. if os.path.exists(newpath):
  138. return newpath
  139. return ""
  140. #raise CouldNotFindFrameworkError(path)
  141. def FixAllLibraries(broken_libs):
  142. for framework in broken_libs['frameworks']:
  143. FixFramework(framework)
  144. for lib in broken_libs['libs']:
  145. FixLibrary(lib)
  146. def FixFramework(path):
  147. if path in fixed_libraries:
  148. return
  149. else:
  150. fixed_libraries.append(path)
  151. abs_path = FindFramework(path)
  152. broken_libs = GetBrokenLibraries(abs_path)
  153. FixAllLibraries(broken_libs)
  154. new_path = CopyFramework(abs_path)
  155. id = os.sep.join(new_path.split(os.sep)[3:])
  156. FixFrameworkId(new_path, id)
  157. for framework in broken_libs['frameworks']:
  158. FixFrameworkInstallPath(framework, new_path)
  159. for library in broken_libs['libs']:
  160. FixLibraryInstallPath(library, new_path)
  161. def FixLibrary(path):
  162. if path in fixed_libraries or FindSystemLibrary(os.path.basename(path)) is not None:
  163. return
  164. else:
  165. fixed_libraries.append(path)
  166. abs_path = FindLibrary(path)
  167. if abs_path == "":
  168. print "Could not resolve %s, not fixing!" % path
  169. return
  170. broken_libs = GetBrokenLibraries(abs_path)
  171. FixAllLibraries(broken_libs)
  172. new_path = CopyLibrary(abs_path)
  173. FixLibraryId(new_path)
  174. for framework in broken_libs['frameworks']:
  175. FixFrameworkInstallPath(framework, new_path)
  176. for library in broken_libs['libs']:
  177. FixLibraryInstallPath(library, new_path)
  178. def FixPlugin(abs_path, subdir):
  179. broken_libs = GetBrokenLibraries(abs_path)
  180. FixAllLibraries(broken_libs)
  181. new_path = CopyPlugin(abs_path, subdir)
  182. for framework in broken_libs['frameworks']:
  183. FixFrameworkInstallPath(framework, new_path)
  184. for library in broken_libs['libs']:
  185. FixLibraryInstallPath(library, new_path)
  186. def FixBinary(path):
  187. broken_libs = GetBrokenLibraries(path)
  188. FixAllLibraries(broken_libs)
  189. for framework in broken_libs['frameworks']:
  190. FixFrameworkInstallPath(framework, path)
  191. for library in broken_libs['libs']:
  192. FixLibraryInstallPath(library, path)
  193. def CopyLibrary(path):
  194. new_path = os.path.join(binary_dir, os.path.basename(path))
  195. args = ['ditto', '--arch=x86_64', path, new_path]
  196. commands.append(args)
  197. args = ['chmod', 'u+w', new_path]
  198. commands.append(args)
  199. return new_path
  200. def CopyPlugin(path, subdir):
  201. new_path = os.path.join(plugins_dir, subdir, os.path.basename(path))
  202. args = ['mkdir', '-p', os.path.dirname(new_path)]
  203. commands.append(args)
  204. args = ['ditto', '--arch=x86_64', path, new_path]
  205. commands.append(args)
  206. args = ['chmod', 'u+w', new_path]
  207. commands.append(args)
  208. return new_path
  209. def CopyFramework(source_dylib):
  210. parts = source_dylib.split(os.sep)
  211. print "CopyFramework:", source_dylib
  212. for i, part in enumerate(parts):
  213. matchObj = re.match(r'(\w+\.framework)', part)
  214. if matchObj:
  215. framework = matchObj.group(1)
  216. dylib_name = parts[-1]
  217. source_path = os.path.join('/', *parts[:i+1])
  218. dest_path = os.path.join(frameworks_dir, framework)
  219. dest_dylib_path = os.path.join(frameworks_dir, *parts[i:-1])
  220. break
  221. if os.path.exists(dest_path):
  222. print dest_path, "already exists, skipping copy..."
  223. return os.path.join(dest_dylib_path, dylib_name)
  224. args = ['mkdir', '-p', dest_dylib_path]
  225. commands.append(args)
  226. args = ['ditto', '--arch=x86_64', source_dylib, dest_dylib_path]
  227. commands.append(args)
  228. args = ['chmod', 'u+w', os.path.join(dest_dylib_path, parts[-1])]
  229. commands.append(args)
  230. args = ['ln', '-s', '5', os.path.join(dest_path, 'Versions', 'Current')]
  231. commands.append(args)
  232. args = ['ln', '-s', os.path.join('Versions', 'Current', dylib_name), os.path.join(dest_path, dylib_name)]
  233. commands.append(args)
  234. args = ['ln', '-s', os.path.join('Versions', 'Current', 'Resources'), os.path.join(dest_path, 'Resources')]
  235. commands.append(args)
  236. args = ['cp', '-r', os.path.join(source_path, 'Versions', '5', 'Resources'), os.path.join(dest_path, 'Versions', '5')]
  237. commands.append(args)
  238. return os.path.join(dest_dylib_path, dylib_name)
  239. def FixId(path, library_name):
  240. id = '@executable_path/../Frameworks/%s' % library_name
  241. args = ['install_name_tool', '-id', id, path]
  242. commands.append(args)
  243. def FixLibraryId(path):
  244. library_name = os.path.basename(path)
  245. FixId(path, library_name)
  246. def FixFrameworkId(path, id):
  247. FixId(path, id)
  248. def FixInstallPath(library_path, library, new_path):
  249. args = ['install_name_tool', '-change', library_path, new_path, library]
  250. commands.append(args)
  251. def FindSystemLibrary(library_name):
  252. for path in ['/lib', '/usr/lib']:
  253. full_path = os.path.join(path, library_name)
  254. if os.path.exists(full_path):
  255. return full_path
  256. return None
  257. def FixLibraryInstallPath(library_path, library):
  258. system_library = FindSystemLibrary(os.path.basename(library_path))
  259. if system_library is None:
  260. new_path = '@executable_path/../MacOS/%s' % os.path.basename(library_path)
  261. FixInstallPath(library_path, library, new_path)
  262. else:
  263. FixInstallPath(library_path, library, system_library)
  264. def FixFrameworkInstallPath(library_path, library):
  265. parts = library_path.split(os.sep)
  266. for i, part in enumerate(parts):
  267. if re.match(r'\w+\.framework', part):
  268. full_path = os.path.join(*parts[i:])
  269. break
  270. new_path = '@executable_path/../Frameworks/%s' % full_path
  271. FixInstallPath(library_path, library, new_path)
  272. def FindQtPlugin(name):
  273. search_path = QT_PLUGINS_SEARCH_PATH
  274. search_path.insert(0, QueryQMake('QT_INSTALL_PLUGINS'))
  275. for path in search_path:
  276. if os.path.exists(path):
  277. if os.path.exists(os.path.join(path, name)):
  278. return os.path.join(path, name)
  279. raise CouldNotFindQtPluginError(name)
  280. for binary in binaries:
  281. FixBinary(binary)
  282. for plugin in QT_PLUGINS:
  283. FixPlugin(FindQtPlugin(plugin), os.path.dirname(plugin))
  284. if len(sys.argv) <= 2:
  285. print 'Will run %d commands:' % len(commands)
  286. for command in commands:
  287. print ' '.join(command)
  288. for command in commands:
  289. p = subprocess.Popen(command)
  290. os.waitpid(p.pid, 0)
  291. WriteQtConf()