Initial commit

This commit is contained in:
Valentin Niess
2020-03-29 11:59:23 +02:00
commit dc3acadb9a
24 changed files with 1735 additions and 0 deletions

View File

View File

@@ -0,0 +1,58 @@
import argparse
from importlib import import_module
import logging
import os
import sys
from .deps import fetch_all
__all__ = ['main']
def main():
# Parse arguments
parser = argparse.ArgumentParser(
description='Bundle a Python install into an AppImage')
subparsers = parser.add_subparsers(title='builder',
help='Appimage builder',
dest='builder')
parser.add_argument('-q', '--quiet', help='disable logging',
dest='verbosity', action='store_const', const=logging.ERROR)
parser.add_argument('-v', '--verbose', help='print extra information',
dest='verbosity', action='store_const', const=logging.DEBUG)
parser.add_argument('--deploy', help=argparse.SUPPRESS,
action='store_true', default=False)
local_parser = subparsers.add_parser('local')
local_parser.add_argument('-d', '--destination',
help='AppImage destination')
local_parser.add_argument('-p', '--python', help='python executable')
manylinux_parser = subparsers.add_parser('manylinux')
manylinux_parser.add_argument('tag', help='manylinux image tag')
manylinux_parser.add_argument('abi', help='python ABI')
manylinux_parser.add_argument('--contained', help=argparse.SUPPRESS,
action='store_true', default=False)
args = parser.parse_args()
# Configure the verbosity
if args.verbosity:
logging.getLogger().setLevel(args.verbosity)
if args.deploy:
# Fetch dependencies and exit
fetch_all()
sys.exit(0)
# Call the AppImage builder
builder = import_module('.' + args.builder, package=__package__)
builder.build(*builder._unpack_args(args))
if __name__ == "__main__":
main()

55
python_appimage/build.py Normal file
View File

@@ -0,0 +1,55 @@
import os
import subprocess
import sys
from .deps import APPIMAGETOOL, ensure_appimagetool
from .docker import docker_run
from .fs import copy_tree
from .log import debug, log
from .tmp import TemporaryDirectory
__all__ = ['build_appimage']
def build_appimage(appdir=None, destination=None):
'''Build an AppImage from an AppDir
'''
if appdir is None:
appdir = 'AppDir'
log('BUILD', appdir)
ensure_appimagetool()
cmd = [APPIMAGETOOL, appdir]
if destination is not None:
cmd.append(destination)
cmd = ' '.join(cmd)
debug('SYSTEM', cmd)
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
stdout = []
while True:
out = p.stdout.readline()
try:
out = out.decode()
except AttributeError:
out = str(out)
stdout.append(out)
if out == '' and p.poll() is not None:
break
elif out:
out = out.replace('%', '%%')[:-1]
for line in out.split(os.linesep):
if line.startswith('WARNING'):
log('WARNING', line[9:])
elif line.startswith('Error'):
raise RuntimeError(line)
else:
debug('APPIMAGE', line)
rc = p.poll()
if rc != 0:
print(''.join(stdout))
sys.stdout.flush()
raise RuntimeError('Could not build AppImage')

9
python_appimage/data/apprun.sh Executable file
View File

@@ -0,0 +1,9 @@
#! /bin/bash
# Export APPRUN if running from an extracted image
self="$(readlink -f -- $0)"
here="${self%/*}"
APPDIR="${APPDIR:-${here}}"
# Call the python wrapper
"${APPDIR}/usr/bin/python{{version}}" "$@"

View File

