deploy-mac.py 5.3 KB

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