From 4beacdf473b2a0bda78786ea0ad6d9eecfd40762 Mon Sep 17 00:00:00 2001 From: Valentin Niess Date: Fri, 18 Feb 2022 16:11:44 +0100 Subject: [PATCH] Improve python selection when building apps --- docs/docs/apps.md | 77 +++++++++++++++++++++++++-- docs/include/references.md | 4 ++ python_appimage/commands/build/app.py | 19 +++++-- python_appimage/utils/version.py | 7 +++ 4 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 python_appimage/utils/version.py diff --git a/docs/docs/apps.md b/docs/docs/apps.md index ffe42ef..d7ceb97 100644 --- a/docs/docs/apps.md +++ b/docs/docs/apps.md @@ -62,7 +62,7 @@ returns the location of `appimagetool`, if it has been installed. If not, the {{ end(".capsule") }} -## Manylinux Python AppImage +## Manylinux Python AppImages AppImages of your local `python` are unlikely to be portable, except if you run an ancient Linux distribution. Indeed, a core component preventing portability @@ -99,9 +99,78 @@ install found in the `manylinux2014_x86_64` Docker image. ## Simple packaging -The recipe folder contains -the app metadata, a Python requirements file and an entry point script. Examples -of recipes can be found on GitHub in the [applications][APPLICATIONS] folder. +The `python-appimage` utility can also be used in order to build simple +applications, that can be `pip` installed. The syntax is + +```bash +python-appimage build app -p 3.10 /path/to/recipe/folder +``` + +in order to build a Python 3.10 based application from a recipe folder. +Examples of recipes can be found on GitHub in the [applications][APPLICATIONS] +folder. The recipe folder contains: + +- the AppImage metadata (`application.xml` and `application.desktop`), +- an application icon (e.g. `application.png`), +- a Python requirements file (`requirements.txt`) +- an entry point script (`entrypoint.sh`). + +Additional information on metadata can be found in the AppImage documentation. +That is, for [desktop][APPIMAGE_DESKTOP] and [AppStream XML][APPIMAGE_XML] +files. The `requirements.txt` file allows to specify additional site packages +to be bundled in the AppImage, using `pip`. + +!!! Caution + Site packages bundled in the AppImage, as well as their dependencies, must + either be pure python packages, or they must be available as portable binary + wheels. + + If a **C extension** is bundled from **source**, then it will likely **not** + be **portable**, as further discussed in the [Advanced + packaging](#advanced-packaging) section. + +{{ begin(".capsule") }} +### Entry point script + +{% raw %} +The entry point script deserves some additional explanations. This script allows +to customize the startup of your application. A typical `entrypoint.sh` script +would look like + +```bash +{{ python-executable }} ${APPDIR}/opt/python{{ python-version }}/bin/my_app.py "$@" +``` + +where `my_app.py` is the application startup script, installed by `pip`. As can +be seen from the previous example, the `entrypoint.sh` script recognises some +particular variables, nested between double curly braces, `{{ }}`. Those +variables are listed in the table hereafter. In addition, usual [AppImage +environement variables][APPIMAGE_ENV] can be used as well, if needed. For +example, `$APPDIR` points to the AppImage mount point at runtime. +{% endraw %} + +{{ begin("#entrypoint-variables") }} +| variable | Description | +|----------------------|---------------------------------------------------------------| +| `architecture` | The AppImage architecture, e.g. `x86_64`. | +| `linux-tag` | The Manylinux compatibility tag, e.g. `manylinux2014_x86_64`. | +| `python-executable` | Path to the AppImage Python runtime. | +| `python-fullversion` | The Python full version string, e.g. `3.10.2`. | +| `python-tag` | The Python compatibility tag, e.g. `cp310-cp310`. | +| `python-version` | The Python short version string, e.g. `3.10`. | +{{ end("#entrypoint-variables") }} +{{ end(".capsule") }} + +{% raw %} +!!! Note + By default, Python AppImages are not isolated from the user space, nor from + Python specific environment variables, the like `PYTHONPATH`. Depending on + your use case, this can be problematic. + + The runtime isolation level can be changed by adding the `-s` and `-E` + options, when invoking the runtime. For example, + `{{ python-executable }} -sE` starts a fully isolated Python instance. +{% endraw %} ## Advanced packaging diff --git a/docs/include/references.md b/docs/include/references.md index 607fdfb..6a021ba 100644 --- a/docs/include/references.md +++ b/docs/include/references.md @@ -1,6 +1,10 @@ {# References used in the documentation #} [APPIMAGE]: https://appimage.org/ +[APPIMAGE_APPRUN]: https://docs.appimage.org/introduction/software-overview.html#apprun +[APPIMAGE_DESKTOP]: https://docs.appimage.org/reference/desktop-integration.html# +[APPIMAGE_ENV]: https://docs.appimage.org/packaging-guide/environment-variables.html +[APPIMAGE_XML]: https://docs.appimage.org/packaging-guide/optional/appstream.html [APPIMAGETOOL]: https://appimage.github.io/appimagetool/ [APPLICATIONS]: {{ config.repo_url }}tree/master/applications/ [GITHUB]: {{ config.repo_url }} diff --git a/python_appimage/commands/build/app.py b/python_appimage/commands/build/app.py index 495bbcb..e6fa521 100644 --- a/python_appimage/commands/build/app.py +++ b/python_appimage/commands/build/app.py @@ -16,6 +16,7 @@ from ...utils.system import system from ...utils.template import copy_template, load_template from ...utils.tmp import TemporaryDirectory from ...utils.url import urlopen, urlretrieve +from ...utils.version import tonumbers __all__ = ['execute'] @@ -29,6 +30,7 @@ def _unpack_args(args): _tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage') +_linux_pattern = re.compile('manylinux([0-9]+)_' + platform.machine()) def execute(appdir, name=None, python_version=None, linux_tag=None, python_tag=None, base_image=None, in_tree_build=False): @@ -52,7 +54,7 @@ def execute(appdir, name=None, python_version=None, linux_tag=None, continue v = tag[6:] if python_version is None: - if v > version: + if tonumbers(v) > tonumbers(version): release, version = entry, v elif v == python_version: version = python_version @@ -66,18 +68,27 @@ def execute(appdir, name=None, python_version=None, linux_tag=None, # Check for a suitable image + assets = release['assets'] + if linux_tag is None: - linux_tag = 'manylinux1_' + platform.machine() + plat = None + for asset in assets: + match = _linux_pattern.search(asset['name']) + if match: + tmp = str(match.group(1)) + if (plat is None) or (tmp < plat): + plat = tmp + + linux_tag = 'manylinux' + plat + '_' + platform.machine() if python_tag is None: v = ''.join(version.split('.')) python_tag = 'cp{0:}-cp{0:}'.format(v) - if version < '3.8': + if tonumbers(version) < tonumbers('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: diff --git a/python_appimage/utils/version.py b/python_appimage/utils/version.py new file mode 100644 index 0000000..ffd2d88 --- /dev/null +++ b/python_appimage/utils/version.py @@ -0,0 +1,7 @@ +__all__ = ['tonumbers'] + + +def tonumbers(s): + '''Convert a version string to a list of numbers, for comparison + ''' + return [int(v) for v in s.split('.')]