@@ -0,0 +1,45 @@
#!/bin/bash
SCRIPT="$(readlink -f -- $0)"
SCRIPTPATH="$(dirname $SCRIPT)"
APPDIR="${APPDIR:-$SCRIPTPATH/../..}"
# Configure the environment
if [ -d "${APPDIR}/usr/share/tcltk" ]; then
export TCL_LIBRARY="$(ls -d ${APPDIR}/usr/share/tcltk/tcl* | tail -1)"
export TK_LIBRARY="$(ls -d ${APPDIR}/usr/share/tcltk/tk* | tail -1)"
export TKPATH="${TK_LIBRARY}"
fi
# Resolve symlinks within the image
prefix="opt/{{PYTHON}}"
nickname="{{PYTHON}}"
executable="${APPDIR}/${prefix}/bin/${nickname}"
if [ -L "${executable}" ]; then
nickname="$(basename $(readlink -f ${executable}))"
fi
for opt in "$@"
do
[ "${opt:0:1}" != "-" ] && break
if [[ "${opt}" =~ "I" ]] || [[ "${opt}" =~ "E" ]]; then
# Environment variables are disabled ($PYTHONHOME). Let's run in a safe
# mode from the raw Python binary inside the AppImage
"$APPDIR/${prefix}/bin/${nickname}" "$@"
exit "$?"
fi
done
# But don't resolve symlinks from outside!
if [[ "${ARGV0}" =~ "/" ]]; then
executable="$(cd $(dirname ${ARGV0}) && pwd)/$(basename ${ARGV0})"
elif [[ "${ARGV0}" != "" ]]; then
executable=$(which "${ARGV0}")
fi
# Wrap the call to Python in order to mimic a call from the source
# executable ($ARGV0), but potentially located outside of the Python
# install ($PYTHONHOME)
(PYTHONHOME="${APPDIR}/${prefix}" exec -a "${executable}" "$APPDIR/${prefix}/bin/${nickname}" "$@")
exit "$?"

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>python{{fullversion}}</id>
<metadata_license>Python-2.0</metadata_license>
<project_license>Python-2.0</project_license>
<name>Python {{version}}</name>
<summary>A Python {{version}} runtime</summary>
<description>
<p> A relocated Python {{version}} installation running from an
AppImage.
</p>
</description>
<launchable type="desktop-id">python.desktop</launchable>
<url type="homepage">https://python.org</url>
<provides>
<binary>python{{version}}</binary>
</provides>
</component>

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=python{{fullversion}}
Exec=python{{version}}
Comment=A Python {{version}} runtime
Icon=python
Categories=Development;
Terminal=true

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,26 @@
"""Hook for cleaning the paths detected by Python
"""
import os
import sys
def clean_path():
site_packages = "/usr/local/lib/python{:}.{:}/site-packages".format(
*sys.version_info[:2])
binaries_path = "/usr/local/bin"
env_path = os.getenv("PYTHONPATH")
if env_path is None:
env_path = []
else:
env_path = [os.path.realpath(path) for path in env_path.split(":")]
if ((os.path.dirname(sys.executable) != binaries_path) and
(site_packages not in env_path)):
# Remove the builtin site-packages from the path
try:
sys.path.remove(site_packages)
except ValueError:
pass
clean_path()

99
python_appimage/deps.py Normal file
View File

