relocate.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import glob
  2. import os
  3. import re
  4. import shutil
  5. import sys
  6. from .appify import Appifier
  7. from ..manylinux import PythonVersion
  8. from ..utils.deps import EXCLUDELIST, PATCHELF, ensure_excludelist, \
  9. ensure_patchelf
  10. from ..utils.fs import copy_file, copy_tree, make_tree, remove_file, \
  11. remove_tree
  12. from ..utils.log import log
  13. from ..utils.system import ldd, system
  14. __all__ = ['patch_binary', 'relocate_python']
  15. _excluded_libs = None
  16. '''Appimage excluded libraries, i.e. assumed to be installed on the host
  17. '''
  18. def patch_binary(path, libdir, recursive=True):
  19. '''Patch the RPATH of a binary and fetch its dependencies
  20. '''
  21. global _excluded_libs
  22. if _excluded_libs is None:
  23. ensure_excludelist()
  24. excluded = []
  25. with open(EXCLUDELIST) as f:
  26. for line in f:
  27. line = line.strip()
  28. if (not line) or line.startswith('#'):
  29. continue
  30. excluded.append(line.split(' ', 1)[0])
  31. _excluded_libs = excluded
  32. else:
  33. excluded = _excluded_libs
  34. deps = ldd(path) # Fetch deps before patching RPATH.
  35. ensure_patchelf()
  36. rpath = '\'' + system((PATCHELF, '--print-rpath', path)) + '\''
  37. relpath = os.path.relpath(libdir, os.path.dirname(path))
  38. relpath = '' if relpath == '.' else '/' + relpath
  39. expected = '\'$ORIGIN' + relpath + ':$ORIGIN/../lib\''
  40. if rpath != expected:
  41. system((PATCHELF, '--set-rpath', expected, path))
  42. for dep in deps:
  43. name = os.path.basename(dep)
  44. if name in excluded:
  45. continue
  46. target = libdir + '/' + name
  47. if not os.path.exists(target):
  48. copy_file(dep, target)
  49. if recursive:
  50. patch_binary(target, libdir, recursive=True)
  51. def relocate_python(python=None, appdir=None):
  52. '''Bundle a Python install inside an AppDir
  53. '''
  54. if python is not None:
  55. if not os.path.exists(python):
  56. raise ValueError('could not access ' + python)
  57. if appdir is None:
  58. appdir = 'AppDir'
  59. # Set some key variables & paths
  60. if python:
  61. FULLVERSION = system((python, '-c', '"import sys; print(sys.version)"'))
  62. FULLVERSION = FULLVERSION.strip()
  63. else:
  64. FULLVERSION = sys.version
  65. FULLVERSION = FULLVERSION.split(None, 1)[0]
  66. VERSION = '.'.join(FULLVERSION.split('.')[:2])
  67. PYTHON_X_Y = 'python' + VERSION
  68. PIP_X_Y = 'pip' + VERSION
  69. PIP_X = 'pip' + VERSION[0]
  70. APPDIR = os.path.abspath(appdir)
  71. APPDIR_BIN = APPDIR + '/usr/bin'
  72. APPDIR_LIB = APPDIR + '/usr/lib'
  73. APPDIR_SHARE = APPDIR + '/usr/share'
  74. if python:
  75. HOST_PREFIX = system((
  76. python, '-c', '"import sys; print(sys.prefix)"')).strip()
  77. else:
  78. HOST_PREFIX = sys.prefix
  79. HOST_BIN = HOST_PREFIX + '/bin'
  80. HOST_INC = HOST_PREFIX + '/include/' + PYTHON_X_Y
  81. HOST_LIB = HOST_PREFIX + '/lib'
  82. HOST_PKG = HOST_LIB + '/' + PYTHON_X_Y
  83. PYTHON_PREFIX = APPDIR + '/opt/' + PYTHON_X_Y
  84. PYTHON_BIN = PYTHON_PREFIX + '/bin'
  85. PYTHON_INC = PYTHON_PREFIX + '/include/' + PYTHON_X_Y
  86. PYTHON_LIB = PYTHON_PREFIX + '/lib'
  87. PYTHON_PKG = PYTHON_LIB + '/' + PYTHON_X_Y
  88. if not os.path.exists(HOST_PKG):
  89. paths = glob.glob(HOST_PKG + '*')
  90. if paths:
  91. HOST_PKG = paths[0]
  92. PYTHON_PKG = PYTHON_LIB + '/' + os.path.basename(HOST_PKG)
  93. else:
  94. raise ValueError('could not find {0:}'.format(HOST_PKG))
  95. if not os.path.exists(HOST_INC):
  96. paths = glob.glob(HOST_INC + '*')
  97. if paths:
  98. HOST_INC = paths[0]
  99. PYTHON_INC = PYTHON_INC + '/' + os.path.basename(HOST_INC)
  100. else:
  101. raise ValueError('could not find {0:}'.format(HOST_INC))
  102. # Copy the running Python's install
  103. log('CLONE', '%s from %s', PYTHON_X_Y, HOST_PREFIX)
  104. source = HOST_BIN + '/' + PYTHON_X_Y
  105. if not os.path.exists(source):
  106. raise ValueError('could not find {0:} executable'.format(PYTHON_X_Y))
  107. make_tree(PYTHON_BIN)
  108. target = PYTHON_BIN + '/' + PYTHON_X_Y
  109. copy_file(source, target, update=True)
  110. copy_tree(HOST_PKG, PYTHON_PKG)
  111. copy_tree(HOST_INC, PYTHON_INC)
  112. make_tree(APPDIR_BIN)
  113. pip_source = HOST_BIN + '/' + PIP_X_Y
  114. if not os.path.exists(pip_source):
  115. pip_source = HOST_BIN + '/' + PIP_X
  116. if os.path.exists(pip_source):
  117. with open(pip_source) as f:
  118. f.readline()
  119. body = f.read()
  120. target = PYTHON_BIN + '/' + PIP_X_Y
  121. with open(target, 'w') as f:
  122. f.write('#! /bin/sh\n')
  123. f.write(' '.join((
  124. '"exec"',
  125. '"$(dirname $(readlink -f ${0}))/../../../usr/bin/' +
  126. PYTHON_X_Y + '"',
  127. '"$0"',
  128. '"$@"\n'
  129. )))
  130. f.write(body)
  131. shutil.copymode(pip_source, target)
  132. # Remove unrelevant files
  133. log('PRUNE', '%s packages', PYTHON_X_Y)
  134. remove_file(PYTHON_LIB + '/lib' + PYTHON_X_Y + '.a')
  135. remove_tree(PYTHON_PKG + '/test')
  136. remove_file(PYTHON_PKG + '/dist-packages')
  137. matches = glob.glob(PYTHON_PKG + '/config-*-linux-*')
  138. for path in matches:
  139. remove_tree(path)
  140. # Set RPATHs and bundle external libraries
  141. log('LINK', '%s C-extensions', PYTHON_X_Y)
  142. make_tree(APPDIR_LIB)
  143. patch_binary(PYTHON_BIN + '/' + PYTHON_X_Y, APPDIR_LIB, recursive=False)
  144. for root, dirs, files in os.walk(PYTHON_PKG + '/lib-dynload'):
  145. for file_ in files:
  146. if not file_.endswith('.so'):
  147. continue
  148. patch_binary(os.path.join(root, file_), APPDIR_LIB, recursive=False)
  149. for file_ in glob.iglob(APPDIR_LIB + '/lib*.so*'):
  150. patch_binary(file_, APPDIR_LIB, recursive=True)
  151. # Copy shared data for TCl/Tk
  152. tk_version = _get_tk_version(PYTHON_PKG)
  153. if tk_version is not None:
  154. tcltkdir = APPDIR_SHARE + '/tcltk'
  155. if (not os.path.exists(tcltkdir + '/tcl' + tk_version)) or \
  156. (not os.path.exists(tcltkdir + '/tk' + tk_version)):
  157. libdir = _get_tk_libdir(tk_version)
  158. log('INSTALL', 'Tcl/Tk' + tk_version)
  159. make_tree(tcltkdir)
  160. tclpath = libdir + '/tcl' + tk_version
  161. copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
  162. tkpath = libdir + '/tk' + tk_version
  163. copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
  164. # Copy any SSL certificate
  165. cert_file = os.getenv('SSL_CERT_FILE')
  166. if cert_file:
  167. # Package certificates as well for SSL
  168. # (see https://github.com/niess/python-appimage/issues/24)
  169. dirname, basename = os.path.split(cert_file)
  170. make_tree('AppDir' + dirname)
  171. copy_file(cert_file, 'AppDir' + cert_file)
  172. log('INSTALL', basename)
  173. # Bundle AppImage specific files.
  174. appifier = Appifier(
  175. appdir = APPDIR,
  176. appdir_bin = APPDIR_BIN,
  177. python_bin = PYTHON_BIN,
  178. python_pkg = PYTHON_PKG,
  179. tk_version = tk_version,
  180. version = PythonVersion.from_str(FULLVERSION)
  181. )
  182. appifier.appify()
  183. def _get_tk_version(python_pkg):
  184. tkinter = glob.glob(python_pkg + '/lib-dynload/_tkinter*.so')
  185. if tkinter:
  186. tkinter = tkinter[0]
  187. for dep in ldd(tkinter):
  188. name = os.path.basename(dep)
  189. if name.startswith('libtk'):
  190. match = re.search('libtk([0-9]+[.][0-9]+)', name)
  191. return match.group(1)
  192. else:
  193. raise RuntimeError('could not guess Tcl/Tk version')
  194. def _get_tk_libdir(version):
  195. try:
  196. library = system(('tclsh' + version,), stdin='puts [info library]')
  197. except SystemError:
  198. raise RuntimeError('could not locate Tcl/Tk' + version + ' library')
  199. return os.path.dirname(library)