app.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import json
  2. import glob
  3. import os
  4. import platform
  5. import re
  6. import shutil
  7. import stat
  8. import struct
  9. from ...appimage import build_appimage
  10. from ...utils.compat import decode
  11. from ...utils.deps import PREFIX
  12. from ...utils.fs import copy_file, make_tree, remove_file, remove_tree
  13. from ...utils.log import log
  14. from ...utils.system import system
  15. from ...utils.template import copy_template, load_template
  16. from ...utils.tmp import TemporaryDirectory
  17. from ...utils.url import urlopen, urlretrieve
  18. __all__ = ['execute']
  19. def _unpack_args(args):
  20. '''Unpack command line arguments
  21. '''
  22. return args.appdir, args.name, args.python_version, args.linux_tag, \
  23. args.python_tag
  24. _tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage')
  25. def execute(appdir, name=None, python_version=None, linux_tag=None,
  26. python_tag=None):
  27. '''Build a Python application using a base AppImage
  28. '''
  29. # Download releases meta data
  30. content = urlopen(
  31. 'https://api.github.com/repos/niess/python-appimage/releases').read()
  32. releases = json.loads(content.decode())
  33. # Fetch the requested Python version or the latest if no specific version
  34. # was requested
  35. release, version = None, '0.0'
  36. for entry in releases:
  37. tag = entry['tag_name']
  38. if not tag.startswith('python'):
  39. continue
  40. v = tag[6:]
  41. if python_version is None:
  42. if v > version:
  43. release, version = entry, v
  44. elif v == python_version:
  45. version = python_version
  46. release = entry
  47. break
  48. if release is None:
  49. raise ValueError('could not find base image for Python ' +
  50. python_version)
  51. elif python_version is None:
  52. python_version = version
  53. # Check for a suitable image
  54. if linux_tag is None:
  55. linux_tag = 'manylinux1_' + platform.machine()
  56. if python_tag is None:
  57. v = ''.join(version.split('.'))
  58. python_tag = 'cp{0:}-cp{0:}'.format(v)
  59. if version < '3.8':
  60. python_tag += 'm'
  61. target_tag = '-'.join((python_tag, linux_tag))
  62. assets = release['assets']
  63. for asset in assets:
  64. match = _tag_pattern.search(asset['name'])
  65. if str(match.group(2)) == target_tag:
  66. python_fullversion = str(match.group(1))
  67. break
  68. else:
  69. raise ValueError('Could not find base image for tag ' + target_tag)
  70. base_image = asset['browser_download_url']
  71. # Set the dictionary for template files
  72. dictionary = {
  73. 'architecture' : platform.machine(),
  74. 'linux-tag' : linux_tag,
  75. 'python-executable' : '${APPDIR}/usr/bin/python' + python_version,
  76. 'python-fullversion' : python_fullversion,
  77. 'python-tag' : python_tag,
  78. 'python-version' : python_version
  79. }
  80. # Get the list of requirements
  81. requirements_list = []
  82. requirements_path = appdir + '/requirements.txt'
  83. if os.path.exists(requirements_path):
  84. with open(requirements_path) as f:
  85. for line in f:
  86. line = line.strip()
  87. if line.startswith('#'):
  88. continue
  89. requirements_list.append(line)
  90. requirements = sorted(requirements_list)
  91. n = len(requirements)
  92. if n == 0:
  93. requirements = ''
  94. elif n == 1:
  95. requirements = requirements[0]
  96. elif n == 2:
  97. requirements = ' and '.join(requirements)
  98. else:
  99. tmp = ', '.join(requirements[:-1])
  100. requirements = tmp + ' and ' + requirements[-1]
  101. dictionary['requirements'] = requirements
  102. # Build the application
  103. appdir = os.path.realpath(appdir)
  104. pwd = os.getcwd()
  105. with TemporaryDirectory() as tmpdir:
  106. application_name = os.path.basename(appdir)
  107. application_icon = application_name
  108. # Extract the base AppImage
  109. log('EXTRACT', '%s', os.path.basename(base_image))
  110. urlretrieve(base_image, 'base.AppImage')
  111. os.chmod('base.AppImage', stat.S_IRWXU)
  112. system(('./base.AppImage', '--appimage-extract'))
  113. system(('mv', 'squashfs-root', 'AppDir'))
  114. # Bundle the desktop file
  115. desktop_path = glob.glob(appdir + '/*.desktop')
  116. if desktop_path:
  117. desktop_path = desktop_path[0]
  118. name = os.path.basename(desktop_path)
  119. log('BUNDLE', name)
  120. python = 'python' + python_fullversion
  121. remove_file('AppDir/{:}.desktop'.format(python))
  122. remove_file('AppDir/usr/share/applications/{:}.desktop'.format(
  123. python))
  124. relpath = 'usr/share/applications/' + name
  125. copy_template(desktop_path, 'AppDir/' + relpath, **dictionary)
  126. os.symlink(relpath, 'AppDir/' + name)
  127. with open('AppDir/' + relpath) as f:
  128. for line in f:
  129. if line.startswith('Name='):
  130. application_name = line[5:].strip()
  131. elif line.startswith('Icon='):
  132. application_icon = line[5:].strip()
  133. # Bundle the application icon
  134. icon_paths = glob.glob('{:}/{:}.*'.format(appdir, application_icon))
  135. if icon_paths:
  136. for icon_path in icon_paths:
  137. ext = os.path.splitext(icon_path)[1]
  138. if ext in ('.png', '.svg'):
  139. break
  140. else:
  141. icon_path = None
  142. else:
  143. icon_path = None
  144. if icon_path is not None:
  145. name = os.path.basename(icon_path)
  146. log('BUNDLE', name)
  147. remove_file('AppDir/python.png')
  148. remove_tree('AppDir/usr/share/icons/hicolor/256x256')
  149. ext = os.path.splitext(name)[1]
  150. if ext == '.svg':
  151. size = 'scalable'
  152. else:
  153. with open(icon_path, 'rb') as f:
  154. head = f.read(24)
  155. width, height = struct.unpack('>ii', head[16:24])
  156. size = '{:}x{:}'.format(width, height)
  157. relpath = 'usr/share/icons/hicolor/{:}/apps/{:}'.format(size, name)
  158. destination = 'AppDir/' + relpath
  159. make_tree(os.path.dirname(destination))
  160. copy_file(icon_path, destination)
  161. os.symlink(relpath, 'AppDir/' + name)
  162. # Bundle any appdata
  163. meta_path = glob.glob(appdir + '/*.appdata.xml')
  164. if meta_path:
  165. meta_path = meta_path[0]
  166. name = os.path.basename(meta_path)
  167. log('BUNDLE', name)
  168. python = 'python' + python_fullversion
  169. remove_file('AppDir/usr/share/metainfo/{:}.appdata.xml'.format(
  170. python))
  171. relpath = 'usr/share/metainfo/' + name
  172. copy_template(meta_path, 'AppDir/' + relpath, **dictionary)
  173. # Bundle the requirements
  174. if requirements_list:
  175. deprecation = 'DEPRECATION: Python 2.7 reached the end of its life'
  176. system(('./AppDir/AppRun', '-m', 'pip', 'install', '-U',
  177. '--no-warn-script-location', 'pip'), exclude=deprecation)
  178. for requirement in requirements_list:
  179. log('BUNDLE', requirement)
  180. system(('./AppDir/AppRun', '-m', 'pip', 'install', '-U',
  181. '--no-warn-script-location', requirement),
  182. exclude=deprecation)
  183. # Bundle the entry point
  184. entrypoint_path = glob.glob(appdir + '/entrypoint.*')
  185. if entrypoint_path:
  186. entrypoint_path = entrypoint_path[0]
  187. log('BUNDLE', os.path.basename(entrypoint_path))
  188. entrypoint = load_template(entrypoint_path, **dictionary)
  189. copy_template(PREFIX + '/data/apprun.sh', 'AppDir/AppRun',
  190. entrypoint=entrypoint)
  191. # Build the new AppImage
  192. destination = '{:}-{:}.AppImage'.format(application_name,
  193. platform.machine())
  194. build_appimage(destination=destination)
  195. shutil.move(destination, os.path.join(pwd, destination))