@@ -0,0 +1,99 @@
import os
import platform
import stat
from .fs import copy_file, copy_tree, make_tree
from .log import log
from .system import system
from .tmp import TemporaryDirectory
from .url import urlretrieve
__all__ = ['APPIMAGETOOL', 'EXCLUDELIST', 'PATCHELF', 'PREFIX',
'ensure_appimagetool', 'ensure_excludelist', 'ensure_patchelf',
'fetch_all']
_ARCH = platform.machine()
PREFIX = os.path.abspath(os.path.dirname(__file__))
'''Package installation prefix'''
APPIMAGETOOL = PREFIX + '/bin/appimagetool.' + _ARCH
'''Location of the appimagetool binary'''
EXCLUDELIST = PREFIX + '/data/excludelist'
'''AppImage exclusion list'''
PATCHELF = PREFIX + '/bin/patchelf.' + _ARCH
'''Location of the PatchELF binary'''
def ensure_appimagetool():
'''Fetch appimagetool from the web if not available locally
'''
if os.path.exists(APPIMAGETOOL):
return
appimage = 'appimagetool-{0:}.AppImage'.format(_ARCH)
baseurl = 'https://github.com/AppImage/AppImageKit/releases/' \
'download/continuous'
log('INSTALL', 'appimagetool from %s', baseurl)
appdir_name = 'appimagetool-{:}.appdir'.format(_ARCH)
appdir = os.path.join(os.path.dirname(APPIMAGETOOL), appdir_name)
if not os.path.exists(appdir):
make_tree(os.path.dirname(appdir))
with TemporaryDirectory() as tmpdir:
urlretrieve(os.path.join(baseurl, appimage), appimage)
os.chmod(appimage, stat.S_IRWXU)
system('./' + appimage, '--appimage-extract')
copy_tree('squashfs-root', appdir)
if not os.path.exists(APPIMAGETOOL):
os.symlink(appdir_name + '/AppRun', APPIMAGETOOL)
# Installers for dependencies
def ensure_excludelist():
'''Fetch the AppImage excludelist from the web if not available locally
'''
if os.path.exists(EXCLUDELIST):
return
baseurl = 'https://raw.githubusercontent.com/probonopd/AppImages/master'
log('INSTALL', 'excludelist from %s', baseurl)
urlretrieve(baseurl + '/excludelist', EXCLUDELIST)
mode = os.stat(EXCLUDELIST)[stat.ST_MODE]
os.chmod(EXCLUDELIST, mode | stat.S_IWGRP | stat.S_IWOTH)
def ensure_patchelf():
'''Fetch PatchELF from the web if not available locally
'''
if os.path.exists(PATCHELF):
return
iarch = 'i386' if _ARCH == 'i686' else _ARCH
appimage = 'patchelf-{0:}.AppImage'.format(iarch)
baseurl = 'https://github.com/niess/patchelf.appimage/releases/download'
log('INSTALL', 'patchelf from %s', baseurl)
dirname = os.path.dirname(PATCHELF)
patchelf = dirname + '/patchelf.' + _ARCH
make_tree(dirname)
with TemporaryDirectory() as tmpdir:
urlretrieve(os.path.join(baseurl, 'rolling', appimage), appimage)
os.chmod(appimage, stat.S_IRWXU)
system('./' + appimage, '--appimage-extract')
copy_file('squashfs-root/usr/bin/patchelf', patchelf)
os.chmod(patchelf, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
def fetch_all():
'''Fetch all dependencies from the web
'''
ensure_appimagetool()
ensure_excludelist()
ensure_patchelf()

49
python_appimage/docker.py Normal file
View File

@@ -0,0 +1,49 @@
import os
import platform
import stat
import subprocess
import sys
from .log import log
from .system import system
def docker_run(image, extra_cmds):
'''Execute commands within a docker container
'''
ARCH = platform.machine()
if image.endswith(ARCH):
bash_arg = '/pwd/run.sh'
elif image.endswith('i686') and ARCH == 'x86_64':
bash_arg = '-c "linux32 /pwd/run.sh"'
elif image.endswith('x86_64') and ARCH == 'i686':
bash_arg = '-c "linux64 /pwd/run.sh"'
else:
raise ValueError('Unsupported Docker image: ' + image)
log('PULL', image)
system('docker', 'pull', image)
script = [
'set -e',
'trap "chown -R {:}:{:} *" EXIT'.format(os.getuid(),
os.getgid()),
'cd /pwd'
]
script += extra_cmds
with open('run.sh', 'w') as f:
f.write(os.linesep.join(script))
os.chmod('run.sh', stat.S_IRWXU)
cmd = ' '.join(('docker', 'run', '--mount',
'type=bind,source={:},target=/pwd'.format(os.getcwd()),
image, '/bin/bash', bash_arg))
log('RUN', image)
p = subprocess.Popen(cmd, shell=True)
p.communicate()
if p.returncode != 0:
sys.exit(p.returncode)

75
python_appimage/fs.py Normal file
View File

@@ -0,0 +1,75 @@
from distutils.dir_util import mkpath as _mkpath, remove_tree as _remove_tree
from distutils.file_util import copy_file as _copy_file
import errno
import os
from .log import debug
__all__ = ['copy_file', 'copy_tree', 'make_tree', 'remove_file', 'remove_tree']
# Wrap some file system related functions
def make_tree(path):
'''Create directories recursively if they don't exist
'''
debug('MKDIR', path)
return _mkpath(path)
def copy_file(source, destination, update=False, verbose=True):
'''
'''
name = os.path.basename(source)
if verbose:
debug('COPY', '%s from %s', name, os.path.dirname(source))
_copy_file(source, destination, update=update)
def copy_tree(source, destination):
'''Copy (or update) a directory preserving symlinks
'''
if not os.path.exists(source):
raise OSError(errno.ENOENT, 'No such file or directory: ' + source)
name = os.path.basename(source)
debug('COPY', '%s from %s', name, os.path.dirname(source))
for root, _, files in os.walk(source):
relpath = os.path.relpath(root, source)
dirname = os.path.join(destination, relpath)
_mkpath(dirname)
for file_ in files:
src = os.path.join(root, file_)
dst = os.path.join(dirname, file_)
if os.path.islink(src):
try:
os.remove(dst)
except OSError:
pass
linkto = os.readlink(src)
os.symlink(linkto, dst)
else:
copy_file(src, dst, update=True, verbose=False)
def remove_file(path):
'''remove a file if it exists
'''
name = os.path.basename(path)
debug('REMOVE', '%s from %s', name, os.path.dirname(path))
try:
os.remove(path)
except OSError:
pass
def remove_tree(path):
'''remove a directory if it exists
'''
name = os.path.basename(path)
debug('REMOVE', '%s from %s', name, os.path.dirname(path))
try:
_remove_tree(path)
except OSError:
pass

40
python_appimage/local.py Normal file
View File

@@ -0,0 +1,40 @@
import glob
import os
import shutil
from .build import build_appimage
from .relocate import relocate_python
from .tmp import TemporaryDirectory
__all__ = ['build']
def _unpack_args(args):
'''Unpack command line arguments
'''
return args.python, args.destination
def build(python=None, destination=None):
'''Build a Python AppImage using a local installation
'''
pwd = os.getcwd()
with TemporaryDirectory() as tmpdir:
relocate_python(python)
dirname, pattern = None, None
if destination is not None:
dirname, destination = os.path.split(destination)
pattern = destination
if pattern is None:
pattern = 'python*.AppImage'
build_appimage(destination=destination)
appimage = glob.glob(pattern)[0]
if dirname is None:
dirname = pwd
else:
os.chdir(pwd)
dirname = os.path.abspath(dirname)
os.chdir(tmpdir)
shutil.move(appimage, os.path.join(dirname, appimage))

23
python_appimage/log.py Normal file
View File

@@ -0,0 +1,23 @@
import logging
__all__ = ['debug', 'log']
# Configure the logger
logging.basicConfig(
format='[%(asctime)s] %(message)s',
level=logging.INFO
)
def log(task, fmt, *args):
'''Log a standard message
'''
logging.info('%-8s ' + fmt, task, *args)
def debug(task, fmt, *args):
'''Report some debug information
'''
logging.debug('%-8s ' + fmt, task, *args)

View File

@@ -0,0 +1,94 @@
import glob
import os
import platform
import shutil
import sys
from .build import build_appimage
from .docker import docker_run
from .fs import copy_tree
from .relocate import relocate_python
from .tmp import TemporaryDirectory
__all__ = ['build']
def _unpack_args(args):
'''Unpack command line arguments
'''
return args.tag, args.abi, args.contained
def _get_appimage_name(abi, tag):
'''Format the Python AppImage name using the ABI and OS tags
'''
# Read the Python version from the desktop file
desktop = glob.glob('AppDir/python*.desktop')[0]
fullversion = desktop[13:-8]
# Finish building the AppImage on the host. See below.
return 'python{:}-{:}-manylinux{:}.AppImage'.format(
fullversion, abi, tag)
def build(tag, abi, contained=False):
'''Build a Python AppImage using a manylinux docker image
'''
if not contained:
# Forward the build to a Docker image
image = 'quay.io/pypa/manylinux' + tag
python = '/opt/python/' + abi + '/bin/python'
pwd = os.getcwd()
dirname = os.path.abspath(os.path.dirname(__file__))
with TemporaryDirectory() as tmpdir:
copy_tree(dirname, 'python_appimage')
argv = ' '.join(sys.argv[1:])
script = (
'yum --disablerepo="*" --enablerepo=base install -q -y tk',
python + ' -m python_appimage ' + argv + ' --contained',
''
)
docker_run(image, script)
appimage_name = _get_appimage_name(abi, tag)
if tag.startswith('1_'):
# appimagetool does not run on manylinux1 (CentOS 5). Below is
# a patch for this specific case.
arch = tag.split('_', 1)[-1]
if arch == platform.machine():
# Pack the image directly from the host
build_appimage(destination=appimage_name)
else:
# Use a manylinux2010 Docker image (CentOS 6) in order to
# pack the image.
script = (
python + ' -m python_appimage ' + argv + ' --contained',
''
)
docker_run('quay.io/pypa/manylinux2010_' + arch, script)
shutil.move(appimage_name, os.path.join(pwd, appimage_name))
else:
# We are running within a manylinux Docker image
is_manylinux1 = tag.startswith('1_')
if not os.path.exists('AppDir'):
# Relocate the targeted manylinux Python installation
relocate_python()
else:
# This is a second stage build. The Docker image has actually been
# overriden (see above).
is_manylinux1 = False
if is_manylinux1:
# Build only the AppDir when running within a manylinux1 Docker
# image because appimagetool does not support CentOS 5.
pass
else:
build_appimage(destination=_get_appimage_name(abi, tag))

268
python_appimage/relocate.py Normal file
View File

@@ -0,0 +1,268 @@
import glob
import os
import re
import shutil
import sys
from .deps import EXCLUDELIST, PATCHELF, PREFIX, ensure_excludelist, \
ensure_patchelf
from .fs import make_tree, copy_file, copy_tree, remove_file, remove_tree
from .log import debug, log
from .system import ldd, system
__all__ = ["patch_binary", "relocate_python"]
_template_pattern = re.compile('[{][{]([^{}]+)[}][}]')
def _copy_template(name, destination, **kwargs):
'''Copy a template file and substitue keywords
'''
debug('COPY', '%s as %s', name, destination)
source = os.path.join(PREFIX, 'data', name)
with open(source) as f:
template = f.read()
def matcher(m):
return kwargs[m.group(1)]
txt = _template_pattern.sub(matcher, template)
with open(destination, 'w') as f:
f.write(txt)
shutil.copymode(source, destination)
_excluded_libs = None
'''Appimage excluded libraries, i.e. assumed to be installed on the host
'''
def patch_binary(path, libdir, recursive=True):
'''Patch the RPATH of a binary and and fetch its dependencies
'''
global _excluded_libs
if _excluded_libs is None:
ensure_excludelist()
excluded = []
with open(EXCLUDELIST) as f:
for line in f:
line = line.strip()
if (not line) or line.startswith('#'):
continue
excluded.append(line.split(' ', 1)[0])
_excluded_libs = excluded
else:
excluded = _excluded_libs
ensure_patchelf()
rpath = '\'' + system(PATCHELF, '--print-rpath', path) + '\''
relpath = os.path.relpath(libdir, os.path.dirname(path))
relpath = '' if relpath == '.' else '/' + relpath
expected = '\'$ORIGIN' + relpath + '\''
if rpath != expected:
system(PATCHELF, '--set-rpath', expected, path)
deps = ldd(path)
for dep in deps:
name = os.path.basename(dep)
if name in excluded:
continue
target = libdir + '/' + name
if not os.path.exists(target):
libname = os.path.basename(dep)
copy_file(dep, target)
if recursive:
patch_binary(target, libdir, recursive=True)
def relocate_python(python=None, appdir=None):
'''Bundle a Python install inside an AppDir
'''
if python is not None:
if not os.path.exists(python):
raise ValueError('could not access ' + python)
if appdir is None:
appdir = 'AppDir'
# Set some key variables & paths
if python:
FULLVERSION = system(python, '-c',
'"import sys; print(\'{:}.{:}.{:}\'.format(*sys.version_info[:3]))"')
FULLVERSION = FULLVERSION.strip()
else:
FULLVERSION = '{:}.{:}.{:}'.format(*sys.version_info[:3])
VERSION = '.'.join(FULLVERSION.split('.')[:2])
PYTHON_X_Y = 'python' + VERSION
APPDIR = os.path.abspath(appdir)
APPDIR_BIN = APPDIR + '/usr/bin'
APPDIR_LIB = APPDIR + '/usr/lib'
APPDIR_SHARE = APPDIR + '/usr/share'
if python:
HOST_PREFIX = system(
python, '-c', '"import sys; print(sys.prefix)"').strip()
else:
HOST_PREFIX = sys.prefix
HOST_BIN = HOST_PREFIX + '/bin'
HOST_INC = HOST_PREFIX + '/include/' + PYTHON_X_Y
if not os.path.exists(HOST_INC):
HOST_INC += 'm'
HOST_LIB = HOST_PREFIX + '/lib'
HOST_PKG = HOST_LIB + '/' + PYTHON_X_Y
PYTHON_PREFIX = APPDIR + '/opt/' + PYTHON_X_Y
PYTHON_BIN = PYTHON_PREFIX + '/bin'
PYTHON_INC = PYTHON_PREFIX + '/include/' + PYTHON_X_Y
PYTHON_LIB = PYTHON_PREFIX + '/lib'
PYTHON_PKG = PYTHON_LIB + '/' + PYTHON_X_Y
# Copy the running Python's install
log('CLONE', '%s from %s', PYTHON_X_Y, HOST_PREFIX)
source = HOST_BIN + '/' + PYTHON_X_Y
if not os.path.exists(source):
raise ValueError('could not find {0:} executable'.format(PYTHON_X_Y))
make_tree(PYTHON_BIN)
target = PYTHON_BIN + '/' + PYTHON_X_Y
copy_file(source, target, update=True)
copy_tree(HOST_PKG, PYTHON_PKG)
copy_tree(HOST_INC, PYTHON_INC)
# Remove unrelevant files
log('PRUNE', '%s packages', PYTHON_X_Y)
remove_file(PYTHON_LIB + '/lib' + PYTHON_X_Y + '.a')
remove_tree(PYTHON_PKG + '/test')
remove_file(PYTHON_PKG + '/dist-packages')
matches = glob.glob(PYTHON_PKG + '/config-*-linux-*')
for path in matches:
remove_tree(path)
# Wrap the Python executable
log('WRAP', '%s executable', PYTHON_X_Y)
with open(PREFIX + '/data/python-wrapper.sh') as f:
text = f.read()
text = text.replace('{{PYTHON}}', PYTHON_X_Y)
make_tree(APPDIR_BIN)
target = APPDIR_BIN + '/' + PYTHON_X_Y
with open(target, 'w') as f:
f.write(text)
shutil.copymode(PYTHON_BIN + '/' + PYTHON_X_Y, target)
# Set a hook in Python for cleaning the path detection
log('HOOK', '%s site packages', PYTHON_X_Y)
sitepkgs = PYTHON_PKG + '/site-packages'
make_tree(sitepkgs)
copy_file(PREFIX + '/data/sitecustomize.py', sitepkgs)
# Set RPATHs and bundle external libraries
log('LINK', '%s C-extensions', PYTHON_X_Y)
make_tree(APPDIR_LIB)
patch_binary(PYTHON_BIN + '/' + PYTHON_X_Y, APPDIR_LIB, recursive=False)
for root, dirs, files in os.walk(PYTHON_PKG + '/lib-dynload'):
for file_ in files:
if not file_.endswith('.so'):
continue
patch_binary(os.path.join(root, file_), APPDIR_LIB, recursive=False)
for file_ in glob.iglob(APPDIR_LIB + '/lib*.so*'):
patch_binary(file_, APPDIR_LIB, recursive=True)
# Copy shared data for TCl/Tk
tkinter = glob.glob(PYTHON_PKG + '/lib-dynload/_tkinter*.so')
if tkinter:
tkinter = tkinter[0]
for dep in ldd(tkinter):
name = os.path.basename(dep)
if name.startswith('libtk'):
match = re.search('libtk([0-9]+[.][0-9]+)', name)
tk_version = match.group(1)
break
else:
raise RuntimeError('could not guess Tcl/Tk version')
tcltkdir = APPDIR_SHARE + '/tcltk'
if (not os.path.exists(tcltkdir + '/tcl' + tk_version)) or \
(not os.path.exists(tcltkdir + '/tk' + tk_version)):
hostdir = '/usr/share/tcltk'
if os.path.exists(hostdir):
make_tree(APPDIR_SHARE)
copy_tree(hostdir, tcltkdir)
else:
make_tree(tcltkdir)
tclpath = '/usr/share/tcl' + tk_version
if not tclpath:
raise ValueError('could not find ' + tclpath)
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
tkpath = '/usr/share/tk' + tk_version
if not tkpath:
raise ValueError('could not find ' + tkpath)
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
# Bundle the entry point
apprun = APPDIR + '/AppRun'
if not os.path.exists(apprun):
log('INSTALL', 'AppRun')
_copy_template('apprun.sh', apprun, version=VERSION)
# Bundle the desktop file
desktop_name = 'python{:}.desktop'.format(FULLVERSION)
desktop = os.path.join(APPDIR, desktop_name)
if not os.path.exists(desktop):
log('INSTALL', desktop_name)
apps = 'usr/share/applications'
appfile = '{:}/{:}/python{:}.desktop'.format(APPDIR, apps, FULLVERSION)
if not os.path.exists(appfile):
make_tree(os.path.join(APPDIR, apps))
_copy_template('python.desktop', appfile, version=VERSION,
fullversion=FULLVERSION)
os.symlink(os.path.join(apps, desktop_name), desktop)
# Bundle icons
icons = 'usr/share/icons/hicolor/256x256/apps'
icon = os.path.join(APPDIR, 'python.png')
if not os.path.exists(icon):
log('INSTALL', 'python.png')
make_tree(os.path.join(APPDIR, icons))
copy_file(PREFIX + '/data/python.png',
os.path.join(APPDIR, icons, 'python.png'))
os.symlink(os.path.join(icons, 'python.png'), icon)
diricon = os.path.join(APPDIR, '.DirIcon')
if not os.path.exists(diricon):
os.symlink('python.png', diricon)
# Bundle metadata
meta_name = 'python{:}.appdata.xml'.format(FULLVERSION)
meta_dir = os.path.join(APPDIR, 'usr/share/metainfo')
meta_file = os.path.join(meta_dir, meta_name)
if not os.path.exists(meta_file):
log('INSTALL', meta_name)
make_tree(meta_dir)
_copy_template('python.appdata.xml', meta_file, version=VERSION,
fullversion=FULLVERSION)

46
python_appimage/system.py Normal file
View File

@@ -0,0 +1,46 @@
import os
import re
import subprocess
from .log import debug
__all__ = ['ldd', 'system']
def _decode(s):
'''Decode Python 3 bytes as str
'''
try:
return s.decode()
except AttributeError:
return s
def system(*args):
'''System call with capturing output
'''
cmd = ' '.join(args)
debug('SYSTEM', cmd)
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = p.communicate()
if err:
err = _decode(err)
stripped = [line for line in err.split(os.linesep)
if line and not line.startswith('fuse: warning:')]
if stripped:
raise RuntimeError(err)
return str(_decode(out).strip())
_ldd_pattern = re.compile('=> (.+) [(]0x')
def ldd(path):
'''Get dependencies list of dynamic libraries
'''
out = system('ldd', path)
return _ldd_pattern.findall(out)

24
python_appimage/tmp.py Normal file
View File

@@ -0,0 +1,24 @@
from contextlib import contextmanager as contextmanager
import os
import tempfile
from .fs import remove_tree
from .log import debug
__all__ = ['TemporaryDirectory']
@contextmanager
def TemporaryDirectory():
'''Create a temporary directory (Python 2 wrapper)
'''
tmpdir = tempfile.mkdtemp(prefix='python-appimage-')
debug('MKDIR', tmpdir)
pwd = os.getcwd()
os.chdir(tmpdir)
try:
yield tmpdir
finally:
os.chdir(pwd)
remove_tree(tmpdir)

28
python_appimage/url.py Normal file
View File

@@ -0,0 +1,28 @@
import os
try:
from urllib.request import urlretrieve as _urlretrieve
except ImportError:
import urllib2
_urlretrieve = None
from .log import debug
__all__ = ['urlretrieve']
def urlretrieve(url, filename=None):
'''Download a file to disk
'''
if filename is None:
filename = os.path.basename(url)
debug('DOWNLOAD', '%s from %s', name, os.path.dirname(url))
else:
debug('DOWNLOAD', '%s as %s', url, filename)
if _urlretrieve is None:
data = urllib2.urlopen(url).read()
with open(filename, 'w') as f:
f.write(data)
else:
_urlretrieve(url, filename)