24 Commits

Author SHA1 Message Date
Valentin Niess
08ee36fc45 Update PyPI action 2025-05-19 15:51:03 +02:00
Valentin Niess
41e9b109d0 Bump version: v1.3.1 2025-05-19 15:40:36 +02:00
Valentin Niess
755dd91f45 Tweak full version 2025-05-19 15:39:38 +02:00
Valentin Niess
1a777a00c4 Bump version: v1.3.0 2025-02-10 22:51:41 +01:00
Valentin Niess
fb54370a7e Patch which appimagetool 2025-02-10 22:41:55 +01:00
Valentin Niess
b8c443b27c Merge branch 'jorio-master' 2025-02-10 22:27:16 +01:00
Valentin Niess
72a52b6f34 Select appimagetool version 2025-02-10 18:31:05 +01:00
Iliyas Jorio
583a61686a Migrate to AppImage/appimagetool (fuse3) 2025-02-08 11:47:56 +01:00
Valentin Niess
736f8dcd7c Tweak the updater 2024-10-14 16:15:53 +02:00
Valentin Niess
da067b4831 Fetch deps before patching RPATH 2024-10-14 14:48:16 +02:00
Valentin Niess
efc41b6079 Alternative packages location 2024-10-11 11:44:50 +02:00
Valentin Niess
223f35f757 shutil.copyfile -> shutil.copy 2024-10-11 11:13:40 +02:00
Valentin Niess
bb2e178c2a Merge branch 'mxmlnkn-master' 2024-10-11 11:04:07 +02:00
Valentin Niess
ea671fe7ed Python 2 compat tweaks 2024-10-11 11:03:22 +02:00
Maximilian Knespel
c8fde2906a Replace distutils usage with os and shutil 2024-10-10 15:15:23 +02:00
Sławomir Zborowski
46b2efb359 Added information about extra data in the docs 2024-06-09 22:45:53 +02:00
Valentin Niess
0c66562ad4 Merge branch 'dotzborro-aux-files' 2024-05-22 09:37:03 +02:00
Valentin Niess
03bab9b38d Multiple extra data 2024-05-22 09:34:57 +02:00
Sławomir Zborowski
a6d0da5f0b Applied review remarks 2024-05-07 13:15:56 +02:00
Sławomir Zborowski
77ae6c7d55 Added support for bundling in auxilliary files 2024-04-28 22:48:00 +02:00
Valentin Niess
9de84d8b22 Remove 2.7 from PyPI test 2024-04-27 10:31:28 +02:00
Valentin Niess
899f40102a Bump version to 1.2.6 2024-04-27 10:19:59 +02:00
Valentin Niess
48b28af040 Merge branch 'dotzborro-git-dependencies-with-branch-name' 2024-04-27 10:10:51 +02:00
Sławomir Zborowski
955149ad6a Added support for git dependencies with custom branch name. 2024-04-27 00:08:40 +02:00
13 changed files with 194 additions and 48 deletions

View File

@@ -5,17 +5,23 @@ on:
- master - master
paths: paths:
- 'VERSION' - 'VERSION'
workflow_dispatch:
inputs:
upload:
description: 'Upload to PyPI'
required: true
type: boolean
jobs: jobs:
Test: Test:
runs-on: ubuntu-20.04 runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
version: ['2.7', '3.9'] version: ['3.11']
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-python@v1 - uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.version }} python-version: ${{ matrix.version }}
@@ -31,10 +37,10 @@ jobs:
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-python@v1 - uses: actions/setup-python@v5
with: with:
python-version: '3.9' python-version: '3.11'
- name: Build wheel - name: Build wheel
run: | run: |
@@ -43,7 +49,7 @@ jobs:
python setup.py bdist_wheel --universal python setup.py bdist_wheel --universal
- name: Upload to PyPI - name: Upload to PyPI
if: github.ref == 'refs/heads/master' if: (github.ref == 'refs/heads/master') && inputs.upload
uses: pypa/gh-action-pypi-publish@master uses: pypa/gh-action-pypi-publish@master
with: with:
password: ${{ secrets.PYPI_TOKEN }} password: ${{ secrets.PYPI_TOKEN }}

View File

@@ -1 +1 @@
1.2.5 1.3.1

View File

