1
0

appify.py 9.4 KB

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