1
0

appify.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. from dataclasses import dataclass
  2. import glob
  3. import os
  4. import re
  5. from typing import Optional, Tuple
  6. from ..utils.deps import PREFIX
  7. from ..utils.fs import copy_file, copy_tree, make_tree, remove_file, remove_tree
  8. from ..utils.log import debug, log
  9. from ..utils.template import copy_template, load_template
  10. @dataclass(frozen=True)
  11. class Appifier:
  12. '''Path to AppDir root.'''
  13. appdir: str
  14. '''Path to AppDir executables.'''
  15. appdir_bin: str
  16. '''Path to Python executables.'''
  17. python_bin: str
  18. '''Path to Python site-packages.'''
  19. python_pkg: str
  20. '''Tcl/Tk version.'''
  21. tk_version: str
  22. '''Python version.'''
  23. version: 'PythonVersion'
  24. '''Path to SSL certification file.'''
  25. cert_src: Optional[str]=None
  26. def appify(self):
  27. python_x_y = f'python{self.version.short()}'
  28. pip_x_y = f'pip{self.version.short()}'
  29. # Add a runtime patch for sys.executable, before site.main() execution
  30. log('PATCH', f'{python_x_y} sys.executable')
  31. set_executable_patch(
  32. self.version.short(),
  33. self.python_pkg,
  34. PREFIX + '/data/_initappimage.py'
  35. )
  36. # Set a hook for cleaning sys.path, after site.main() execution
  37. log('HOOK', f'{python_x_y} sys.path')
  38. sitepkgs = self.python_pkg + '/site-packages'
  39. make_tree(sitepkgs)
  40. copy_file(PREFIX + '/data/sitecustomize.py', sitepkgs)
  41. # Symlink SSL certificates
  42. # (see https://github.com/niess/python-appimage/issues/24)
  43. cert_file = '/opt/_internal/certs.pem'
  44. cert_dst = f'{self.appdir}{cert_file}'
  45. if self.cert_src is not None:
  46. if os.path.exists(self.cert_src):
  47. if not os.path.exists(cert_dst):
  48. dirname, basename = os.path.split(cert_dst)
  49. relpath = os.path.relpath(self.cert_src, dirname)
  50. make_tree(dirname)
  51. os.symlink(relpath, cert_dst)
  52. log('INSTALL', basename)
  53. if not os.path.exists(cert_dst):
  54. cert_file = None
  55. # Bundle the python wrapper
  56. wrapper = f'{self.appdir_bin}/{python_x_y}'
  57. if not os.path.exists(wrapper):
  58. log('INSTALL', f'{python_x_y} wrapper')
  59. entrypoint_path = PREFIX + '/data/entrypoint.sh'
  60. entrypoint = load_template(
  61. entrypoint_path,
  62. python=f'python{self.version.flavoured()}'
  63. )
  64. dictionary = {
  65. 'entrypoint': entrypoint,
  66. 'shebang': '#! /bin/bash',
  67. 'tcltk-env': tcltk_env_string(self.python_pkg, self.tk_version)
  68. }
  69. if cert_file:
  70. dictionary['cert-file'] = cert_file_env_string(cert_file)
  71. else:
  72. dictionary['cert-file'] = ''
  73. _copy_template('python-wrapper.sh', wrapper, **dictionary)
  74. # Set or update symlinks to python and pip.
  75. pip_target = f'{self.python_bin}/{pip_x_y}'
  76. if os.path.exists(pip_target):
  77. relpath = os.path.relpath(pip_target, self.appdir_bin)
  78. os.symlink(relpath, f'{self.appdir_bin}/{pip_x_y}')
  79. pythons = glob.glob(self.appdir_bin + '/python?.*')
  80. versions = [os.path.basename(python)[6:] for python in pythons]
  81. latest2, latest3 = '0.0', '0.0'
  82. for version in versions:
  83. if version.startswith('2') and version >= latest2:
  84. latest2 = version
  85. elif version.startswith('3') and version >= latest3:
  86. latest3 = version
  87. if latest2 == self.version.short():
  88. python2 = self.appdir_bin + '/python2'
  89. remove_file(python2)
  90. os.symlink(python_x_y, python2)
  91. has_pip = os.path.exists(self.appdir_bin + '/' + pip_x_y)
  92. if has_pip:
  93. pip2 = self.appdir_bin + '/pip2'
  94. remove_file(pip2)
  95. os.symlink(pip_x_y, pip2)
  96. if latest3 == '0.0':
  97. log('SYMLINK', 'python, python2 to ' + python_x_y)
  98. python = self.appdir_bin + '/python'
  99. remove_file(python)
  100. os.symlink('python2', python)
  101. if has_pip:
  102. log('SYMLINK', 'pip, pip2 to ' + pip_x_y)
  103. pip = self.appdir_bin + '/pip'
  104. remove_file(pip)
  105. os.symlink('pip2', pip)
  106. else:
  107. log('SYMLINK', 'python2 to ' + python_x_y)
  108. if has_pip:
  109. log('SYMLINK', 'pip2 to ' + pip_x_y)
  110. elif latest3 == self.version.short():
  111. log('SYMLINK', 'python, python3 to ' + python_x_y)
  112. python3 = self.appdir_bin + '/python3'
  113. remove_file(python3)
  114. os.symlink(python_x_y, python3)
  115. python = self.appdir_bin + '/python'
  116. remove_file(python)
  117. os.symlink('python3', python)
  118. if os.path.exists(self.appdir_bin + '/' + pip_x_y):
  119. log('SYMLINK', 'pip, pip3 to ' + pip_x_y)
  120. pip3 = self.appdir_bin + '/pip3'
  121. remove_file(pip3)
  122. os.symlink(pip_x_y, pip3)
  123. pip = self.appdir_bin + '/pip'
  124. remove_file(pip)
  125. os.symlink('pip3', pip)
  126. # Bundle the entry point
  127. apprun = f'{self.appdir}/AppRun'
  128. if not os.path.exists(apprun):
  129. log('INSTALL', 'AppRun')
  130. relpath = os.path.relpath(wrapper, self.appdir)
  131. os.symlink(relpath, apprun)
  132. # Bundle the desktop file
  133. desktop_name = f'python{self.version.long()}.desktop'
  134. desktop = os.path.join(self.appdir, desktop_name)
  135. if not os.path.exists(desktop):
  136. log('INSTALL', desktop_name)
  137. apps = 'usr/share/applications'
  138. appfile = f'{self.appdir}/{apps}/{desktop_name}'
  139. if not os.path.exists(appfile):
  140. make_tree(os.path.join(self.appdir, apps))
  141. _copy_template('python.desktop', appfile,
  142. version=self.version.short(),
  143. fullversion=self.version.long())
  144. os.symlink(os.path.join(apps, desktop_name), desktop)
  145. # Bundle icons
  146. icons = 'usr/share/icons/hicolor/256x256/apps'
  147. icon = os.path.join(self.appdir, 'python.png')
  148. if not os.path.exists(icon):
  149. log('INSTALL', 'python.png')
  150. make_tree(os.path.join(self.appdir, icons))
  151. copy_file(PREFIX + '/data/python.png',
  152. os.path.join(self.appdir, icons, 'python.png'))
  153. os.symlink(os.path.join(icons, 'python.png'), icon)
  154. diricon = os.path.join(self.appdir, '.DirIcon')
  155. if not os.path.exists(diricon):
  156. os.symlink('python.png', diricon)
  157. # Bundle metadata
  158. meta_name = f'python{self.version.long()}.appdata.xml'
  159. meta_dir = os.path.join(self.appdir, 'usr/share/metainfo')
  160. meta_file = os.path.join(meta_dir, meta_name)
  161. if not os.path.exists(meta_file):
  162. log('INSTALL', meta_name)
  163. make_tree(meta_dir)
  164. _copy_template(
  165. 'python.appdata.xml',
  166. meta_file,
  167. version = self.version.short(),
  168. fullversion = self.version.long()
  169. )
  170. def cert_file_env_string(cert_file):
  171. '''Environment for using a bundled certificate
  172. '''
  173. if cert_file:
  174. return '''
  175. # Export SSL certificate
  176. export SSL_CERT_FILE="${{APPDIR}}{cert_file:}"'''.format(
  177. cert_file=cert_file)
  178. else:
  179. return ''
  180. def _copy_template(name, destination, **kwargs):
  181. path = os.path.join(PREFIX, 'data', name)
  182. copy_template(path, destination, **kwargs)
  183. def tcltk_env_string(python_pkg, tk_version):
  184. '''Environment for using AppImage's TCl/Tk
  185. '''
  186. if tk_version:
  187. return '''
  188. # Export TCl/Tk
  189. export TCL_LIBRARY="${{APPDIR}}/usr/share/tcltk/tcl{tk_version:}"
  190. export TK_LIBRARY="${{APPDIR}}/usr/share/tcltk/tk{tk_version:}"
  191. export TKPATH="${{TK_LIBRARY}}"'''.format(
  192. tk_version=tk_version)
  193. else:
  194. return ''
  195. def set_executable_patch(version, pkgpath, patch):
  196. '''Set a runtime patch for sys.executable name
  197. '''
  198. # This patch needs to be executed before site.main() is called. A natural
  199. # option is to apply it directy to the site module. But, starting with
  200. # Python 3.11, the site module is frozen within Python executable. Then,
  201. # doing so would require to recompile Python. Thus, starting with 3.11 we
  202. # instead apply the patch to the encodings package. Indeed, the latter is
  203. # loaded before the site module, and it is not frozen (as for now).
  204. major, minor = [int(v) for v in version.split('.')]
  205. if (major >= 3) and (minor >= 11):
  206. path = os.path.join(pkgpath, 'encodings', '__init__.py')
  207. else:
  208. path = os.path.join(pkgpath, 'site.py')
  209. with open(path) as f:
  210. source = f.read()
  211. if '_initappimage' in source: return
  212. lines = source.split(os.linesep)
  213. if path.endswith('site.py'):
  214. # Insert the patch before the main function
  215. for i, line in enumerate(lines):
  216. if line.startswith('def main('): break
  217. else:
  218. # Append the patch at end of file
  219. i = len(lines)
  220. with open(patch) as f:
  221. patch = f.read()
  222. lines.insert(i, patch)
  223. lines.insert(i + 1, '')
  224. source = os.linesep.join(lines)
  225. with open(path, 'w') as f:
  226. f.write(source)