@@ -182,6 +182,36 @@ example, `$APPDIR` points to the AppImage mount point at runtime.
`{{ python-executable }} -I` starts a fully isolated Python instance. `{{ python-executable }} -I` starts a fully isolated Python instance.
{% endraw %} {% endraw %}
### Bundling data files
`python-appimage` is also capable of bundling in auxilliary data files directly
into the resulting AppImage. `-x/--extra-data` switch exists for that task.
Consider following example.
```bash
echo -n "foo" > foo
mkdir bar
echo -n "baz" > bar/baz
python-appimage [your regular parameters] -x foo bar/*
```
User data included in such a way becomes accessible to the Python code
contained within the AppImage in a form of regular files under the directory
pointed to by `APPDIR` environment variable. Example of Python 3 script
that reads these exemplary files is presented below.
```python
import os, pathlib
for fileName in ("foo", "baz"):
print((pathlib.Path(os.getenv("APPDIR")) / fileName).read_text())
```
Above code, when executed, would print following output.
```bash
foo
baz
```
## Advanced packaging ## Advanced packaging

View File

@@ -7,6 +7,11 @@ import sys
__all__ = ['main'] __all__ = ['main']
def exists(path):
if not os.path.exists(path):
raise argparse.ArgumentTypeError("could not find: {}".format(path))
return os.path.abspath(path)
def main(): def main():
'''Entry point for the CLI '''Entry point for the CLI
''' '''
@@ -22,6 +27,8 @@ def main():
help='Command to execute', help='Command to execute',
dest='command') dest='command')
parser.add_argument('-a', '--appimagetool-version',
help='set appimagetool version')
parser.add_argument('-q', '--quiet', help='disable logging', parser.add_argument('-q', '--quiet', help='disable logging',
dest='verbosity', action='store_const', const='ERROR') dest='verbosity', action='store_const', const='ERROR')
parser.add_argument('-v', '--verbose', help='print extra information', parser.add_argument('-v', '--verbose', help='print extra information',
@@ -73,6 +80,8 @@ def main():
help='force pip in-tree-build', help='force pip in-tree-build',
action='store_true', action='store_true',
default=False) default=False)
build_app_parser.add_argument('-x', '--extra-data', type=exists,
help='extra application data (bundled under $APPDIR/)', nargs='+')
list_parser = subparsers.add_parser('list', list_parser = subparsers.add_parser('list',
description='List Python versions installed in a manylinux image') description='List Python versions installed in a manylinux image')
@@ -91,6 +100,10 @@ def main():
from .utils import log from .utils import log
log.set_level(args.verbosity) log.set_level(args.verbosity)
if args.appimagetool_version:
from .utils import deps
deps.APPIMAGETOOL_VERSION = args.appimagetool_version
# check if no arguments are passed # check if no arguments are passed
if args.command is None: if args.command is None:
parser.print_help() parser.print_help()

View File

@@ -5,7 +5,7 @@ import subprocess
import sys import sys
from ..utils.compat import decode from ..utils.compat import decode
from ..utils.deps import APPIMAGETOOL, ensure_appimagetool from ..utils.deps import ensure_appimagetool
from ..utils.docker import docker_run from ..utils.docker import docker_run
from ..utils.fs import copy_tree from ..utils.fs import copy_tree
from ..utils.log import debug, log from ..utils.log import debug, log
@@ -22,10 +22,10 @@ def build_appimage(appdir=None, destination=None):
appdir = 'AppDir' appdir = 'AppDir'
log('BUILD', appdir) log('BUILD', appdir)
ensure_appimagetool() appimagetool = ensure_appimagetool()
arch = platform.machine() arch = platform.machine()
cmd = ['ARCH=' + arch, APPIMAGETOOL, '--no-appstream', appdir] cmd = ['ARCH=' + arch, appimagetool, '--no-appstream', appdir]
if destination is not None: if destination is not None:
cmd.append(destination) cmd.append(destination)
cmd = ' '.join(cmd) cmd = ' '.join(cmd)
@@ -45,7 +45,8 @@ def build_appimage(appdir=None, destination=None):
elif out: elif out:
out = out.replace('%', '%%')[:-1] out = out.replace('%', '%%')[:-1]
for line in out.split(os.linesep): for line in out.split(os.linesep):
if line.startswith('WARNING'): if line.startswith('WARNING') and \
not line[9:].startswith('zsyncmake command is missing'):
log('WARNING', line[9:]) log('WARNING', line[9:])
elif line.startswith('Error'): elif line.startswith('Error'):
raise RuntimeError(line) raise RuntimeError(line)

View File

