|
|
@@ -0,0 +1,236 @@
|
|
|
+import json
|
|
|
+import glob
|
|
|
+import os
|
|
|
+import platform
|
|
|
+import re
|
|
|
+import shutil
|
|
|
+import stat
|
|
|
+import struct
|
|
|
+
|
|
|
+from ...appimage import build_appimage
|
|
|
+from ...utils.compat import decode
|
|
|
+from ...utils.deps import PREFIX
|
|
|
+from ...utils.fs import copy_file, make_tree, remove_file, remove_tree
|
|
|
+from ...utils.log import log
|
|
|
+from ...utils.system import system
|
|
|
+from ...utils.template import copy_template, load_template
|
|
|
+from ...utils.tmp import TemporaryDirectory
|
|
|
+from ...utils.url import urlopen, urlretrieve
|
|
|
+
|
|
|
+
|
|
|
+__all__ = ['execute']
|
|
|
+
|
|
|
+
|
|
|
+def _unpack_args(args):
|
|
|
+ '''Unpack command line arguments
|
|
|
+ '''
|
|
|
+ return args.appdir, args.name
|
|
|
+
|
|
|
+
|
|
|
+_tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage')
|
|
|
+
|
|
|
+def execute(appdir, name=None, python_version=None, linux_tag=None,
|
|
|
+ python_tag=None):
|
|
|
+ '''Build a Python application using a base AppImage
|
|
|
+ '''
|
|
|
+
|
|
|
+ # Download releases meta data
|
|
|
+ releases = json.load(
|
|
|
+ urlopen('https://api.github.com/repos/niess/python-appimage/releases'))
|
|
|
+
|
|
|
+
|
|
|
+ # Fetch the requested Python version or the latest if no specific version
|
|
|
+ # was requested
|
|
|
+ release, version = None, '0.0'
|
|
|
+ for entry in releases:
|
|
|
+ tag = entry['tag_name']
|
|
|
+ if not tag.startswith('python'):
|
|
|
+ continue
|
|
|
+ v = tag[6:]
|
|
|
+ if python_version is None:
|
|
|
+ if v > version:
|
|
|
+ release, version = entry, v
|
|
|
+ elif v == python_version:
|
|
|
+ release = entry
|
|
|
+ break
|
|
|
+ if release is None:
|
|
|
+ raise ValueError('could not find base image for Python ' +
|
|
|
+ python_version)
|
|
|
+ elif python_version is None:
|
|
|
+ python_version = version
|
|
|
+
|
|
|
+
|
|
|
+ # Check for a suitable image
|
|
|
+ if linux_tag is None:
|
|
|
+ linux_tag = 'manylinux1_' + platform.machine()
|
|
|
+
|
|
|
+ if python_tag is None:
|
|
|
+ v = ''.join(version.split('.'))
|
|
|
+ python_tag = 'cp{0:}-cp{0:}'.format(v)
|
|
|
+ if version < '3.8':
|
|
|
+ python_tag += 'm'
|
|
|
+
|
|
|
+ target_tag = '-'.join((python_tag, linux_tag))
|
|
|
+
|
|
|
+ assets = release['assets']
|
|
|
+ for asset in assets:
|
|
|
+ match = _tag_pattern.search(asset['name'])
|
|
|
+ if str(match.group(2)) == target_tag:
|
|
|
+ python_fullversion = str(match.group(1))
|
|
|
+ break
|
|
|
+ else:
|
|
|
+ raise ValueError('Could not find base image for tag ' + target_tag)
|
|
|
+
|
|
|
+ base_image = asset['browser_download_url']
|
|
|
+
|
|
|
+
|
|
|
+ # Set the dictionary for template files
|
|
|
+ dictionary = {
|
|
|
+ 'architecture' : platform.machine(),
|
|
|
+ 'linux-tag' : linux_tag,
|
|
|
+ 'python-executable' : '${APPDIR}/usr/bin/python' + python_version,
|
|
|
+ 'python-fullversion' : python_fullversion,
|
|
|
+ 'python-tag' : python_tag,
|
|
|
+ 'python-version' : python_version
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ # Get the list of requirements
|
|
|
+ requirements_list = []
|
|
|
+ requirements_path = appdir + '/requirements.txt'
|
|
|
+ if os.path.exists(requirements_path):
|
|
|
+ with open(requirements_path) as f:
|
|
|
+ for line in f:
|
|
|
+ line = line.strip()
|
|
|
+ if line.startswith('#'):
|
|
|
+ continue
|
|
|
+ requirements_list.append(line)
|
|
|
+
|
|
|
+ requirements = sorted(requirements_list)
|
|
|
+ n = len(requirements)
|
|
|
+ if n == 0:
|
|
|
+ requirements = ''
|
|
|
+ elif n == 1:
|
|
|
+ requirements = requirements[0]
|
|
|
+ elif n == 2:
|
|
|
+ requirements = ' and '.join(requirements)
|
|
|
+ else:
|
|
|
+ tmp = ', '.join(requirements[:-1])
|
|
|
+ requirements = tmp + ' and ' + requirements[-1]
|
|
|
+ dictionary['requirements'] = requirements
|
|
|
+
|
|
|
+
|
|
|
+ # Build the application
|
|
|
+ appdir = os.path.realpath(appdir)
|
|
|
+ pwd = os.getcwd()
|
|
|
+ with TemporaryDirectory() as tmpdir:
|
|
|
+ application_name = os.path.basename(appdir)
|
|
|
+ application_icon = application_name
|
|
|
+
|
|
|
+ # Extract the base AppImage
|
|
|
+ log('EXTRACT', '%s', os.path.basename(base_image))
|
|
|
+ urlretrieve(base_image, 'base.AppImage')
|
|
|
+ os.chmod('base.AppImage', stat.S_IRWXU)
|
|
|
+ system('./base.AppImage --appimage-extract')
|
|
|
+ system('mv squashfs-root AppDir')
|
|
|
+
|
|
|
+
|
|
|
+ # Bundle the desktop file
|
|
|
+ desktop_path = glob.glob(appdir + '/*.desktop')
|
|
|
+ if desktop_path:
|
|
|
+ desktop_path = desktop_path[0]
|
|
|
+ name = os.path.basename(desktop_path)
|
|
|
+ log('BUNDLE', name)
|
|
|
+
|
|
|
+ python = 'python' + python_fullversion
|
|
|
+ remove_file('AppDir/{:}.desktop'.format(python))
|
|
|
+ remove_file('AppDir/usr/share/applications/{:}.desktop'.format(
|
|
|
+ python))
|
|
|
+
|
|
|
+ relpath = 'usr/share/applications/' + name
|
|
|
+ copy_template(desktop_path, 'AppDir/' + relpath, **dictionary)
|
|
|
+ os.symlink(relpath, 'AppDir/' + name)
|
|
|
+
|
|
|
+ with open('AppDir/' + relpath) as f:
|
|
|
+ for line in f:
|
|
|
+ if line.startswith('Name='):
|
|
|
+ application_name = line[5:].strip()
|
|
|
+ elif line.startswith('Icon='):
|
|
|
+ application_icon = line[5:].strip()
|
|
|
+
|
|
|
+
|
|
|
+ # Bundle the application icon
|
|
|
+ icon_paths = glob.glob('{:}/{:}.*'.format(appdir, application_icon))
|
|
|
+ if icon_paths:
|
|
|
+ for icon_path in icon_paths:
|
|
|
+ ext = os.path.splitext(icon_path)[1]
|
|
|
+ if ext in ('.png', '.svg'):
|
|
|
+ break
|
|
|
+ else:
|
|
|
+ icon_path = None
|
|
|
+ else:
|
|
|
+ icon_path = None
|
|
|
+
|
|
|
+ if icon_path is not None:
|
|
|
+ name = os.path.basename(icon_path)
|
|
|
+ log('BUNDLE', name)
|
|
|
+
|
|
|
+ remove_file('AppDir/python.png')
|
|
|
+ remove_tree('AppDir/usr/share/icons/hicolor/256x256')
|
|
|
+
|
|
|
+ ext = os.path.splitext(name)[1]
|
|
|
+ if ext == '.svg':
|
|
|
+ size = 'scalable'
|
|
|
+ else:
|
|
|
+ with open(icon_path, 'rb') as f:
|
|
|
+ head = f.read(24)
|
|
|
+ width, height = struct.unpack('>ii', head[16:24])
|
|
|
+ size = '{:}x{:}'.format(width, height)
|
|
|
+
|
|
|
+ relpath = 'usr/share/icons/hicolor/{:}/apps/{:}'.format(size, name)
|
|
|
+ destination = 'AppDir/' + relpath
|
|
|
+ make_tree(os.path.dirname(destination))
|
|
|
+ copy_file(icon_path, destination)
|
|
|
+ os.symlink(relpath, 'AppDir/' + name)
|
|
|
+
|
|
|
+
|
|
|
+ # Bundle any appdata
|
|
|
+ meta_path = glob.glob(appdir + '/*.appdata.xml')
|
|
|
+ if meta_path:
|
|
|
+ meta_path = meta_path[0]
|
|
|
+ name = os.path.basename(meta_path)
|
|
|
+ log('BUNDLE', name)
|
|
|
+
|
|
|
+ python = 'python' + python_fullversion
|
|
|
+ remove_file('AppDir/usr/share/metainfo/{:}.appdata.xml'.format(
|
|
|
+ python))
|
|
|
+
|
|
|
+ relpath = 'usr/share/metainfo/' + name
|
|
|
+ copy_template(meta_path, 'AppDir/' + relpath, **dictionary)
|
|
|
+
|
|
|
+
|
|
|
+ # Bundle the requirements
|
|
|
+ if requirements_list:
|
|
|
+ system('./AppDir/AppRun -m pip install -U '
|
|
|
+ '--no-warn-script-location pip')
|
|
|
+ for requirement in requirements_list:
|
|
|
+ log('BUNDLE', requirement)
|
|
|
+ system('./AppDir/AppRun -m pip install -U '
|
|
|
+ '--no-warn-script-location ' + requirement)
|
|
|
+
|
|
|
+
|
|
|
+ # Bundle the entry point
|
|
|
+ entrypoint_path = glob.glob(appdir + '/entrypoint.*')
|
|
|
+ if entrypoint_path:
|
|
|
+ entrypoint_path = entrypoint_path[0]
|
|
|
+ log('BUNDLE', os.path.basename(entrypoint_path))
|
|
|
+ entrypoint = load_template(entrypoint_path, **dictionary)
|
|
|
+ copy_template(PREFIX + '/data/apprun.sh', 'AppDir/AppRun',
|
|
|
+ entrypoint=entrypoint)
|
|
|
+
|
|
|
+
|
|
|
+ # Build the new AppImage
|
|
|
+ destination = '{:}-{:}.AppImage'.format(application_name,
|
|
|
+ platform.machine())
|
|
|
+ build_appimage(destination=destination)
|
|
|
+ shutil.move(destination, os.path.join(pwd, destination))
|