@@ -94,6 +94,8 @@ def patch_binary(path, libdir, recursive=True):
else: else:
excluded = _excluded_libs excluded = _excluded_libs
deps = ldd(path) # Fetch deps before patching RPATH.
ensure_patchelf() ensure_patchelf()
rpath = '\'' + system((PATCHELF, '--print-rpath', path)) + '\'' rpath = '\'' + system((PATCHELF, '--print-rpath', path)) + '\''
relpath = os.path.relpath(libdir, os.path.dirname(path)) relpath = os.path.relpath(libdir, os.path.dirname(path))
@@ -102,7 +104,6 @@ def patch_binary(path, libdir, recursive=True):
if rpath != expected: if rpath != expected:
system((PATCHELF, '--set-rpath', expected, path)) system((PATCHELF, '--set-rpath', expected, path))
deps = ldd(path)
for dep in deps: for dep in deps:
name = os.path.basename(dep) name = os.path.basename(dep)
if name in excluded: if name in excluded:
@@ -171,11 +172,11 @@ def relocate_python(python=None, appdir=None):
# Set some key variables & paths # Set some key variables & paths
if python: if python:
FULLVERSION = system((python, '-c', FULLVERSION = system((python, '-c', '"import sys; print(sys.version)"'))
'"import sys; print(\'{:}.{:}.{:}\'.format(*sys.version_info[:3]))"'))
FULLVERSION = FULLVERSION.strip() FULLVERSION = FULLVERSION.strip()
else: else:
FULLVERSION = '{:}.{:}.{:}'.format(*sys.version_info[:3]) FULLVERSION = sys.version
FULLVERSION = FULLVERSION.split(None, 1)[0]
VERSION = '.'.join(FULLVERSION.split('.')[:2]) VERSION = '.'.join(FULLVERSION.split('.')[:2])
PYTHON_X_Y = 'python' + VERSION PYTHON_X_Y = 'python' + VERSION
PIP_X_Y = 'pip' + VERSION PIP_X_Y = 'pip' + VERSION
@@ -202,9 +203,21 @@ def relocate_python(python=None, appdir=None):
PYTHON_LIB = PYTHON_PREFIX + '/lib' PYTHON_LIB = PYTHON_PREFIX + '/lib'
PYTHON_PKG = PYTHON_LIB + '/' + PYTHON_X_Y PYTHON_PKG = PYTHON_LIB + '/' + PYTHON_X_Y
if not os.path.exists(HOST_PKG):
paths = glob.glob(HOST_PKG + '*')
if paths:
HOST_PKG = paths[0]
PYTHON_PKG = PYTHON_LIB + '/' + os.path.basename(HOST_PKG)
else:
raise ValueError('could not find {0:}'.format(HOST_PKG))
if not os.path.exists(HOST_INC): if not os.path.exists(HOST_INC):
HOST_INC += 'm' paths = glob.glob(HOST_INC + '*')
PYTHON_INC += 'm' if paths:
HOST_INC = paths[0]
PYTHON_INC = PYTHON_INC + '/' + os.path.basename(HOST_INC)
else:
raise ValueError('could not find {0:}'.format(HOST_INC))
# Copy the running Python's install # Copy the running Python's install

View File

@@ -26,14 +26,16 @@ def _unpack_args(args):
'''Unpack command line arguments '''Unpack command line arguments
''' '''
return args.appdir, args.name, args.python_version, args.linux_tag, \ return args.appdir, args.name, args.python_version, args.linux_tag, \
args.python_tag, args.base_image, args.in_tree_build args.python_tag, args.base_image, args.in_tree_build, \
args.extra_data
_tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage') _tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage')
_linux_pattern = re.compile('manylinux([0-9]+)_' + platform.machine()) _linux_pattern = re.compile('manylinux([0-9]+)_' + platform.machine())
def execute(appdir, name=None, python_version=None, linux_tag=None, def execute(appdir, name=None, python_version=None, linux_tag=None,
python_tag=None, base_image=None, in_tree_build=False): python_tag=None, base_image=None, in_tree_build=False,
extra_data=None):
'''Build a Python application using a base AppImage '''Build a Python application using a base AppImage
''' '''
@@ -253,6 +255,12 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
'WARNING: Running pip as' 'WARNING: Running pip as'
) )
git_warnings = (
re.compile(r'\s+Running command git (clone|checkout) '),
re.compile(r"\s+Branch '.*' set up to track remote"),
re.compile(r"\s+Switched to a new branch '.*'"),
)
isolation_flag = '-sE' if python_version[0] == '2' else '-I' isolation_flag = '-sE' if python_version[0] == '2' else '-I'
system(('./AppDir/AppRun', isolation_flag, '-m', 'pip', 'install', '-U', in_tree_build, system(('./AppDir/AppRun', isolation_flag, '-m', 'pip', 'install', '-U', in_tree_build,
'--no-warn-script-location', 'pip'), exclude=deprecation) '--no-warn-script-location', 'pip'), exclude=deprecation)
@@ -279,8 +287,17 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
log('BUNDLE', requirement) log('BUNDLE', requirement)
system(('./AppDir/AppRun', isolation_flag, '-m', 'pip', 'install', '-U', in_tree_build, system(('./AppDir/AppRun', isolation_flag, '-m', 'pip', 'install', '-U', in_tree_build,
'--no-warn-script-location', requirement), '--no-warn-script-location', requirement),
exclude=(deprecation, ' Running command git clone')) exclude=(deprecation + git_warnings))
# Bundle auxilliary application data
if extra_data is not None:
for path in extra_data:
basename = os.path.basename(path)
log('BUNDLE', basename)
if os.path.isdir(path):
copy_tree(path, 'AppDir/' + basename)
else:
copy_file(path, 'AppDir/')
# Bundle the entry point # Bundle the entry point
entrypoint_path = glob.glob(appdir + '/entrypoint.*') entrypoint_path = glob.glob(appdir + '/entrypoint.*')

View File

@@ -16,6 +16,9 @@ def _unpack_args(args):
def execute(binary): def execute(binary):
'''Print the location of a binary dependency '''Print the location of a binary dependency
''' '''
path = os.path.join(os.path.dirname(deps.PATCHELF), binary) if binary == 'appimagetool':
path = deps.ensure_appimagetool(dry=True)
else:
path = os.path.join(os.path.dirname(deps.PATCHELF), binary)
if os.path.exists(path): if os.path.exists(path):
print(path) print(path)

View File

@@ -19,29 +19,47 @@ _ARCH = platform.machine()
PREFIX = os.path.abspath(os.path.dirname(__file__) + '/..') PREFIX = os.path.abspath(os.path.dirname(__file__) + '/..')
'''Package installation prefix''' '''Package installation prefix'''
APPIMAGETOOL = os.path.expanduser('~/.local/bin/appimagetool') APPIMAGETOOL_DIR = os.path.expanduser('~/.local/bin')
'''Location of the appimagetool binary''' '''Location of the appimagetool binary'''
APPIMAGETOOL_VERSION = '12'
'''Version of the appimagetool binary'''
EXCLUDELIST = PREFIX + '/data/excludelist' EXCLUDELIST = PREFIX + '/data/excludelist'
'''AppImage exclusion list''' '''AppImage exclusion list'''
PATCHELF = os.path.expanduser('~/.local/bin/patchelf') PATCHELF = os.path.expanduser('~/.local/bin/patchelf')
'''Location of the PatchELF binary''' '''Location of the PatchELF binary'''
def ensure_appimagetool(dry=False):
def ensure_appimagetool():
'''Fetch appimagetool from the web if not available locally '''Fetch appimagetool from the web if not available locally
''' '''
if os.path.exists(APPIMAGETOOL):
return False
if APPIMAGETOOL_VERSION == '12':
appimagetool_name = 'appimagetool'
else:
appimagetool_name = 'appimagetool-' + APPIMAGETOOL_VERSION
appimagetool = os.path.join(APPIMAGETOOL_DIR, appimagetool_name)
appdir_name = '.'.join(('', appimagetool_name, 'appdir', _ARCH))
appdir = os.path.join(APPIMAGETOOL_DIR, appdir_name)
apprun = os.path.join(appdir, 'AppRun')
if dry or os.path.exists(apprun):
return apprun
appimage = 'appimagetool-{0:}.AppImage'.format(_ARCH) appimage = 'appimagetool-{0:}.AppImage'.format(_ARCH)
baseurl = 'https://github.com/AppImage/AppImageKit/releases/' \
'download/12' if APPIMAGETOOL_VERSION in map(str, range(1, 14)):
repository = 'AppImageKit'
else:
repository = 'appimagetool'
baseurl = os.path.join(
'https://github.com/AppImage',
repository,
'releases/download',
APPIMAGETOOL_VERSION
)
log('INSTALL', 'appimagetool from %s', baseurl) 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): if not os.path.exists(appdir):
make_tree(os.path.dirname(appdir)) make_tree(os.path.dirname(appdir))
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
@@ -50,10 +68,7 @@ def ensure_appimagetool():
system(('./' + appimage, '--appimage-extract')) system(('./' + appimage, '--appimage-extract'))
copy_tree('squashfs-root', appdir) copy_tree('squashfs-root', appdir)
if not os.path.exists(APPIMAGETOOL): return apprun
os.symlink(appdir_name + '/AppRun', APPIMAGETOOL)
return True
# Installers for dependencies # Installers for dependencies

View File

@@ -1,8 +1,30 @@
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 errno
import os import os
try:
from distutils.dir_util import mkpath as _mkpath
from distutils.dir_util import remove_tree as _remove_tree
from distutils.file_util import copy_file as _copy_file
except ImportError:
import shutil
def _mkpath(path):
os.makedirs(path, exist_ok=True)
def _remove_tree(path):
shutil.rmtree(path)
def _copy_file(source, destination, update=0):
if os.path.exists(source) and (
not update
or (
(not os.path.exists(destination))
or (os.path.getmtime(source) > os.path.getmtime(destination))
)
):
shutil.copy(source, destination)
from .log import debug from .log import debug

View File

@@ -41,9 +41,16 @@ def system(args, exclude=None, stdin=None):
if err: if err:
err = decode(err) err = decode(err)
stripped = [line for line in err.split(os.linesep) if line] stripped = [line for line in err.split(os.linesep) if line]
def matches_pattern(line, pattern):
if isinstance(pattern, re.Pattern):
return bool(pattern.match(line))
return line.startswith(pattern)
for pattern in exclude: for pattern in exclude:
stripped = [line for line in stripped stripped = [line for line in stripped
if not line.startswith(pattern)] if not matches_pattern(line, pattern)]
if stripped: if stripped:
# Tolerate single line warning(s) # Tolerate single line warning(s)
for line in stripped: for line in stripped:

View File

@@ -20,5 +20,6 @@ def TemporaryDirectory():
try: try:
yield tmpdir yield tmpdir
finally: finally:
debug('REMOVE', tmpdir)
os.chdir(pwd) os.chdir(pwd)
remove_tree(tmpdir) remove_tree(tmpdir)

View File

@@ -233,20 +233,30 @@ def update(args):
for meta in new_assets: for meta in new_assets:
release = releases[meta.release_tag()].release release = releases[meta.release_tag()].release
appimage = meta.appimage_name() appimage = meta.appimage_name()
new_asset = release.upload_asset( if meta.asset and (meta.asset.name == appimage):
path = f'{APPIMAGES_DIR}/{appimage}',
name = appimage
)
if meta.asset:
meta.asset.delete_asset() meta.asset.delete_asset()
update_summary.append( update_summary.append(
f'- update {meta.formated_tag()}/{meta.abi} ' f'- update {meta.formated_tag()}/{meta.abi} {meta.version}'
f'{meta.previous_version()} -> {meta.version}' )
new_asset = release.upload_asset(
path = f'{APPIMAGES_DIR}/{appimage}',
name = appimage
) )
else: else:
update_summary.append( new_asset = release.upload_asset(
f'- add {meta.formated_tag()}/{meta.abi} {meta.version}' path = f'{APPIMAGES_DIR}/{appimage}',
name = appimage
) )
if meta.asset:
meta.asset.delete_asset()
update_summary.append(
f'- update {meta.formated_tag()}/{meta.abi} '
f'{meta.previous_version()} -> {meta.version}'
)
else:
update_summary.append(
f'- add {meta.formated_tag()}/{meta.abi} {meta.version}'
)
meta.asset = new_asset meta.asset = new_asset
assets[meta.tag][meta.abi] = meta assets[meta.tag][meta.abi] = meta
@@ -300,6 +310,10 @@ if __name__ == '__main__':
action = 'store_true', action = 'store_true',
default = False default = False
) )
parser.add_argument('-m', '--manylinux',
help = 'target specific manylinux tags',
nargs = "+"
)
parser.add_argument("-s", "--sha", parser.add_argument("-s", "--sha",
help = "reference commit SHA" help = "reference commit SHA"
) )
@@ -308,5 +322,9 @@ if __name__ == '__main__':
) )
args = parser.parse_args() args = parser.parse_args()
if args.manylinux:
MANYLINUSES = args.manylinux
sys.argv = sys.argv[:1] # Empty args for fake call sys.argv = sys.argv[:1] # Empty args for fake call
update(args) update(args)