mirror of
https://github.com/niess/python-appimage.git
synced 2026-03-15 21:00:12 +01:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f3e9165b |
103
.github/workflows/appimage.yml
vendored
103
.github/workflows/appimage.yml
vendored
@@ -1,36 +1,89 @@
|
|||||||
name: AppImage
|
name: AppImage
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
branches:
|
||||||
dry:
|
- master
|
||||||
description: 'Dry run'
|
paths:
|
||||||
required: true
|
- '.github/workflows/appimage.yml'
|
||||||
type: boolean
|
- 'python_appimage/**'
|
||||||
all:
|
|
||||||
description: 'Update all'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * 0'
|
- cron: '0 3 * * 0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Update:
|
Build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
strategy:
|
||||||
contents: write
|
matrix:
|
||||||
|
image: ['1', '2010', '2014']
|
||||||
|
arch: [x86_64, i686]
|
||||||
|
tag: [cp27-cp27m, cp27-cp27mu, cp35-cp35m, cp36-cp36m, cp37-cp37m,
|
||||||
|
cp38-cp38, cp39-cp39, cp310-cp310, cp311-cp311]
|
||||||
|
exclude:
|
||||||
|
- image: '1'
|
||||||
|
tag: cp310-cp310
|
||||||
|
- image: '1'
|
||||||
|
tag: cp311-cp311
|
||||||
|
- image: '2010'
|
||||||
|
tag: cp27-cp27m
|
||||||
|
- image: '2010'
|
||||||
|
tag: cp27-cp27mu
|
||||||
|
- image: '2010'
|
||||||
|
tag: cp35-cp35m
|
||||||
|
- image: '2014'
|
||||||
|
tag: cp27-cp27m
|
||||||
|
- image: '2014'
|
||||||
|
tag: cp27-cp27mu
|
||||||
|
- image: '2014'
|
||||||
|
tag: cp35-cp35m
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Build
|
||||||
run: pip install PyGithub requests
|
|
||||||
|
|
||||||
- name: Run updater
|
|
||||||
run: |
|
|
||||||
./scripts/update-appimages.py \
|
|
||||||
--token=${{ secrets.GITHUB_TOKEN }} \
|
|
||||||
--sha=${{ github.sha }} \
|
|
||||||
${{ inputs.all && '--all' || '' }} \
|
|
||||||
${{ inputs.dry && '--dry' || '' }}
|
|
||||||
env:
|
env:
|
||||||
PYTHONPATH: ${{ github.workspace }}
|
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
||||||
|
run: |
|
||||||
|
# Build the AppImage
|
||||||
|
python -m python_appimage build manylinux \
|
||||||
|
${{ matrix.image }}_${{ matrix.arch }} \
|
||||||
|
${{ matrix.tag }}
|
||||||
|
|
||||||
|
# Export the AppImage name and the Python version
|
||||||
|
appimage=$(ls python*.AppImage)
|
||||||
|
SCRIPT=$(cat <<-END
|
||||||
|
version = '${appimage}'[6:].split('.', 2)
|
||||||
|
print('{:}.{:}'.format(*version[:2]))
|
||||||
|
END
|
||||||
|
)
|
||||||
|
version=$(python -c "${SCRIPT}")
|
||||||
|
|
||||||
|
echo "::set-env name=PYTHON_APPIMAGE::${appimage}"
|
||||||
|
echo "::set-env name=PYTHON_VERSION::${version}"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v1
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
with:
|
||||||
|
name: python${{ env.PYTHON_VERSION }}-appimages
|
||||||
|
path: ${{ env.PYTHON_APPIMAGE }}
|
||||||
|
|
||||||
|
Release:
|
||||||
|
needs: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v1
|
||||||
|
with:
|
||||||
|
name: python${{ matrix.version }}-appimages
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: marvinpinto/action-automatic-releases@latest
|
||||||
|
with:
|
||||||
|
automatic_release_tag: python${{ matrix.version }}
|
||||||
|
title: Python ${{ matrix.version }}
|
||||||
|
files: |
|
||||||
|
python${{ matrix.version }}-appimages/python*.AppImage
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
45
.github/workflows/applications.yml
vendored
45
.github/workflows/applications.yml
vendored
@@ -1,42 +1,27 @@
|
|||||||
name: Applications
|
name: Applications
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
branches:
|
||||||
scipy:
|
- master
|
||||||
required: true
|
paths:
|
||||||
default: true
|
- '.github/workflows/applications.yml'
|
||||||
type: boolean
|
- 'applications/**'
|
||||||
tasmotizer:
|
- 'python_appimage/**'
|
||||||
required: true
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
xonsh:
|
|
||||||
required: true
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
ssh-mitm:
|
|
||||||
required: true
|
|
||||||
default: true
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Test:
|
Test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
version: ['3.9']
|
version: ['2.7', '3.7', '3.9']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.version }}
|
python-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: pip install requests
|
|
||||||
|
|
||||||
- name: Test scipy
|
- name: Test scipy
|
||||||
if: ${{ inputs.scipy }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m python_appimage build app applications/scipy \
|
python -m python_appimage build app applications/scipy \
|
||||||
--python-version=2.7 \
|
--python-version=2.7 \
|
||||||
@@ -45,24 +30,20 @@ jobs:
|
|||||||
./scipy-x86_64.AppImage -c 'import numpy, pandas, scipy'
|
./scipy-x86_64.AppImage -c 'import numpy, pandas, scipy'
|
||||||
|
|
||||||
- name: Test tasmotizer
|
- name: Test tasmotizer
|
||||||
if: ${{ inputs.tasmotizer }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m python_appimage build app applications/tasmotizer \
|
python -m python_appimage build app applications/tasmotizer \
|
||||||
--linux-tag=manylinux1_x86_64 \
|
--linux-tag=manylinux2014_x86_64
|
||||||
--python-version=3.9
|
|
||||||
test -e tasmotizer-x86_64.AppImage
|
test -e tasmotizer-x86_64.AppImage
|
||||||
|
|
||||||
- name: Test xonsh
|
- name: Test xonsh
|
||||||
if: ${{ inputs.xonsh }}
|
|
||||||
run: |
|
run: |
|
||||||
python -m python_appimage build app applications/xonsh
|
python -m python_appimage build app applications/xonsh
|
||||||
test -e xonsh-x86_64.AppImage
|
test -e xonsh-x86_64.AppImage
|
||||||
./xonsh-x86_64.AppImage -c 'import xonsh'
|
./xonsh-x86_64.AppImage -c 'import xonsh'
|
||||||
|
|
||||||
- name: Test ssh-mitm
|
- name: Test ssh-mitm
|
||||||
if: ${{ inputs.ssh-mitm }}
|
if: ${{ matrix.version == '3.9' }}
|
||||||
run: |
|
run: |
|
||||||
python -m python_appimage build app applications/ssh-mitm \
|
python -m python_appimage build app applications/ssh-mitm
|
||||||
--python-version=3.11
|
|
||||||
test -e ssh-mitm-x86_64.AppImage
|
test -e ssh-mitm-x86_64.AppImage
|
||||||
./ssh-mitm-x86_64.AppImage --help
|
./ssh-mitm-x86_64.AppImage --help
|
||||||
|
|||||||
13
.github/workflows/delete-artifacts.yml
vendored
Normal file
13
.github/workflows/delete-artifacts.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: Delete artifacts
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete-artifacts:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: kolpav/purge-artifacts-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
expire-in: 0days
|
||||||
36
.github/workflows/pypi.yml
vendored
36
.github/workflows/pypi.yml
vendored
@@ -1,29 +1,24 @@
|
|||||||
name: PyPI
|
name: PyPI
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
push:
|
||||||
inputs:
|
branches:
|
||||||
upload:
|
- master
|
||||||
description: 'Upload to PyPI'
|
paths:
|
||||||
required: true
|
- 'VERSION'
|
||||||
default: false
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Test:
|
Test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
version: ['3.11']
|
version: ['2.7', '3.9']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.version }}
|
python-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: pip install requests
|
|
||||||
|
|
||||||
- name: Test local builder
|
- name: Test local builder
|
||||||
run: |
|
run: |
|
||||||
python -m python_appimage build local -p $(which python) \
|
python -m python_appimage build local -p $(which python) \
|
||||||
@@ -36,18 +31,19 @@ jobs:
|
|||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.9'
|
||||||
|
|
||||||
- name: Build wheel
|
- name: Build wheel
|
||||||
run: |
|
run: |
|
||||||
pip install -U pip build
|
pip install -U pip
|
||||||
python -m build
|
pip install -U wheel
|
||||||
|
python setup.py bdist_wheel --universal
|
||||||
|
|
||||||
- name: Upload to PyPI
|
- name: Upload to PyPI
|
||||||
if: (github.ref == 'refs/heads/master') && inputs.upload
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
with:
|
with:
|
||||||
password: ${{ secrets.PYPI_TOKEN }}
|
password: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,6 +5,8 @@ __pycache__
|
|||||||
AppDir
|
AppDir
|
||||||
build/*
|
build/*
|
||||||
dist
|
dist
|
||||||
docs/ENV
|
|
||||||
python_appimage.egg-info
|
python_appimage.egg-info
|
||||||
|
python_appimage/bin
|
||||||
|
python_appimage/data/excludelist
|
||||||
|
python_appimage/version.py
|
||||||
!python_appimage/commands/build
|
!python_appimage/commands/build
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
build:
|
|
||||||
os: ubuntu-22.04
|
|
||||||
tools:
|
|
||||||
python: "3.9"
|
|
||||||
|
|
||||||
mkdocs:
|
mkdocs:
|
||||||
configuration: docs/mkdocs.yml
|
configuration: docs/mkdocs.yml
|
||||||
|
|
||||||
python:
|
python:
|
||||||
|
version: 3.8
|
||||||
install:
|
install:
|
||||||
- requirements: docs/requirements.txt
|
- requirements: docs/requirements.txt
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
include python_appimage/data/*
|
|
||||||
29
README.md
29
README.md
@@ -5,18 +5,18 @@ _Ready to use AppImages of Python are available as GitHub [releases][RELEASES]._
|
|||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
We provide relocatable Python runtimes in the form of [AppImages][APPIMAGE] for
|
We provide relocatable Python runtimes as [AppImages][APPIMAGE] for Linux
|
||||||
Linux systems. These runtimes are extracted from [Manylinux][MANYLINUX] Docker
|
systems. These runtimes are extracted from [Manylinux][MANYLINUX] Docker images,
|
||||||
images and are available as GitHub [releases][RELEASES]. Our Python AppImages
|
and they are available as GitHub [releases][RELEASES]. Our Python AppImages are
|
||||||
are updated weekly, on every Sunday.
|
updated weekly, on every Sunday.
|
||||||
|
|
||||||
Instructions for _installing_ and running _Python AppImages_ can be found on
|
Instructions for _installing_ and running _Python AppImages_ are provided on
|
||||||
[Read the Docs][READTHEDOCS].
|
[Read the Docs][READTHEDOCS].
|
||||||
|
|
||||||
The online documentation also describes the [`python-appimage`][PYPI] utility
|
In addition, the online documentation describes the [`python-appimage`][PYPI]
|
||||||
for application developers. This utility can facilitate the development of
|
utility, for application developers. This utility can facilitate the building of
|
||||||
Python applications, provided you have an existing Python AppImage and a recipe
|
Python apps, given an existing Python AppImage and a recipe folder.
|
||||||
folder. [Examples][APPLICATIONS] of recipes are available on GitHub.
|
[Examples][APPLICATIONS] of recipes are available from GitHub.
|
||||||
|
|
||||||
|
|
||||||
## Projects using [`python-appimage`][GITHUB]
|
## Projects using [`python-appimage`][GITHUB]
|
||||||
@@ -31,22 +31,11 @@ folder. [Examples][APPLICATIONS] of recipes are available on GitHub.
|
|||||||
through the ssh
|
through the ssh
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
The [`python-appimage`][PYPI] package (**A**) is under the GNU GPLv3 license,
|
|
||||||
except for files located under `python_appimage/data` which are MIT licensed.
|
|
||||||
Thus, the produced Manylinux Python AppImages (**B**) are not GPL'd. They
|
|
||||||
contain a CPython distribution that is (mostly) under the [PSF
|
|
||||||
license][PSF_LICENSE]. Other parts of **B** (e.g. `AppRun`) are under the MIT
|
|
||||||
license.
|
|
||||||
|
|
||||||
|
|
||||||
[APPLICATIONS]: https://github.com/niess/python-appimage/tree/master/applications
|
[APPLICATIONS]: https://github.com/niess/python-appimage/tree/master/applications
|
||||||
[APPIMAGE]: https://appimage.org/
|
[APPIMAGE]: https://appimage.org/
|
||||||
[GITHUB]: https://github.com/niess/python-appimage
|
[GITHUB]: https://github.com/niess/python-appimage
|
||||||
[GRAND]: http://grand.cnrs.fr
|
[GRAND]: http://grand.cnrs.fr
|
||||||
[MANYLINUX]: https://github.com/pypa/manylinux
|
[MANYLINUX]: https://github.com/pypa/manylinux
|
||||||
[PSF_LICENSE]: https://docs.python.org/3/license.html#psf-license
|
|
||||||
[PYPI]: https://pypi.org/project/python-appimage/
|
[PYPI]: https://pypi.org/project/python-appimage/
|
||||||
[READTHEDOCS]: https://python-appimage.readthedocs.io/en/latest/
|
[READTHEDOCS]: https://python-appimage.readthedocs.io/en/latest/
|
||||||
[RELEASES]: https://github.com/niess/python-appimage/releases
|
[RELEASES]: https://github.com/niess/python-appimage/releases
|
||||||
|
|||||||
@@ -12,10 +12,8 @@
|
|||||||
[GITHUB]: {{ config.repo_url }}
|
[GITHUB]: {{ config.repo_url }}
|
||||||
[LINUXDEPLOY]: https://github.com/linuxdeploy/linuxdeploy/
|
[LINUXDEPLOY]: https://github.com/linuxdeploy/linuxdeploy/
|
||||||
[MANYLINUX]: https://github.com/pypa/manylinux/
|
[MANYLINUX]: https://github.com/pypa/manylinux/
|
||||||
[NUMPY]: https://numpy.org/
|
|
||||||
[PATCHELF]: https://github.com/NixOS/patchelf/
|
[PATCHELF]: https://github.com/NixOS/patchelf/
|
||||||
[PEP_425]: https://peps.python.org/pep-0425/
|
[PYPI]: https://pypi.org/project/python-appimaAge/
|
||||||
[PYPI]: https://pypi.org/project/python-appimage/
|
|
||||||
[RELEASES]: {{ config.repo_url }}releases/
|
[RELEASES]: {{ config.repo_url }}releases/
|
||||||
[SHEBANG]: https://en.wikipedia.org/wiki/Shebang_(Unix)/
|
[SHEBANG]: https://en.wikipedia.org/wiki/Shebang_(Unix)/
|
||||||
[VENV]: https://docs.python.org/3/library/venv.html/
|
[VENV]: https://docs.python.org/3/library/venv.html/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ docs_dir: src
|
|||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Python AppImages: index.md
|
- Python AppImages: index.md
|
||||||
- Developers' corner: apps.md
|
- Developers corner: apps.md
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: readthedocs
|
name: readthedocs
|
||||||
|
|||||||
242
docs/src/apps.md
242
docs/src/apps.md
@@ -10,169 +10,150 @@
|
|||||||
{% include "references.md" %}
|
{% include "references.md" %}
|
||||||
|
|
||||||
|
|
||||||
# Developers' corner
|
# Developers corner
|
||||||
|
|
||||||
Python [AppImages][APPIMAGE] are created using the `python-appimage` utility,
|
Python [AppImages][APPIMAGE] are built with the `python-appimage` utility,
|
||||||
which is available on [PyPI][PYPI]. This utility can also be used to package
|
available from [PyPI][PYPI]. This utility can also help packaging Python based
|
||||||
Python-based applications as AppImages using an existing AppImage and a recipe
|
applications as AppImages, using an existing Python AppImage and a recipe
|
||||||
folder.
|
folder.
|
||||||
|
|
||||||
!!! Caution
|
!!! Caution
|
||||||
The `python-appimage` utility can only package applications that can be
|
The `python-appimage` utility can only package applications that can be
|
||||||
installed directly with `pip`. For more advanced usage, it is necessary to
|
directly installed with `pip`. For more advanced usage, one needs to extract
|
||||||
extract and edit the Python AppImage, as explained in the [Advanced
|
the Python AppImage and to edit it, e.g. as explained in the [Advanced
|
||||||
installation](index.md#advanced-installation) section. Further details on
|
installation](index.md#advanced-installation) section. Additional details
|
||||||
this use case can be found [below](#advanced-packaging).
|
on this use case are provided [below](#advanced-packaging).
|
||||||
|
|
||||||
|
|
||||||
## Building a Python AppImage
|
## Building a Python AppImage
|
||||||
|
|
||||||
The primary purpose of `python-appimage` is to relocate an existing Python
|
The primary scope of `python-appimage` is to relocate an existing Python
|
||||||
installation to an AppDir and build the corresponding AppImage. For example, the
|
installation inside an AppDir, and to build the corresponding AppImage. For
|
||||||
command
|
example, the following
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python-appimage build local -p $(which python2)
|
python-appimage build local -p $(which python2)
|
||||||
```
|
```
|
||||||
|
|
||||||
should create an AppImage of your local Python installation, provided that it
|
should build an AppImage of your local Python 2 installation, provided that it
|
||||||
exists.
|
exists.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
Help on the available arguments and options for `python-appimage` can be
|
Help on available arguments and options to `python-appimage` can be obtained
|
||||||
obtained by using the `-h` flag. For example, running
|
with the `-h` flag. For example, `python-appimage build local -h` provides
|
||||||
`python-appimage build local -h` provides help on local builds.
|
help on local builds.
|
||||||
|
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### Auxiliary tools
|
### Auxiliary tools
|
||||||
|
|
||||||
The `python-appimage` utility relies on auxiliary tools that are downloaded and
|
The `python-appimage` utility relies on auxiliary tools that are downloaded and
|
||||||
installed on demand during application execution. These are
|
installed at runtime, on need. Those are [appimagetool][APPIMAGETOOL] for
|
||||||
[appimagetool][APPIMAGETOOL], which is used to build AppImages, and
|
building AppImages, and [patchelf][PATCHELF] in order to edit ELFs runtime paths
|
||||||
[patchelf][PATCHELF], which is used to edit runtime paths (`RPATH`) in ELF
|
(`RPATH`). Auxiliary tools are installed to the the user space. One can get
|
||||||
files. These auxiliary tools are installed in the application cache. Their
|
their location with the `which` command word. For example,
|
||||||
location can be found using the `which` command. For example, the command
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python-appimage which appimagetool
|
python-appimage which appimagetool
|
||||||
```
|
```
|
||||||
|
|
||||||
returns the location of [appimagetool][APPIMAGETOOL] if it has been installed.
|
returns the location of `appimagetool`, if it has been installed. If not, the
|
||||||
If not, the `install` command can be used to trigger its installation.
|
`install` command word can be used in order to trigger its installation.
|
||||||
{{ end(".capsule") }}
|
{{ end(".capsule") }}
|
||||||
|
|
||||||
|
|
||||||
## Manylinux Python AppImages
|
## Manylinux Python AppImages
|
||||||
|
|
||||||
AppImages of your local `python` are unlikely to be portable, unless you are
|
AppImages of your local `python` are unlikely to be portable, except if you run
|
||||||
running an outdated Linux distribution. A core component that prevents
|
an ancient Linux distribution. Indeed, a core component preventing portability
|
||||||
portability across Linux distributions is the use of different versions of the
|
across Linuses is the use of different versions of the `glibc` system library.
|
||||||
`glibc` system library. Fortunately, `glibc` is highly backward compatible.
|
Hopefully, `glibc` is highly backward compatible. Therefore, a simple
|
||||||
Therefore, a simple workaround is to compile binaries using the oldest Linux
|
work-around is to compile binaries using the oldest Linux distro you can afford
|
||||||
distribution you can. This strategy is used to create portable AppImages and to
|
to. This is the strategy used for creating portable AppImages, as well as for
|
||||||
distribute Python site packages as ready-to-use binary [wheels][WHEELS].
|
distributing Python site packages as ready-to-use binary [wheels][WHEELS].
|
||||||
|
|
||||||
The Python Packaging Authority (PyPA) has defined standard platform tags for
|
The Python Packaging Authority (PyPA) has defined standard platform tags for
|
||||||
building Python site packages labelled [Manylinux][MANYLINUX]. These build
|
building Python site packages, labelled [manylinux][MANYLINUX]. These build
|
||||||
platforms are available as Docker images, with different versions of Python
|
platforms are available as Docker images with various versions of Python already
|
||||||
already installed. The `python-appimage` utility can be used to package these
|
installed. The `python-appimage` utility can be used to package those installs
|
||||||
installations as AppImages. For example, the following command
|
as AppImages. For example, the following command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python-appimage build manylinux 2014_x86_64 cp313-cp313
|
python-appimage build manylinux 2014_x86_64 cp310-cp310
|
||||||
```
|
```
|
||||||
|
|
||||||
should build an AppImage of Python __3.13__ using the CPython (__cp313-cp313__)
|
should build an AppImage of Python 3.10 using the CPython (_cp310-cp310_)
|
||||||
installation found in the `manylinux2014_x86_64` Docker image.
|
install found in the `manylinux2014_x86_64` Docker image.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
From version `1.4.0` of `python-appimage` onwards, Docker is **no longer**
|
Docker needs to be already installed on your system in order to build
|
||||||
required to build the Manylinux Python images. Cross-building is also
|
Manylinux Python images. However, the command above can be run on the host.
|
||||||
supported, for example producing an `aarch64` Python image from an `x86_64`
|
That is, you need **not** to explictly shell inside the manylinux Docker
|
||||||
host.
|
image.
|
||||||
|
|
||||||
!!! Warning
|
|
||||||
Creating multiple Manylinux Python images can significantly increase the
|
|
||||||
size of the application cache. This can be managed using the
|
|
||||||
`python-appimage cache` command.
|
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
A compilation of ready-to-use Manylinux Python AppImages is available in the
|
A compilation of ready-to-use Manylinux Python AppImages is available from
|
||||||
[releases][RELEASES] section of the `python-appimage` [GitHub
|
the [releases][RELEASES] area of the `python-appimage` [GitHub
|
||||||
repository][GITHUB]. These AppImages are updated weekly, on every Sunday.
|
repository][GITHUB]. These AppImages are updated weekly, on every Sunday.
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
Instead of an AppImage, the `python-appimage build manylinux` command can
|
|
||||||
produce either an `AppDir` or a bare tarball (i.e. without the AppImage
|
|
||||||
layer) of a Manylinux Python installation. See the `-b` and `-n` command
|
|
||||||
line options for more information.
|
|
||||||
|
|
||||||
## Simple packaging
|
## Simple packaging
|
||||||
|
|
||||||
The `python-appimage` utility can also be used to package simple AppImage
|
The `python-appimage` utility can also be used in order to build simple
|
||||||
applications, whose dependencies can be installed using `pip`. The syntax is
|
applications, that can be `pip` installed. The syntax is
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python-appimage build app -p 3.13 /path/to/recipe/folder
|
python-appimage build app -p 3.10 /path/to/recipe/folder
|
||||||
```
|
```
|
||||||
|
|
||||||
to build a Python 3.13-based application from a recipe folder. Examples of
|
in order to build a Python 3.10 based application from a recipe folder.
|
||||||
recipes can be found in the [applications][APPLICATIONS] folder on GitHub. The
|
Examples of recipes can be found on GitHub in the [applications][APPLICATIONS]
|
||||||
recipe folder contains
|
folder. The recipe folder contains:
|
||||||
|
|
||||||
- the AppImage metadata (`application.xml` and `application.desktop`),
|
- the AppImage metadata (`application.xml` and `application.desktop`),
|
||||||
- an application icon (e.g. `application.png`),
|
- an application icon (e.g. `application.png`),
|
||||||
- a Python requirements file (`requirements.txt`),
|
- a Python requirements file (`requirements.txt`)
|
||||||
- an entry point script (`entrypoint.sh`).
|
- an entry point script (`entrypoint.sh`).
|
||||||
|
|
||||||
Further information on metadata can be found in the AppImage documentation
|
Additional information on metadata can be found in the AppImage documentation.
|
||||||
(e.g., regarding [desktop][APPIMAGE_DESKTOP] and [AppStream XML][APPIMAGE_XML]
|
That is, for [desktop][APPIMAGE_DESKTOP] and [AppStream XML][APPIMAGE_XML]
|
||||||
files). The `requirements.txt` file enables additional site packages to be
|
files. The `requirements.txt` file allows to specify additional site packages
|
||||||
specified for bundling in the AppImage using `pip`.
|
to be bundled in the AppImage, using `pip`.
|
||||||
|
|
||||||
!!! Caution
|
!!! Caution
|
||||||
In order for the application to be portable, the site packages bundled in
|
Site packages bundled in the AppImage, as well as their dependencies, must
|
||||||
the AppImage and their dependencies must be available as binary wheels or
|
either be pure python packages, or they must be available as portable binary
|
||||||
pure Python packages.
|
wheels.
|
||||||
|
|
||||||
If a **C extension** is bundled from **source**, it will likely **not be
|
If a **C extension** is bundled from **source**, then it will likely **not
|
||||||
portable**; this is discussed further in the [Advanced
|
be portable**, as further discussed in the [Advanced
|
||||||
packaging](#advanced-packaging) section.
|
packaging](#advanced-packaging) section.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
Some site packages are only available for specific Manylinux tags. You can
|
Some site packages are available only for specific Manylinux tags. This can
|
||||||
check this by browsing the `Download files` section on the package's PyPI
|
be cross-checked by browsing the `Download files` section on the package's
|
||||||
page.
|
PyPI page.
|
||||||
|
|
||||||
!!! Tip
|
|
||||||
Since version 1.2, `python-appimage` allows local requirements to be
|
|
||||||
specified using the `local+` tag (see
|
|
||||||
[PR49](https://github.com/niess/python-appimage/pull/49)). Please note,
|
|
||||||
however, that this involves directly copying the local package, which has
|
|
||||||
several limitations.
|
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### Entry point script
|
### Entry point script
|
||||||
|
|
||||||
{% raw %}
|
{% raw %}
|
||||||
The entry point script deserves some additional explanations. This script lets
|
The entry point script deserves some additional explanations. This script allows
|
||||||
you customise your application's startup. A typical `entrypoint.sh` script would
|
to customize the startup of your application. A typical `entrypoint.sh` script
|
||||||
look like this
|
would look like
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
{{ python-executable }} ${APPDIR}/opt/python{{ python-version }}/bin/my_app.py "$@"
|
{{ 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
|
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
|
be seen from the previous example, the `entrypoint.sh` script recognises some
|
||||||
particular variables nested between double curly braces (`{{}}`). These
|
particular variables, nested between double curly braces, `{{ }}`. Those
|
||||||
variables are listed in the table below. In addition, the usual [AppImage
|
variables are listed in the table hereafter. In addition, usual [AppImage
|
||||||
environement variables][APPIMAGE_ENV] can be used if needed. For instance,
|
environement variables][APPIMAGE_ENV] can be used as well, if needed. For
|
||||||
`$APPDIR` points to the AppImage mount point at runtime.
|
example, `$APPDIR` points to the AppImage mount point at runtime.
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
|
|
||||||
|
|
||||||
| variable | Description |
|
| variable | Description |
|
||||||
|----------------------|---------------------------------------------------------------|
|
|----------------------|---------------------------------------------------------------|
|
||||||
| `architecture` | The AppImage architecture, e.g. `x86_64`. |
|
| `architecture` | The AppImage architecture, e.g. `x86_64`. |
|
||||||
@@ -185,78 +166,49 @@ environement variables][APPIMAGE_ENV] can be used if needed. For instance,
|
|||||||
|
|
||||||
{% raw %}
|
{% raw %}
|
||||||
!!! Note
|
!!! Note
|
||||||
By default, Python AppImages are not isolated from user space or
|
By default, Python AppImages are not isolated from the user space, nor from
|
||||||
Python-specific environment variables such as `PYTHONPATH`. Depending on
|
Python specific environment variables, the like `PYTHONPATH`. Depending on
|
||||||
your use case, this can cause problems.
|
your use case, this can be problematic.
|
||||||
|
|
||||||
You can change the isolation level by adding the `-E`, `-s` or `-I` options
|
The runtime isolation level can be changed by adding the `-E`, `-s` or `-I`
|
||||||
when invoking the runtime. For example, `{{ python-executable }} -I` starts
|
options, when invoking the runtime. For example,
|
||||||
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 auxiliary data files directly into
|
|
||||||
the resulting AppImage. The `-x/--extra-data` switch is used for this purpose.
|
|
||||||
Consider the following example.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo -n "foo" > foo
|
|
||||||
mkdir bar
|
|
||||||
echo -n "baz" > bar/baz
|
|
||||||
python-appimage [your regular parameters] -x foo bar/*
|
|
||||||
```
|
|
||||||
|
|
||||||
In this way, user data becomes accessible to the Python code contained within
|
|
||||||
the AppImage as regular files under the directory pointed to by the `APPDIR`
|
|
||||||
environment variable. An example of a Python 3 script that reads these files is
|
|
||||||
presented below.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import os, pathlib
|
|
||||||
for fileName in ("foo", "baz"):
|
|
||||||
print((pathlib.Path(os.getenv("APPDIR")) / fileName).read_text())
|
|
||||||
```
|
|
||||||
|
|
||||||
When executed, the above code would produce the following output.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
foo
|
|
||||||
baz
|
|
||||||
```
|
|
||||||
|
|
||||||
## Advanced packaging
|
## Advanced packaging
|
||||||
|
|
||||||
In more complex cases, for example if your application relies on external C
|
In more complex cases, e.g. if your application relies on external C libraries
|
||||||
libraries that are not bundled with the Python runtime, the simple packaging
|
not bundled with the Python runtime, then the simple packaging scheme described
|
||||||
scheme described previously will not work. This falls outside the scope of
|
previously will fail. Indeed, this falls out of the scope of `python-appimage`,
|
||||||
`python-appimage`, which is primarily intended for relocating an existing Python
|
whose main purpose it to relocate an existing Python install. In this case, you
|
||||||
installation. In this case, you may wish to refer to the initial AppImage
|
might rather refer to the initial AppImage [Packaging
|
||||||
[Packaging Guide][APPIMAGE_PACKAGING], and use alternative tools such as
|
Guide][APPIMAGE_PACKAGING], and use alternative tools like
|
||||||
[linuxdeploy][LINUXDEPLOY].
|
[linuxdeploy][LINUXDEPLOY].
|
||||||
|
|
||||||
However, `python-appimage` can still be useful in more complex cases, as it can
|
However, `python-appimage` can still be of use in more complex cases by
|
||||||
generate a base AppDir containing a relocatable Python runtime (e.g., using the
|
extracting its AppImages to an AppDir, as discussed in the [Advanced
|
||||||
`-n` option). This can then serve as a starting point to create more complex
|
installation](index.md#advanced-installation) section. The extracted AppImages
|
||||||
AppImages.
|
contain a relocatable Python runtime, that can be used as a starting base for
|
||||||
|
building more complex AppImages.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
In some cases, a simple workaround for missing external libraries is to
|
In some cases, a simple workaround to missing external libraries can be to
|
||||||
download portable versions of them from a Manylinux distribution and bundle
|
fetch portable versions of those from a Manylinux distro, and to bundle them
|
||||||
them in `AppDir/usr/lib`. You may also need to edit the dynamic section
|
under `AppDir/usr/lib`. You might also need to edit their dynamic section,
|
||||||
using [`patchelf`][PATCHELF], which is installed by `python-appimage`.
|
e.g. using [`patchelf`][PATCHELF], which is installed by `python-appimage`.
|
||||||
|
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### C extension modules
|
### C extension modules
|
||||||
|
|
||||||
If your application relies on C extension modules, these must be compiled on a
|
If your application relies on C extension modules, they need to be compiled on a
|
||||||
Manylinux distribution in order to be portable. Their dependencies also need to
|
Manylinux distro in order to be portable. In addition, their dependencies need
|
||||||
be bundled. In this case, it would be better to start by building a binary wheel
|
to be bundled as well. In this case, you might better start by building a binary
|
||||||
of your package using tools like [Auditwheel][AUDITWHEEL], which can automate
|
wheel of your package, using tools like [Auditwheel][AUDITWHEEL] which can
|
||||||
some parts of the packaging process. Please note that `auditwheel` is already
|
automate some parts of the packaging process. Note that `auditwheel` is already
|
||||||
installed on the Manylinux Docker images.
|
installed on the Manylinux Docker images.
|
||||||
|
|
||||||
Once you have built a binary wheel of your package, you can use it with
|
Once you have built a binary wheel of your package, it can be used with
|
||||||
`python-appimage` to package your application as an AppImage.
|
`python-appimage` in order to package your application as an AppImage.
|
||||||
{{ end(".capsule") }}
|
{{ end(".capsule") }}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
# Python AppImages
|
# Python AppImages
|
||||||
|
|
||||||
We provide relocatable Python runtimes for _Linux_ systems, as
|
We provide relocatable Python runtimes for _Linux_ systems, as
|
||||||
[AppImages][APPIMAGE]. These runtimes have been extracted from a variety of
|
[AppImages][APPIMAGE]. These runtimes have been extracted from
|
||||||
[Manylinux][MANYLINUX] Docker images.
|
[manylinux][MANYLINUX] Docker images.
|
||||||
{{ "" | id("append-releases-list") }}
|
{{ "" | id("append-releases-list") }}
|
||||||
|
|
||||||
## Basic installation
|
## Basic installation
|
||||||
@@ -35,24 +35,23 @@ chmod +x python3.10.2-cp310-cp310-manylinux2014_x86_64.AppImage
|
|||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
As can be seen from the previous [example](#basic-installation-example), the
|
As can be seen from the previous [example](#basic-installation-example), the
|
||||||
AppImage name contains several pieces of information. This includes the
|
AppImage name contains several informations. That are, the Python full
|
||||||
Python full version ({{ "3.10.2" | id("example-full-version") }}), the
|
version ({{ "3.10.2" | id("example-full-version") }}), the CPython tag
|
||||||
[CPython tag][PEP_425] ({{ "cp310-cp310" | id("example-python-tag") }}), the
|
({{ "cp310-cp310" | id("example-python-tag") }}), the Linux compatibility
|
||||||
[Linux compatibility tag][MANYLINUX] ({{ "manylinux2014" |
|
tag ({{ "manylinux2014" | id("example-linux-tag") }}) and the machine
|
||||||
id("example-linux-tag") }}) and the machine architecture ({{ "x86_64" |
|
architecture ({{ "x86_64" | id("example-arch-tag") }}).
|
||||||
id("example-arch-tag") }}).
|
|
||||||
|
|
||||||
!!! Caution
|
!!! Caution
|
||||||
It is essential to **select an AppImage** that aligns with the **system's
|
One needs to **select an AppImage** that matches **system requirements**. A
|
||||||
specifications**. An overview of the available Python AppImages is provided
|
summmary of available Python AppImages is provided at the
|
||||||
at the [bottom](#available-python-appimages) of this page.
|
[bottom](#available-python-appimages) of this page.
|
||||||
|
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### Creating a symbolic link
|
### Creating a symbolic link
|
||||||
|
|
||||||
As AppImages' native names are quite lengthy, it might be relevant to create a
|
Since AppImages native names are rather lengthy, one might create a symbolic
|
||||||
symbolic link, for example as
|
link, e.g. as
|
||||||
|
|
||||||
{{ begin("#basic-installation-example-symlink") }}
|
{{ begin("#basic-installation-example-symlink") }}
|
||||||
```bash
|
```bash
|
||||||
@@ -60,23 +59,23 @@ ln -s python3.10.2-cp310-cp310-manylinux2014_x86_64.AppImage python3.10
|
|||||||
```
|
```
|
||||||
{{ end("#basic-installation-example-symlink") }}
|
{{ end("#basic-installation-example-symlink") }}
|
||||||
|
|
||||||
Executing the AppImage as {{ "`./python3.10`" |
|
Then, executing the AppImage as
|
||||||
id("basic-installation-example-execution") }} should then start a Python
|
{{ "`./python3.10`" | id("basic-installation-example-execution") }} should
|
||||||
interactive session on almost any Linux distribution, provided that **fuse** is
|
start a Python interactive session on _almost_ any Linux, provided that **fuse**
|
||||||
supported.
|
is supported.
|
||||||
{{ end(".capsule") }}
|
{{ end(".capsule") }}
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
Fuse is not supported on Windows Subsystem for Linux v1 (WSL1), which
|
Fuse is not supported on Windows Subsytem for Linux v1 (WSL1), preventing
|
||||||
prevents the direct execution of AppImages. However, it is still possible to
|
AppImages direct execution. Yet, one can still extract the content of Python
|
||||||
extract the contents of Python AppImages and use them, as explained in the
|
AppImages and use them, as explained in the [Advanced
|
||||||
[Advanced installation](#advanced-installation) section.
|
installation](#advanced-installation) section.
|
||||||
|
|
||||||
|
|
||||||
## Installing site packages
|
## Installing site packages
|
||||||
|
|
||||||
Site packages can be installed using `pip`, which is distributed with Python
|
Site packages can be installed using `pip`, distributed with the AppImage. For
|
||||||
AppImages. For example, the following command
|
example, the following
|
||||||
|
|
||||||
{{ begin("#site-packages-example") }}
|
{{ begin("#site-packages-example") }}
|
||||||
```bash
|
```bash
|
||||||
@@ -84,22 +83,23 @@ AppImages. For example, the following command
|
|||||||
```
|
```
|
||||||
{{ end("#site-packages-example") }}
|
{{ end("#site-packages-example") }}
|
||||||
|
|
||||||
installs the [numpy][NUMPY] package, assuming that a symlink to the AppImage has
|
installs the numpy package, where it is assumed that a symlink to the AppImage
|
||||||
been created beforehand. When using this **basic installation** scheme, Python
|
has been previously created. When using the **basic installation** scheme, by
|
||||||
packages are installed by default to your **user space** (i.e. under `~/.local`
|
default Python packages are installed to your **user space**, i.e. under
|
||||||
on Linux).
|
`~/.local` on Linux.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
AppImages are read-only. Therefore, site packages cannot be installed
|
AppImage are read-only. Therefore, site packages cannot be directly
|
||||||
directly to the Python AppImage. However, the AppImage can be extracted, as
|
installed to the AppImage. However, the AppImage can be extracted, as
|
||||||
explained in the [Advanced installation](#advanced-installation) section.
|
explained in the [Advanced installation](#advanced-installation) section.
|
||||||
|
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### Alternative site packages location
|
### Alternative site packages location
|
||||||
|
|
||||||
The `--target option` of pip can be used to specify an alternative installation
|
One can
|
||||||
directory for site packages. For example, the following command
|
specify an alternative installation directory for site packages using the
|
||||||
|
`--target` option of pip. For example, the following
|
||||||
|
|
||||||
{{ begin("#site-packages-example-target") }}
|
{{ begin("#site-packages-example-target") }}
|
||||||
```bash
|
```bash
|
||||||
@@ -107,9 +107,7 @@ directory for site packages. For example, the following command
|
|||||||
```
|
```
|
||||||
{{ end("#site-packages-example-target") }}
|
{{ end("#site-packages-example-target") }}
|
||||||
|
|
||||||
installs the [numpy][NUMPY] package in the `packages` folder, besides the
|
installs the numpy package besides the AppImage, in a `packages` folder.
|
||||||
AppImage.
|
|
||||||
|
|
||||||
{{ end(".capsule") }}
|
{{ end(".capsule") }}
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
@@ -118,33 +116,33 @@ AppImage.
|
|||||||
`PYTHONPATH` environment variable.
|
`PYTHONPATH` environment variable.
|
||||||
|
|
||||||
!!! Caution
|
!!! Caution
|
||||||
Although Python AppImages are relocatable, site packages may not be. In
|
While Python AppImages are relocatable, site packages might not be. In
|
||||||
particular, packages that install executable Python scripts assume a fixed
|
particular, packages installing executable Python scripts assume a fix
|
||||||
location for the Python runtime. If the Python AppImage is moved, these
|
location of the Python runtime. If the Python AppImage is moved, then these
|
||||||
scripts will fail. This can be resolved by either editing the script
|
scripts will fail. This can be patched by editing the script
|
||||||
[shebang][SHEBANG] or reinstalling the corresponding package.
|
[shebang][SHEBANG], or be reinstalling the corresponding package.
|
||||||
|
|
||||||
|
|
||||||
## Isolating from the user environment
|
## Isolating from the user environment
|
||||||
|
|
||||||
By default, Python AppImages are not isolated from the user environment. For
|
By default, Python AppImages are not isolated from the user environment. For
|
||||||
example, packages located under `~/.local/lib/pythonX.Y/site-packages` are
|
example, packages located under `~/.local/lib/pythonX.Y/site-packages` are
|
||||||
loaded before the AppImage's ones. Note that this is the standard Python runtime
|
loaded prior to AppImage's (system) ones. Note that this is the usual Python
|
||||||
behaviour. However, this can be conflictual for some applications.
|
runtime behaviour. However, it can be conflictual for some applications.
|
||||||
|
|
||||||
To isolate your application from the user environment, the Python runtime
|
In order to isolate your application from the user environment, the Python
|
||||||
provides the `-E`, `-s` and `-I` options. For example, running {{ "`./python3.10
|
runtime provides the `-E`, `-s` and `-I` options. For example, invoking a Python
|
||||||
-s`" | id("user-isolation-example") }} prevents the loading of user site
|
AppImage as {{ "`./python3.10 -s`" | id("user-isolation-example") }} prevents
|
||||||
packages located under `~/.local`. Additionally, the `-E` option disables
|
the loading of user site packages (located under `~/.local`). Additionaly, the
|
||||||
Python-related environment variables. In particular, it prevents packages under
|
`-E` option disables Python related environment variables. In particular, it
|
||||||
`PYTHONPATH` from being loaded. The `-I` option triggers both the `-E` and `-s`
|
prevents packages under `PYTHONPATH` to be loaded. The `-I` option triggers both
|
||||||
options.
|
`-E` and `-s`.
|
||||||
|
|
||||||
|
|
||||||
## Using a virtual environement
|
## Using a virtual environement
|
||||||
|
|
||||||
[Virtual environments][VENV] can also be used to achieve isolation. For example,
|
Isolation can also be achieved with a [virtual environment][VENV]. Python
|
||||||
Python AppImages can create a `venv` using the standard syntax, as
|
AppImages can create a `venv` using the standard syntax, e.g. as
|
||||||
|
|
||||||
{{ begin("#venv-example") }}
|
{{ begin("#venv-example") }}
|
||||||
```bash
|
```bash
|
||||||
@@ -152,16 +150,15 @@ Python AppImages can create a `venv` using the standard syntax, as
|
|||||||
```
|
```
|
||||||
{{ end("#venv-example") }}
|
{{ end("#venv-example") }}
|
||||||
|
|
||||||
Please note that moving the base Python AppImage to a different location will
|
Note that moving the base Python AppImage to another location breaks the virtual
|
||||||
break the virtual environment. This can be resolved by editing the symbolic
|
environment. This can be patched by editing symbolic links under `venv/bin`, as
|
||||||
links in `venv/bin`, as well as the `home` variable in `venv/pyvenv.cfg`. The
|
well as the `home` variable in `venv/pyvenv.cfg`. The latter must point to the
|
||||||
latter must point to the AppImage directory.
|
AppImage directory.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
Old Python AppImages created before version 1.1 fail to set up `pip`
|
Old Python AppImages, created before version 1.1, fail setting up `pip`
|
||||||
automatically during `venv` creation. However, this can be resolved by
|
automaticaly during `venv` creation. However, this can be patched by calling
|
||||||
calling `ensurepip` within the virtual environment after its creation. For
|
`ensurepip` from within the `venv`, after its creation. For example, as
|
||||||
example, as
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source /path/to/new/virtual/environment/bin/activate
|
source /path/to/new/virtual/environment/bin/activate
|
||||||
@@ -173,10 +170,10 @@ python -m ensurepip
|
|||||||
## Advanced installation
|
## Advanced installation
|
||||||
|
|
||||||
The [basic installation](#basic-installation) scheme described previously has
|
The [basic installation](#basic-installation) scheme described previously has
|
||||||
certain limitations when Python AppImages are used as the runtime environment.
|
some limitations when using Python AppImages as a runtime. For example, site
|
||||||
For example, site packages need to be installed in a different location. This
|
packages need to be installed to a separate location. This can be solved by
|
||||||
issue can be resolved by extracting a Python AppImage to an `AppDir`
|
extracting a Python AppImage to an `*.AppDir` directory, e.g. as
|
||||||
directory, e.g. as
|
|
||||||
|
|
||||||
{{ begin("#advanced-installation-example") }}
|
{{ begin("#advanced-installation-example") }}
|
||||||
```bash
|
```bash
|
||||||
@@ -188,32 +185,32 @@ ln -s python3.10.2-cp310-cp310-manylinux2014_x86_64.AppDir/AppRun python3.10
|
|||||||
```
|
```
|
||||||
{{ end("#advanced-installation-example") }}
|
{{ end("#advanced-installation-example") }}
|
||||||
|
|
||||||
Then, by default, **site packages** are installed to the extracted `AppDir`
|
Then, by default **site packages** are installed to the extracted **AppDir**,
|
||||||
when using `pip`. Additionally, executable scripts installed by `pip` are
|
when using `pip`. In addition, executable scripts installed by `pip` are patched
|
||||||
patched to use relative [shebangs][SHEBANG]. Consequently, the `AppDir` can be
|
in order to use relative [shebangs][SHEBANG]. Consequently, the AppDir can be
|
||||||
moved around freely.
|
freely moved around.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python `AppDirs` follow the [Manylinux][MANYLINUX] installation scheme.
|
Python AppDirs follow the [manylinux][MANYLINUX] installation scheme.
|
||||||
Executable scripts are installed under the `AppDir/opt/pythonX.Y/bin`
|
Executable scripts are installed under `AppDir/opt/pythonX.Y/bin` where _X_
|
||||||
directory, where _X_ and _Y_ represent the major and minor version numbers,
|
and _Y_ in _pythonX.Y_ stand for the major and minor version numbers. Site
|
||||||
respectively. Site packages are located under
|
packages are located under
|
||||||
`AppDir/opt/pythonX.Y/lib/pythonX.Y/site-packages`. For convenience,
|
`AppDir/opt/pythonX.Y/lib/pythonX.Y/site-packages`. For convenience, `pip`
|
||||||
applications installed using `pip` are also mirrored under `AppDir/usr/bin`
|
installed applications are also mirrored under `AppDir/usr/bin`, using
|
||||||
using symbolic links.
|
symbolic links.
|
||||||
|
|
||||||
!!! Tip
|
!!! Tip
|
||||||
As for Python AppImages, the extracted runtime is [not isolated from the
|
As for Python AppImages, by default the extracted runtime is [not isolated
|
||||||
user environment](#isolating-from-the-user-environment) by default. This
|
from the user environment](#isolating-from-the-user-environment). This
|
||||||
behaviour can be changed by editing the `AppDir/usr/bin/pythonX.Y` wrapper
|
behaviour can be changed by editing the `AppDir/usr/bin/pythonX.Y` wrapper
|
||||||
script and adding the `-s`, `-E` or `-I` option to the line invoking Python
|
script, and by adding the `-s`, `-E` or `-I` option at the very bottom,
|
||||||
(at the end of the script).
|
where Python is invoked.
|
||||||
|
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### Repackaging the AppImage
|
### Repackaging the AppImage
|
||||||
|
|
||||||
An extracted `AppDir` can be re-packaged as an AppImage using
|
An extracted AppDir can be re-packaged as an AppImage using
|
||||||
[appimagetool][APPIMAGETOOL], e.g. as
|
[appimagetool][APPIMAGETOOL], e.g. as
|
||||||
|
|
||||||
|
|
||||||
@@ -230,21 +227,21 @@ chmod +x appimagetool-x86_64.AppImage
|
|||||||
```
|
```
|
||||||
{{ end("#repackaging-example") }}
|
{{ end("#repackaging-example") }}
|
||||||
|
|
||||||
This allows you to personalise your Python AppImage by adding your preferred
|
This allows to customize your Python AppImage, for example by adding your
|
||||||
site packages, for example.
|
preferred site packages.
|
||||||
{{ end(".capsule") }}
|
{{ end(".capsule") }}
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Python AppImages can also be used to package Python-based applications as
|
Python AppImages can also be used for packaging Python based applications,
|
||||||
AppImages. Further information can be found in the [developers'
|
as AppImages. Additional details are provided in the [developers
|
||||||
section](apps).
|
section](apps).
|
||||||
|
|
||||||
|
|
||||||
## Available Python AppImages
|
## Available Python AppImages
|
||||||
|
|
||||||
The [table](#appimages-download-links) below provides a summary of the available
|
A summary of available Python AppImages [releases][RELEASES] is provided in the
|
||||||
Python AppImage [releases][RELEASES]. Clicking on a badge should download the
|
[table](#appimages-download-links) below. Clicking on a badge should download
|
||||||
corresponding AppImage.
|
the corresponding AppImage.
|
||||||
|
|
||||||
{{ begin("#suggest-appimage-download") }}
|
{{ begin("#suggest-appimage-download") }}
|
||||||
!!! Caution
|
!!! Caution
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ $.getJSON("https://api.github.com/repos/niess/python-appimage/releases").done(fu
|
|||||||
/* Unpack release metadata */
|
/* Unpack release metadata */
|
||||||
const releases = []
|
const releases = []
|
||||||
for (const datum of data) {
|
for (const datum of data) {
|
||||||
if (!datum.name.startsWith("Python")) continue;
|
|
||||||
var full_version = undefined;
|
var full_version = undefined;
|
||||||
const assets = [];
|
const assets = [];
|
||||||
for (const asset of datum.assets) {
|
for (const asset of datum.assets) {
|
||||||
@@ -12,23 +11,11 @@ $.getJSON("https://api.github.com/repos/niess/python-appimage/releases").done(fu
|
|||||||
/* Parse AppImage metadata */
|
/* Parse AppImage metadata */
|
||||||
const tmp0 = asset.name.split("manylinux")
|
const tmp0 = asset.name.split("manylinux")
|
||||||
const tag = tmp0[1].slice(0,-9);
|
const tag = tmp0[1].slice(0,-9);
|
||||||
const tmp1 = tag.split(/_(.+)/, 2);
|
const tmp1 = tag.split(/_(.+)/);
|
||||||
var linux = undefined;
|
const linux = tmp1[0]
|
||||||
var arch = undefined;
|
const arch = tmp1[1]
|
||||||
if (tmp1[0] == "") {
|
const tmp2 = tmp0[0].split("-")
|
||||||
const tmp3 = tmp1[1].split("_");
|
const python = tmp2[1] + "-" + tmp2[2]
|
||||||
linux = tmp3[0] + "_" + tmp3[1];
|
|
||||||
if (tmp3.length == 3) {
|
|
||||||
arch = tmp3[2];
|
|
||||||
} else {
|
|
||||||
arch = tmp3[2] + "_" + tmp3[3];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
linux = tmp1[0];
|
|
||||||
arch = tmp1[1];
|
|
||||||
}
|
|
||||||
const tmp2 = tmp0[0].split("-", 3);
|
|
||||||
const python = tmp2[1] + "-" + tmp2[2];
|
|
||||||
assets.push({
|
assets.push({
|
||||||
name: asset.name,
|
name: asset.name,
|
||||||
url: asset.browser_download_url,
|
url: asset.browser_download_url,
|
||||||
@@ -38,8 +25,8 @@ $.getJSON("https://api.github.com/repos/niess/python-appimage/releases").done(fu
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (full_version === undefined) {
|
if (full_version === undefined) {
|
||||||
const index = asset.name.indexOf("-");
|
const index = asset.name.indexOf("-")
|
||||||
full_version = asset.name.slice(6, index);
|
full_version = asset.name.slice(6, index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +62,7 @@ $.getJSON("https://api.github.com/repos/niess/python-appimage/releases").done(fu
|
|||||||
elements.push(`<a href="${release.url}">${release.version}</a>`)
|
elements.push(`<a href="${release.url}">${release.version}</a>`)
|
||||||
}
|
}
|
||||||
$("#append-releases-list").html(
|
$("#append-releases-list").html(
|
||||||
" The available Python versions are " +
|
" Available Python versions are " +
|
||||||
elements.slice(0, -1).join(", ") +
|
elements.slice(0, -1).join(", ") +
|
||||||
" and " +
|
" and " +
|
||||||
elements[elements.length - 1] +
|
elements[elements.length - 1] +
|
||||||
@@ -175,26 +162,11 @@ $.getJSON("https://api.github.com/repos/niess/python-appimage/releases").done(fu
|
|||||||
|
|
||||||
ln -s ${appdir}/AppRun python${release.version}
|
ln -s ${appdir}/AppRun python${release.version}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
set_snippet("#repackaging-example", `\
|
|
||||||
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/\\
|
|
||||||
appimagetool-x86_64.AppImage
|
|
||||||
|
|
||||||
chmod +x appimagetool-x86_64.AppImage
|
|
||||||
|
|
||||||
./appimagetool-x86_64.AppImage \\
|
|
||||||
${appdir} \\
|
|
||||||
${asset.name}
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function badge (asset, pad) {
|
function badge (asset, pad) {
|
||||||
const colors = {
|
const colors = {i686: "lightgrey", x86_64: "blue"};
|
||||||
aarch64: "d8dee9",
|
|
||||||
i686: "81a1c1",
|
|
||||||
x86_64: "5e81ac"
|
|
||||||
};
|
|
||||||
const python = asset.python.split("-")[1];
|
const python = asset.python.split("-")[1];
|
||||||
const arch = asset.arch.replace("_", "__");
|
const arch = asset.arch.replace("_", "__");
|
||||||
var color = colors[asset.arch];
|
var color = colors[asset.arch];
|
||||||
|
|||||||
13
paper/Makefile
Normal file
13
paper/Makefile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
paper.pdf: paper.md paper.bib
|
||||||
|
docker run --rm \
|
||||||
|
--volume ${PWD}:/data \
|
||||||
|
--user $(id -u):$(id -g) \
|
||||||
|
--env JOURNAL=joss \
|
||||||
|
openjournals/inara \
|
||||||
|
-o pdf \
|
||||||
|
/data/paper.md
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -f paper.pdf
|
||||||
132
paper/paper.bib
Normal file
132
paper/paper.bib
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
@software{kernel,
|
||||||
|
title = {Linux Kernel},
|
||||||
|
author = "Torvalds, Linus and others",
|
||||||
|
howpublished = "\url{https://kernel.org/}",
|
||||||
|
year = 1991
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{appimage,
|
||||||
|
title = {AppImage},
|
||||||
|
booktitle = {Linux applications that run everywhere},
|
||||||
|
author = "Peter, Simon and others",
|
||||||
|
howpublished = "\url{https://appimage.org/}",
|
||||||
|
year = 2013
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{python,
|
||||||
|
title = {Python},
|
||||||
|
author = "{van Rossum}, Guido and others",
|
||||||
|
howpublished = "\url{https://www.python.org/}",
|
||||||
|
year = 1991
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{anaconda,
|
||||||
|
title = {Anaconda},
|
||||||
|
author = "Anaconda{, Inc.}",
|
||||||
|
howpublished = "\url{https://www.anaconda.com/}",
|
||||||
|
year = 2012
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{wheels,
|
||||||
|
title = {Python wheels},
|
||||||
|
author = "{Python Software Foundation}",
|
||||||
|
howpublished = "\url{https://pythonwheels.com/}",
|
||||||
|
year = 2012
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{flatpak,
|
||||||
|
title = {Flatpak},
|
||||||
|
author = "Larsson, Alexander and others",
|
||||||
|
howpublished = "\url{https://flatpak.org/}",
|
||||||
|
year = 2015
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{snap,
|
||||||
|
title = {Snap},
|
||||||
|
author = "{Canonical Group Limited}",
|
||||||
|
howpublished = "\url{https://snapcraft.io/}",
|
||||||
|
year = 2016
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{libfuse,
|
||||||
|
title = {Linux FUSE},
|
||||||
|
author = "Szeredi, Miklos and Rath, Nikolaus and others",
|
||||||
|
howpublished = "\url{https://github.com/libfuse/libfuse}",
|
||||||
|
year = 2013
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{glibc,
|
||||||
|
title = {The GNU C Library},
|
||||||
|
author = "McGrath, Roland and Drepper, Ulrich and others",
|
||||||
|
howpublished = "\url{https://www.gnu.org/software/libc/}",
|
||||||
|
year = 1987
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{docker,
|
||||||
|
title = {Docker},
|
||||||
|
author = "{Docker, Inc.}",
|
||||||
|
howpublished = "\url{https://www.docker.com/}",
|
||||||
|
year = 2013
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{singularity,
|
||||||
|
title = {Singularity},
|
||||||
|
author = "Kurtzer, Gregory and others",
|
||||||
|
howpublished = "\url{https://apptainer.org/}",
|
||||||
|
year = 2015
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{patchelf,
|
||||||
|
title = {PatchELF},
|
||||||
|
author = "Dolstra, Eelco and others",
|
||||||
|
howpublished = "\url{https://github.com/NixOS/patchelf}",
|
||||||
|
year = 2004
|
||||||
|
}
|
||||||
|
|
||||||
|
@software{linuxdeploy,
|
||||||
|
title = {linuxdeploy},
|
||||||
|
author = "{@TheAssassin} and others",
|
||||||
|
howpublished = "\url{https://github.com/linuxdeploy/linuxdeploy}",
|
||||||
|
year = 2018
|
||||||
|
}
|
||||||
|
|
||||||
|
@online{Gillmor:2014,
|
||||||
|
title = "{Q}\&{A} with {Linus} {Torvalds}",
|
||||||
|
date = 2014,
|
||||||
|
organization = "{Debian} {Conference} 2014",
|
||||||
|
author = "Gillmor, Daniel and Guerrero López, Ana and Torvalds,
|
||||||
|
Linus and others",
|
||||||
|
url = {https://www.youtube.com/watch?v=5PmHRSeA2c8}
|
||||||
|
}
|
||||||
|
|
||||||
|
@online{manylinux,
|
||||||
|
title = "Manylinux",
|
||||||
|
date = 2016,
|
||||||
|
organization = "{GitHub}",
|
||||||
|
author = "{Python Packaging Authority}",
|
||||||
|
url = {https://github.com/pypa/manylinux}
|
||||||
|
}
|
||||||
|
|
||||||
|
@online{tiobe,
|
||||||
|
title = "TIOBE index",
|
||||||
|
date = 2022,
|
||||||
|
author = "{The Software Quality Company}",
|
||||||
|
url = {https://www.tiobe.com/tiobe-index/}
|
||||||
|
}
|
||||||
|
|
||||||
|
@online{grand,
|
||||||
|
title = "Giant Radio Array for Neutrino Detection",
|
||||||
|
date = 2018,
|
||||||
|
author = "{The GRAND collaboration}",
|
||||||
|
url = {https://grand.cnrs.fr/}
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{Alvarez-Muniz:2020,
|
||||||
|
author = "J. Álvarez-Muñiz and others",
|
||||||
|
doi = {10.1007/s11433-018-9385-7},
|
||||||
|
issue = {1},
|
||||||
|
journal = {Science China: Physics, Mechanics and Astronomy},
|
||||||
|
title = {The Giant Radio Array for Neutrino Detection (GRAND): Science and design},
|
||||||
|
volume = {63},
|
||||||
|
year = {2020}
|
||||||
|
}
|
||||||
150
paper/paper.md
Normal file
150
paper/paper.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
title: 'The Python-AppImage project'
|
||||||
|
tags:
|
||||||
|
- AppImage
|
||||||
|
- Linux
|
||||||
|
- Packaging
|
||||||
|
- Python
|
||||||
|
authors:
|
||||||
|
- name: Valentin Niess
|
||||||
|
orcid: 0000-0001-7148-6819
|
||||||
|
affiliation: 1
|
||||||
|
affiliations:
|
||||||
|
- name: Université Clermont Auvergne, CNRS/IN2P3, LPC, F-63000 Clermont-Ferrand, France.
|
||||||
|
index: 1
|
||||||
|
date: 22 June 2022
|
||||||
|
bibliography: paper.bib
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
Since its initial release in 1991, the Linux Kernel [@kernel] has given birth to
|
||||||
|
more than 600 hundred Linux distributions (distros). While this is an impressive
|
||||||
|
success, the diversity of Linux flavours complicates the distribution of
|
||||||
|
applications for Linux (see e.g. [@Gillmor:2014]). Thus, contrary to other
|
||||||
|
operating systems, on Linux, source distributions long prevailed over binary
|
||||||
|
(precompiled) ones. Specifically, this is still the case for the Linux Python
|
||||||
|
runtime [@python] distributed by the Python Software Foundation (PSF).
|
||||||
|
Previously, this was also the case for Python packages available from the Python
|
||||||
|
Package Index (PyPI). This situation contributed to the emergence of an
|
||||||
|
alternative Python packaging system [@anaconda] delivering ready-to-use
|
||||||
|
precompiled runtimes and packages for Linux.
|
||||||
|
|
||||||
|
Over the last decade, a change of paradigm occurred in the Linux world. Cross
|
||||||
|
distros packaging systems have emerged, the like AppImage [@appimage], Flatpak
|
||||||
|
[@flatpak] and Snap [@snap]. At the same time, the PSF encouraged the conversion
|
||||||
|
of PyPI packages from source to binary distributions, using the new `wheel`
|
||||||
|
packaging format [@wheels].
|
||||||
|
|
||||||
|
The AppImage format is of particular interest for the present discussion.
|
||||||
|
Contrary to Flatpak and Snap, AppImages do not require to install an external
|
||||||
|
package manager. AppImage applications are bundled as a single executable file
|
||||||
|
upstreamed by developpers, ready-to-use after download. However, building proper
|
||||||
|
AppImages adds some complexity on the developers side.
|
||||||
|
|
||||||
|
Technically, an AppImage is an executable file embedding a software application
|
||||||
|
over a compressed virtual filesystem (VFS). The VFS is extracted and mounted at
|
||||||
|
runtime using `libfuse` [@libfuse]. Apart from core libraries, the like `libc`,
|
||||||
|
application dependencies are directly bundled inside the AppImage. In this
|
||||||
|
context, binary compatibility usually stems down to the host glibc [@glibc]
|
||||||
|
version used at compile time. As a matter of fact, glibc is admirably backward
|
||||||
|
compatible, down to version 2.1 where symbol versioning was introduced. Thus, in
|
||||||
|
practice binary compatibility is achieved by precompiling the application and
|
||||||
|
its relevant dependencies on an *old enough* Linux distro. This is greatly
|
||||||
|
facilitated by recent container technologies, the like Docker [@docker] and
|
||||||
|
Singularity [@singularity].
|
||||||
|
|
||||||
|
In practice, producing a portable binary wheel for Linux, or packaging an
|
||||||
|
application as an AppImage, faces the same issues. Accordingly, almost identical
|
||||||
|
strategies and tools are used in both cases. In particular, the PSF has
|
||||||
|
defined standard build platforms for Linux wheels. Those are available as Docker
|
||||||
|
images from the Manylinux project [@manylinux]. These images contain
|
||||||
|
precompiled Python runtimes with binary compatibility down to glibc 2.5 (for
|
||||||
|
manylinux1, corresponding to CentOS 5).
|
||||||
|
|
||||||
|
The Python-AppImage project provides relocatable Python runtimes as AppImages.
|
||||||
|
These runtimes are extracted from the Manylinux Docker images. Consequently,
|
||||||
|
they have the exact same binary compatibility as PyPI wheels. Python AppImages
|
||||||
|
are available as rolling GitHub releases. They are updated automatically on
|
||||||
|
every Sunday using GitHub Actions.
|
||||||
|
|
||||||
|
At the core of the Python-AppImage project, there is the `python-appimage`
|
||||||
|
executable, written in pure Python, and available from PyPI using `pip install`
|
||||||
|
(down to Python 2.7). The main functionality of `python-appimage` is to relocate
|
||||||
|
an existing (system) Python installation to an AppImage. Vanilla Python is not
|
||||||
|
relocatable. Therefore, some tweaks are needed in order to run Python from an
|
||||||
|
AppImage. In particular,
|
||||||
|
|
||||||
|
- Python initialisation is slightly modified in order to set `sys.executable`
|
||||||
|
and `sys.prefix` to the AppImage and to its temporary mount point. This is
|
||||||
|
achieved by a small edit of the `site` package.
|
||||||
|
|
||||||
|
- The run-time search path of Linux ELF files (Python runtime, binary packages,
|
||||||
|
shared libraries) is changed to a relative location, according to the AppImage
|
||||||
|
VFS hierarchy. Note that `patchelf` [@patchelf] allows to edit the
|
||||||
|
corresponding ELF entry. Thus, the Python runtime and its binary packages need
|
||||||
|
not to be recompiled.
|
||||||
|
|
||||||
|
Besides, `python-appimage` can also build simple Python based applications
|
||||||
|
according to a recipe folder. The recipe folder contains an entry point script,
|
||||||
|
AppImage metadata and an optional `requirements.txt` Python file. The
|
||||||
|
application is built from an existing Manylinux Python AppImage. Extra
|
||||||
|
dependencies are fetched from PyPI with `pip`. Note that they must be available
|
||||||
|
as binary wheels for the resulting AppImage to be portable.
|
||||||
|
|
||||||
|
|
||||||
|
# Statement of need
|
||||||
|
|
||||||
|
Python is among the top computing languages used nowadays. It was ranked number
|
||||||
|
one in 2021 and 2022 according to the TIOBE index [@tiobe]. In particular,
|
||||||
|
Python is widely used by the scientific community. A peculiarity of the Python
|
||||||
|
language is that it constantly evolves, owing to an Open Source community of
|
||||||
|
contributors structured by the PSF. While I personally find this very exciting
|
||||||
|
when working with Python, it comes at a price. Different projects use different
|
||||||
|
Python versions, and package requirements might conflict. This can be
|
||||||
|
circumvented by using virtual environments. For example, the `venv` package
|
||||||
|
allows one to manage different sets of requirements. However, it requires an
|
||||||
|
existing Python installation, i.e. a specific runtime version. On the contrary,
|
||||||
|
`conda` environments automate the management of both different runtime versions,
|
||||||
|
and sets of requirements. But, unfortunately Anaconda fragmented Python
|
||||||
|
packaging since, in practice, `conda` packages and Python wheels are not
|
||||||
|
(always) binary compatible.
|
||||||
|
|
||||||
|
Python-AppImage offers alternative solutions when working within the PSF context
|
||||||
|
(for `conda` based AppImages, `linuxdeploy` [@linuxdeploy] could be used).
|
||||||
|
First, Python-AppImage complements `venv` by providing ready-to-use Linux
|
||||||
|
runtimes for different Python versions. Moreover, Python AppImages can be used
|
||||||
|
as a replacement to `venv` by bundling extra (site) packages aside the runtime.
|
||||||
|
In this case, since AppImages are read-only, it can be convenient to extract
|
||||||
|
their content to a local folder (using the built-in `--appimage-extract`
|
||||||
|
option). Then, the extracted AppImage appears as a classic Python installation,
|
||||||
|
but it is relocatable. Especially, one can `pip install` additional packages to
|
||||||
|
the extracted folder, and it can be moved around, e.g. from a development host
|
||||||
|
directly to a production computing center.
|
||||||
|
|
||||||
|
|
||||||
|
# Mention
|
||||||
|
|
||||||
|
This project was born in the context of the Giant Radio Array for Neutrino
|
||||||
|
Detection (GRAND) [@grand] in order to share Python based software across
|
||||||
|
various Linux hosts: personal or office computers, computing centers, etc.
|
||||||
|
Specifically, Python AppImages were used for the neutrino sensitivity studies
|
||||||
|
presented in the GRAND whitepaper [@Alvarez-Muniz:2020].
|
||||||
|
|
||||||
|
I do not track usage of Python AppImages, which are available as simple
|
||||||
|
downloads. Therefore, I cannot provide detailed statistics. However, I am aware
|
||||||
|
of a dozen of (non scientific) Python based applications using
|
||||||
|
`python-appimage`. Besides, I personally use Python AppImages for my daily
|
||||||
|
academic work.
|
||||||
|
|
||||||
|
|
||||||
|
# Acknowledgements
|
||||||
|
|
||||||
|
We are grateful to several GitHub users for comments, feedback and contributions
|
||||||
|
to this project. In particular, we would like to thank Andy Kipp (\@anki-code),
|
||||||
|
Simon Peter (\@probonopd) and \@TheAssassin for support at the very roots of
|
||||||
|
this project. In addition, we are grateful to our colleagues from the GRAND
|
||||||
|
collaboration for beta testing Python AppImages.
|
||||||
|
|
||||||
|
|
||||||
|
# References
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "python_appimage"
|
|
||||||
authors = [
|
|
||||||
{ name="Valentin Niess", email="valentin.niess@gmail.com" },
|
|
||||||
]
|
|
||||||
dynamic = ["version"]
|
|
||||||
description = "Appimage releases of Python"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.9"
|
|
||||||
dependencies = [
|
|
||||||
"requests",
|
|
||||||
"setuptools",
|
|
||||||
]
|
|
||||||
classifiers = [
|
|
||||||
"Development Status :: 4 - Beta",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Topic :: Software Development",
|
|
||||||
"Operating System :: POSIX :: Linux",
|
|
||||||
]
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
license-files = ["LICENSE"]
|
|
||||||
|
|
||||||
[project.scripts]
|
|
||||||
python-appimage = "python_appimage.__main__:main"
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
homepage = "https://github.com/niess/python-appimage"
|
|
||||||
documentation = "https://python-appimage.readthedocs.io"
|
|
||||||
download = "https://pypi.python.org/pypi/python-appimage"
|
|
||||||
source = "https://github.com/niess/python-appimage"
|
|
||||||
issues = "https://github.com/niess/python-appimage/issues"
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["setuptools >= 77.0.3"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
|
||||||
include = ["python_appimage*"]
|
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
|
||||||
version = {attr = "python_appimage.__version__"}
|
|
||||||
|
|
||||||
[tool.bumpversion]
|
|
||||||
current_version = "1.4.5"
|
|
||||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
||||||
serialize = ["{major}.{minor}.{patch}"]
|
|
||||||
search = "{current_version}"
|
|
||||||
replace = "{new_version}"
|
|
||||||
regex = false
|
|
||||||
ignore_missing_version = false
|
|
||||||
tag = false
|
|
||||||
allow_dirty = false
|
|
||||||
commit = true
|
|
||||||
message = "Bump version: v{new_version}"
|
|
||||||
commit_args = ""
|
|
||||||
|
|
||||||
[[tool.bumpversion.files]]
|
|
||||||
filename = "python_appimage/version.py"
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import platform
|
import platform
|
||||||
|
|
||||||
from .version import version as __version__
|
|
||||||
|
|
||||||
|
|
||||||
if platform.system() != 'Linux':
|
if platform.system() != 'Linux':
|
||||||
raise RuntimeError('invalid system: ' + platform.system())
|
raise RuntimeError('invalid system: ' + platform.system())
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
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
|
||||||
'''
|
'''
|
||||||
@@ -27,45 +23,23 @@ 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=logging.ERROR)
|
||||||
parser.add_argument('-v', '--verbose', help='print extra information',
|
parser.add_argument('-v', '--verbose', help='print extra information',
|
||||||
dest='verbosity', action='store_const', const='DEBUG')
|
dest='verbosity', action='store_const', const=logging.DEBUG)
|
||||||
|
|
||||||
build_parser = subparsers.add_parser('build',
|
|
||||||
description='Build a Python appimage')
|
|
||||||
build_subparsers = build_parser.add_subparsers(title='type',
|
|
||||||
help='Type of AppImage build', dest='sub_command')
|
|
||||||
|
|
||||||
cache_parser = subparsers.add_parser('cache',
|
|
||||||
description='Manage Python appimage cache')
|
|
||||||
cache_subparsers = cache_parser.add_subparsers(title='operation',
|
|
||||||
help='Type of cache operation', dest='sub_command')
|
|
||||||
|
|
||||||
cache_clean_parser = cache_subparsers.add_parser('clean',
|
|
||||||
description='Clean cached image(s)')
|
|
||||||
cache_clean_parser.add_argument('tags', nargs='*',
|
|
||||||
help='manylinux image tag(s) (e.g. 2014_x86_64)')
|
|
||||||
cache_clean_parser.add_argument('-a', '--all', action='store_true',
|
|
||||||
help='remove all image(s) data')
|
|
||||||
|
|
||||||
cache_get_parser = cache_subparsers.add_parser('get',
|
|
||||||
description='Download image(s) to the cache')
|
|
||||||
cache_get_parser.add_argument('tags', nargs='+',
|
|
||||||
help='manylinux image tag(s) (e.g. 2014_x86_64)')
|
|
||||||
cache_get_parser.add_argument('-e', '--extract', action='store_true',
|
|
||||||
help='extract compressed image data')
|
|
||||||
|
|
||||||
cache_list_parser = cache_subparsers.add_parser('list',
|
|
||||||
description='List cached image(s)')
|
|
||||||
|
|
||||||
install_parser = subparsers.add_parser('install',
|
install_parser = subparsers.add_parser('install',
|
||||||
description='Install binary dependencies')
|
description='Install binary dependencies')
|
||||||
install_parser.add_argument('binary', nargs='+',
|
install_parser.add_argument('binary', nargs='+',
|
||||||
choices=binaries, help='one or more binary name')
|
choices=binaries, help='one or more binary name')
|
||||||
|
|
||||||
|
build_parser = subparsers.add_parser('build',
|
||||||
|
description='Build a Python appimage')
|
||||||
|
build_subparsers = build_parser.add_subparsers(
|
||||||
|
title='type',
|
||||||
|
help='Type of AppImage build',
|
||||||
|
dest='sub_command')
|
||||||
|
|
||||||
build_local_parser = build_subparsers.add_parser('local',
|
build_local_parser = build_subparsers.add_parser('local',
|
||||||
description='Bundle a local Python installation')
|
description='Bundle a local Python installation')
|
||||||
build_local_parser.add_argument('-d', '--destination',
|
build_local_parser.add_argument('-d', '--destination',
|
||||||
@@ -73,18 +47,14 @@ def main():
|
|||||||
build_local_parser.add_argument('-p', '--python', help='python executable')
|
build_local_parser.add_argument('-p', '--python', help='python executable')
|
||||||
|
|
||||||
build_manylinux_parser = build_subparsers.add_parser('manylinux',
|
build_manylinux_parser = build_subparsers.add_parser('manylinux',
|
||||||
description='Bundle a manylinux Python installation')
|
description='Bundle a manylinux Python installation using docker')
|
||||||
build_manylinux_parser.add_argument('tag',
|
build_manylinux_parser.add_argument('tag',
|
||||||
help='manylinux image tag (e.g. 2010_x86_64)')
|
help='manylinux image tag (e.g. 2010_x86_64)')
|
||||||
build_manylinux_parser.add_argument('abi',
|
build_manylinux_parser.add_argument('abi',
|
||||||
help='python ABI (e.g. cp37-cp37m)')
|
help='python ABI (e.g. cp37-cp37m)')
|
||||||
build_manylinux_parser.add_argument('-b', '--bare',
|
|
||||||
help='produce a bare image without the AppImage layer',
|
build_manylinux_parser.add_argument('--contained', help=argparse.SUPPRESS,
|
||||||
action='store_true')
|
action='store_true', default=False)
|
||||||
build_manylinux_parser.add_argument('-c', '--clean',
|
|
||||||
help='clean the cache after extraction', action='store_true')
|
|
||||||
build_manylinux_parser.add_argument('-n', '--no-packaging',
|
|
||||||
help='do not package (compress) the image', action='store_true')
|
|
||||||
|
|
||||||
build_app_parser = build_subparsers.add_parser('app',
|
build_app_parser = build_subparsers.add_parser('app',
|
||||||
description='Build a Python application using a base AppImage')
|
description='Build a Python application using a base AppImage')
|
||||||
@@ -96,8 +66,6 @@ def main():
|
|||||||
help='linux compatibility tag (e.g. manylinux1_x86_64)')
|
help='linux compatibility tag (e.g. manylinux1_x86_64)')
|
||||||
build_app_parser.add_argument('-n', '--name',
|
build_app_parser.add_argument('-n', '--name',
|
||||||
help='application name')
|
help='application name')
|
||||||
build_app_parser.add_argument('--no-packaging',
|
|
||||||
help='do not package the app', action='store_true')
|
|
||||||
build_app_parser.add_argument('--python-tag',
|
build_app_parser.add_argument('--python-tag',
|
||||||
help='python compatibility tag (e.g. cp37-cp37m)')
|
help='python compatibility tag (e.g. cp37-cp37m)')
|
||||||
build_app_parser.add_argument('-p', '--python-version',
|
build_app_parser.add_argument('-p', '--python-version',
|
||||||
@@ -106,13 +74,6 @@ 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',
|
|
||||||
description='List Python versions installed in a manylinux image')
|
|
||||||
list_parser.add_argument('tag',
|
|
||||||
help='manylinux image tag (e.g. 2010_x86_64)')
|
|
||||||
|
|
||||||
which_parser = subparsers.add_parser('which',
|
which_parser = subparsers.add_parser('which',
|
||||||
description='Locate a binary dependency')
|
description='Locate a binary dependency')
|
||||||
@@ -123,12 +84,7 @@ def main():
|
|||||||
|
|
||||||
# Configure the verbosity
|
# Configure the verbosity
|
||||||
if args.verbosity:
|
if args.verbosity:
|
||||||
from .utils import log
|
logging.getLogger().setLevel(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:
|
||||||
@@ -153,5 +109,5 @@ def main():
|
|||||||
command.execute(*command._unpack_args(args))
|
command.execute(*command._unpack_args(args))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from .build import build_appimage
|
from .build import build_appimage
|
||||||
from .appify import Appifier, tcltk_env_string
|
from .relocate import cert_file_env_string, patch_binary, relocate_python, \
|
||||||
from .relocate import patch_binary, relocate_python
|
tcltk_env_string
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Appifier', 'build_appimage', 'patch_binary', 'relocate_python',
|
__all__ = ['build_appimage', 'cert_file_env_string', 'patch_binary',
|
||||||
'tcltk_env_string']
|
'relocate_python', 'tcltk_env_string']
|
||||||
|
|||||||
@@ -1,270 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from ..utils.deps import PREFIX
|
|
||||||
from ..utils.fs import copy_file, make_tree, remove_file
|
|
||||||
from ..utils.log import log
|
|
||||||
from ..utils.template import copy_template, load_template
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Appifier:
|
|
||||||
'''Helper class for bundling AppImage specific files'''
|
|
||||||
|
|
||||||
'''Path to AppDir root.'''
|
|
||||||
appdir: str
|
|
||||||
|
|
||||||
'''Path to AppDir executables.'''
|
|
||||||
appdir_bin: str
|
|
||||||
|
|
||||||
'''Path to Python executables.'''
|
|
||||||
python_bin: str
|
|
||||||
|
|
||||||
'''Path to Python site-packages.'''
|
|
||||||
python_pkg: str
|
|
||||||
|
|
||||||
'''Tcl/Tk version.'''
|
|
||||||
tk_version: str
|
|
||||||
|
|
||||||
'''Python version.'''
|
|
||||||
version: 'PythonVersion'
|
|
||||||
|
|
||||||
'''Path to SSL certification file.'''
|
|
||||||
cert_src: Optional[str]=None
|
|
||||||
|
|
||||||
def appify(self):
|
|
||||||
'''Bundle Appimage specific files'''
|
|
||||||
|
|
||||||
python_x_y = f'python{self.version.short()}'
|
|
||||||
pip_x_y = f'pip{self.version.short()}'
|
|
||||||
|
|
||||||
# Add a runtime patch for sys.executable, before site.main() execution
|
|
||||||
log('PATCH', f'{python_x_y} sys.executable')
|
|
||||||
set_executable_patch(
|
|
||||||
self.version.short(),
|
|
||||||
self.python_pkg,
|
|
||||||
PREFIX + '/data/_initappimage.py'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set a hook for cleaning sys.path, after site.main() execution
|
|
||||||
log('HOOK', f'{python_x_y} sys.path')
|
|
||||||
|
|
||||||
sitepkgs = self.python_pkg + '/site-packages'
|
|
||||||
make_tree(sitepkgs)
|
|
||||||
copy_file(PREFIX + '/data/sitecustomize.py', sitepkgs)
|
|
||||||
|
|
||||||
# Symlink SSL certificates
|
|
||||||
# (see https://github.com/niess/python-appimage/issues/24)
|
|
||||||
cert_file = '/opt/_internal/certs.pem'
|
|
||||||
cert_dst = f'{self.appdir}{cert_file}'
|
|
||||||
if self.cert_src is not None:
|
|
||||||
if os.path.exists(self.cert_src):
|
|
||||||
if not os.path.exists(cert_dst):
|
|
||||||
dirname, basename = os.path.split(cert_dst)
|
|
||||||
relpath = os.path.relpath(self.cert_src, dirname)
|
|
||||||
make_tree(dirname)
|
|
||||||
os.symlink(relpath, cert_dst)
|
|
||||||
log('INSTALL', basename)
|
|
||||||
if not os.path.exists(cert_dst):
|
|
||||||
cert_file = None
|
|
||||||
|
|
||||||
# Bundle the python wrapper
|
|
||||||
wrapper = f'{self.appdir_bin}/{python_x_y}'
|
|
||||||
if not os.path.exists(wrapper):
|
|
||||||
log('INSTALL', f'{python_x_y} wrapper')
|
|
||||||
entrypoint_path = PREFIX + '/data/entrypoint.sh'
|
|
||||||
entrypoint = load_template(
|
|
||||||
entrypoint_path,
|
|
||||||
python=f'python{self.version.flavoured()}'
|
|
||||||
)
|
|
||||||
dictionary = {
|
|
||||||
'entrypoint': entrypoint,
|
|
||||||
'shebang': '#! /bin/bash',
|
|
||||||
'tcltk-env': tcltk_env_string(self.python_pkg, self.tk_version)
|
|
||||||
}
|
|
||||||
if cert_file:
|
|
||||||
dictionary['cert-file'] = cert_file_env_string(cert_file)
|
|
||||||
else:
|
|
||||||
dictionary['cert-file'] = ''
|
|
||||||
|
|
||||||
_copy_template('python-wrapper.sh', wrapper, **dictionary)
|
|
||||||
|
|
||||||
# Set or update symlinks to python and pip.
|
|
||||||
pip_target = f'{self.python_bin}/{pip_x_y}'
|
|
||||||
if os.path.exists(pip_target):
|
|
||||||
relpath = os.path.relpath(pip_target, self.appdir_bin)
|
|
||||||
os.symlink(relpath, f'{self.appdir_bin}/{pip_x_y}')
|
|
||||||
|
|
||||||
pythons = glob.glob(self.appdir_bin + '/python?.*')
|
|
||||||
versions = [os.path.basename(python)[6:] for python in pythons]
|
|
||||||
latest2, latest3 = '0.0', '0.0'
|
|
||||||
for version in versions:
|
|
||||||
if version.startswith('2') and version >= latest2:
|
|
||||||
latest2 = version
|
|
||||||
elif version.startswith('3') and version >= latest3:
|
|
||||||
latest3 = version
|
|
||||||
if latest2 == self.version.short():
|
|
||||||
python2 = self.appdir_bin + '/python2'
|
|
||||||
remove_file(python2)
|
|
||||||
os.symlink(python_x_y, python2)
|
|
||||||
has_pip = os.path.exists(self.appdir_bin + '/' + pip_x_y)
|
|
||||||
if has_pip:
|
|
||||||
pip2 = self.appdir_bin + '/pip2'
|
|
||||||
remove_file(pip2)
|
|
||||||
os.symlink(pip_x_y, pip2)
|
|
||||||
if latest3 == '0.0':
|
|
||||||
log('SYMLINK', 'python, python2 to ' + python_x_y)
|
|
||||||
python = self.appdir_bin + '/python'
|
|
||||||
remove_file(python)
|
|
||||||
os.symlink('python2', python)
|
|
||||||
if has_pip:
|
|
||||||
log('SYMLINK', 'pip, pip2 to ' + pip_x_y)
|
|
||||||
pip = self.appdir_bin + '/pip'
|
|
||||||
remove_file(pip)
|
|
||||||
os.symlink('pip2', pip)
|
|
||||||
else:
|
|
||||||
log('SYMLINK', 'python2 to ' + python_x_y)
|
|
||||||
if has_pip:
|
|
||||||
log('SYMLINK', 'pip2 to ' + pip_x_y)
|
|
||||||
elif latest3 == self.version.short():
|
|
||||||
log('SYMLINK', 'python, python3 to ' + python_x_y)
|
|
||||||
python3 = self.appdir_bin + '/python3'
|
|
||||||
remove_file(python3)
|
|
||||||
os.symlink(python_x_y, python3)
|
|
||||||
python = self.appdir_bin + '/python'
|
|
||||||
remove_file(python)
|
|
||||||
os.symlink('python3', python)
|
|
||||||
if os.path.exists(self.appdir_bin + '/' + pip_x_y):
|
|
||||||
log('SYMLINK', 'pip, pip3 to ' + pip_x_y)
|
|
||||||
pip3 = self.appdir_bin + '/pip3'
|
|
||||||
remove_file(pip3)
|
|
||||||
os.symlink(pip_x_y, pip3)
|
|
||||||
pip = self.appdir_bin + '/pip'
|
|
||||||
remove_file(pip)
|
|
||||||
os.symlink('pip3', pip)
|
|
||||||
|
|
||||||
# Bundle the entry point
|
|
||||||
apprun = f'{self.appdir}/AppRun'
|
|
||||||
if not os.path.exists(apprun):
|
|
||||||
log('INSTALL', 'AppRun')
|
|
||||||
|
|
||||||
relpath = os.path.relpath(wrapper, self.appdir)
|
|
||||||
os.symlink(relpath, apprun)
|
|
||||||
|
|
||||||
# Bundle the desktop file
|
|
||||||
desktop_name = f'python{self.version.long()}.desktop'
|
|
||||||
desktop = os.path.join(self.appdir, desktop_name)
|
|
||||||
if not os.path.exists(desktop):
|
|
||||||
log('INSTALL', desktop_name)
|
|
||||||
apps = 'usr/share/applications'
|
|
||||||
appfile = f'{self.appdir}/{apps}/{desktop_name}'
|
|
||||||
if not os.path.exists(appfile):
|
|
||||||
make_tree(os.path.join(self.appdir, apps))
|
|
||||||
_copy_template('python.desktop', appfile,
|
|
||||||
version=self.version.short(),
|
|
||||||
fullversion=self.version.long())
|
|
||||||
os.symlink(os.path.join(apps, desktop_name), desktop)
|
|
||||||
|
|
||||||
# Bundle icons
|
|
||||||
icons = 'usr/share/icons/hicolor/256x256/apps'
|
|
||||||
icon = os.path.join(self.appdir, 'python.png')
|
|
||||||
if not os.path.exists(icon):
|
|
||||||
log('INSTALL', 'python.png')
|
|
||||||
make_tree(os.path.join(self.appdir, icons))
|
|
||||||
copy_file(PREFIX + '/data/python.png',
|
|
||||||
os.path.join(self.appdir, icons, 'python.png'))
|
|
||||||
os.symlink(os.path.join(icons, 'python.png'), icon)
|
|
||||||
|
|
||||||
diricon = os.path.join(self.appdir, '.DirIcon')
|
|
||||||
if not os.path.exists(diricon):
|
|
||||||
os.symlink('python.png', diricon)
|
|
||||||
|
|
||||||
# Bundle metadata
|
|
||||||
meta_name = f'python{self.version.long()}.appdata.xml'
|
|
||||||
meta_dir = os.path.join(self.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 = self.version.short(),
|
|
||||||
fullversion = self.version.long()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def cert_file_env_string(cert_file):
|
|
||||||
'''Environment for using a bundled certificate
|
|
||||||
'''
|
|
||||||
if cert_file:
|
|
||||||
return '''
|
|
||||||
# Export SSL certificate
|
|
||||||
export SSL_CERT_FILE="${{APPDIR}}{cert_file:}"'''.format(
|
|
||||||
cert_file=cert_file)
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
def _copy_template(name, destination, **kwargs):
|
|
||||||
path = os.path.join(PREFIX, 'data', name)
|
|
||||||
copy_template(path, destination, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def tcltk_env_string(python_pkg, tk_version):
|
|
||||||
'''Environment for using AppImage's TCl/Tk
|
|
||||||
'''
|
|
||||||
|
|
||||||
if tk_version:
|
|
||||||
return '''
|
|
||||||
# Export TCl/Tk
|
|
||||||
export TCL_LIBRARY="${{APPDIR}}/usr/share/tcltk/tcl{tk_version:}"
|
|
||||||
export TK_LIBRARY="${{APPDIR}}/usr/share/tcltk/tk{tk_version:}"
|
|
||||||
export TKPATH="${{TK_LIBRARY}}"'''.format(
|
|
||||||
tk_version=tk_version)
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
def set_executable_patch(version, pkgpath, patch):
|
|
||||||
'''Set a runtime patch for sys.executable name
|
|
||||||
'''
|
|
||||||
|
|
||||||
# This patch needs to be executed before site.main() is called. A natural
|
|
||||||
# option is to apply it directy to the site module. But, starting with
|
|
||||||
# Python 3.11, the site module is frozen within Python executable. Then,
|
|
||||||
# doing so would require to recompile Python. Thus, starting with 3.11 we
|
|
||||||
# instead apply the patch to the encodings package. Indeed, the latter is
|
|
||||||
# loaded before the site module, and it is not frozen (as for now).
|
|
||||||
major, minor = [int(v) for v in version.split('.')]
|
|
||||||
if (major >= 3) and (minor >= 11):
|
|
||||||
path = os.path.join(pkgpath, 'encodings', '__init__.py')
|
|
||||||
else:
|
|
||||||
path = os.path.join(pkgpath, 'site.py')
|
|
||||||
|
|
||||||
with open(path) as f:
|
|
||||||
source = f.read()
|
|
||||||
|
|
||||||
if '_initappimage' in source: return
|
|
||||||
|
|
||||||
lines = source.split(os.linesep)
|
|
||||||
|
|
||||||
if path.endswith('site.py'):
|
|
||||||
# Insert the patch before the main function
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if line.startswith('def main('): break
|
|
||||||
else:
|
|
||||||
# Append the patch at end of file
|
|
||||||
i = len(lines)
|
|
||||||
|
|
||||||
with open(patch) as f:
|
|
||||||
patch = f.read()
|
|
||||||
|
|
||||||
lines.insert(i, patch)
|
|
||||||
lines.insert(i + 1, '')
|
|
||||||
|
|
||||||
source = os.linesep.join(lines)
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(source)
|
|
||||||
@@ -5,25 +5,27 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..utils.compat import decode
|
from ..utils.compat import decode
|
||||||
from ..utils.deps import ensure_appimagetool
|
from ..utils.deps import APPIMAGETOOL, ensure_appimagetool
|
||||||
|
from ..utils.docker import docker_run
|
||||||
|
from ..utils.fs import copy_tree
|
||||||
from ..utils.log import debug, log
|
from ..utils.log import debug, log
|
||||||
|
from ..utils.tmp import TemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['build_appimage']
|
__all__ = ['build_appimage']
|
||||||
|
|
||||||
|
|
||||||
def build_appimage(appdir=None, *, arch=None, destination=None):
|
def build_appimage(appdir=None, destination=None):
|
||||||
'''Build an AppImage from an AppDir
|
'''Build an AppImage from an AppDir
|
||||||
'''
|
'''
|
||||||
if appdir is None:
|
if appdir is None:
|
||||||
appdir = 'AppDir'
|
appdir = 'AppDir'
|
||||||
|
|
||||||
log('BUILD', os.path.basename(appdir))
|
log('BUILD', appdir)
|
||||||
appimagetool = ensure_appimagetool()
|
ensure_appimagetool()
|
||||||
|
|
||||||
if arch is None:
|
|
||||||
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)
|
||||||
@@ -34,7 +36,7 @@ def build_appimage(appdir=None, *, arch=None, destination=None):
|
|||||||
|
|
||||||
appimage_pattern = re.compile('should be packaged as ([^ ]+[.]AppImage)')
|
appimage_pattern = re.compile('should be packaged as ([^ ]+[.]AppImage)')
|
||||||
|
|
||||||
stdout = []
|
stdout, appimage = [], None
|
||||||
while True:
|
while True:
|
||||||
out = decode(p.stdout.readline())
|
out = decode(p.stdout.readline())
|
||||||
stdout.append(out)
|
stdout.append(out)
|
||||||
@@ -43,8 +45,7 @@ def build_appimage(appdir=None, *, arch=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') and \
|
if line.startswith('WARNING'):
|
||||||
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)
|
||||||
|
|||||||
@@ -4,17 +4,62 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .appify import Appifier
|
from ..utils.deps import EXCLUDELIST, PATCHELF, PREFIX, ensure_excludelist, \
|
||||||
from ..manylinux import PythonVersion
|
|
||||||
from ..utils.deps import EXCLUDELIST, PATCHELF, ensure_excludelist, \
|
|
||||||
ensure_patchelf
|
ensure_patchelf
|
||||||
from ..utils.fs import copy_file, copy_tree, make_tree, remove_file, \
|
from ..utils.fs import copy_file, copy_tree, make_tree, remove_file, remove_tree
|
||||||
remove_tree
|
from ..utils.log import debug, log
|
||||||
from ..utils.log import log
|
|
||||||
from ..utils.system import ldd, system
|
from ..utils.system import ldd, system
|
||||||
|
from ..utils.template import copy_template, load_template
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['patch_binary', 'relocate_python']
|
__all__ = ["cert_file_env_string", "patch_binary", "relocate_python",
|
||||||
|
"tcltk_env_string"]
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_template(name, destination, **kwargs):
|
||||||
|
path = os.path.join(PREFIX, 'data', name)
|
||||||
|
copy_template(path, destination, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tk_version(python_pkg):
|
||||||
|
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)
|
||||||
|
return match.group(1)
|
||||||
|
else:
|
||||||
|
raise RuntimeError('could not guess Tcl/Tk version')
|
||||||
|
|
||||||
|
|
||||||
|
def tcltk_env_string(python_pkg):
|
||||||
|
'''Environment for using AppImage's TCl/Tk
|
||||||
|
'''
|
||||||
|
tk_version = _get_tk_version(python_pkg)
|
||||||
|
|
||||||
|
if tk_version:
|
||||||
|
return '''
|
||||||
|
# Export TCl/Tk
|
||||||
|
export TCL_LIBRARY="${{APPDIR}}/usr/share/tcltk/tcl{tk_version:}"
|
||||||
|
export TK_LIBRARY="${{APPDIR}}/usr/share/tcltk/tk{tk_version:}"
|
||||||
|
export TKPATH="${{TK_LIBRARY}}"'''.format(
|
||||||
|
tk_version=tk_version)
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def cert_file_env_string(cert_file):
|
||||||
|
'''Environment for using a bundled certificate
|
||||||
|
'''
|
||||||
|
if cert_file:
|
||||||
|
return '''
|
||||||
|
# Export SSL certificate
|
||||||
|
export SSL_CERT_FILE="${{APPDIR}}{cert_file:}"'''.format(
|
||||||
|
cert_file=cert_file)
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
_excluded_libs = None
|
_excluded_libs = None
|
||||||
@@ -23,7 +68,7 @@ _excluded_libs = None
|
|||||||
|
|
||||||
|
|
||||||
def patch_binary(path, libdir, recursive=True):
|
def patch_binary(path, libdir, recursive=True):
|
||||||
'''Patch the RPATH of a binary and fetch its dependencies
|
'''Patch the RPATH of a binary and and fetch its dependencies
|
||||||
'''
|
'''
|
||||||
global _excluded_libs
|
global _excluded_libs
|
||||||
|
|
||||||
@@ -40,27 +85,69 @@ 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))
|
||||||
relpath = '' if relpath == '.' else '/' + relpath
|
relpath = '' if relpath == '.' else '/' + relpath
|
||||||
expected = '\'$ORIGIN' + relpath + ':$ORIGIN/../lib\''
|
expected = '\'$ORIGIN' + relpath + '\''
|
||||||
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:
|
||||||
continue
|
continue
|
||||||
target = libdir + '/' + name
|
target = libdir + '/' + name
|
||||||
if not os.path.exists(target):
|
if not os.path.exists(target):
|
||||||
|
libname = os.path.basename(dep)
|
||||||
copy_file(dep, target)
|
copy_file(dep, target)
|
||||||
if recursive:
|
if recursive:
|
||||||
patch_binary(target, libdir, recursive=True)
|
patch_binary(target, libdir, recursive=True)
|
||||||
|
|
||||||
|
|
||||||
|
def set_executable_patch(version, pkgpath, patch):
|
||||||
|
'''Set a runtime patch for sys.executable name
|
||||||
|
'''
|
||||||
|
|
||||||
|
# This patch needs to be executed before site.main() is called. A natural
|
||||||
|
# option is to apply it directy to the site module. But, starting with
|
||||||
|
# Python 3.11, the site module is frozen within Python executable. Then,
|
||||||
|
# doing so would require to recompile Python. Thus, starting with 3.11 we
|
||||||
|
# instead apply the patch to the encodings package. Indeed, the latter is
|
||||||
|
# loaded before the site module, and it is not frozen (as for now).
|
||||||
|
major, minor = [int(v) for v in version.split('.')]
|
||||||
|
if (major >= 3) and (minor >= 11):
|
||||||
|
path = os.path.join(pkgpath, 'encodings', '__init__.py')
|
||||||
|
else:
|
||||||
|
path = os.path.join(pkgpath, 'site.py')
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
|
source = f.read()
|
||||||
|
|
||||||
|
if '_initappimage' in source: return
|
||||||
|
|
||||||
|
lines = source.split(os.linesep)
|
||||||
|
|
||||||
|
if path.endswith('site.py'):
|
||||||
|
# Insert the patch before the main function
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith('def main('): break
|
||||||
|
else:
|
||||||
|
# Append the patch at end of file
|
||||||
|
i = len(lines)
|
||||||
|
|
||||||
|
with open(patch) as f:
|
||||||
|
patch = f.read()
|
||||||
|
|
||||||
|
lines.insert(i, patch)
|
||||||
|
lines.insert(i + 1, '')
|
||||||
|
|
||||||
|
source = os.linesep.join(lines)
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(source)
|
||||||
|
|
||||||
|
|
||||||
def relocate_python(python=None, appdir=None):
|
def relocate_python(python=None, appdir=None):
|
||||||
'''Bundle a Python install inside an AppDir
|
'''Bundle a Python install inside an AppDir
|
||||||
'''
|
'''
|
||||||
@@ -75,11 +162,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', '"import sys; print(sys.version)"'))
|
FULLVERSION = system((python, '-c',
|
||||||
|
'"import sys; print(\'{:}.{:}.{:}\'.format(*sys.version_info[:3]))"'))
|
||||||
FULLVERSION = FULLVERSION.strip()
|
FULLVERSION = FULLVERSION.strip()
|
||||||
else:
|
else:
|
||||||
FULLVERSION = sys.version
|
FULLVERSION = '{:}.{:}.{:}'.format(*sys.version_info[:3])
|
||||||
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
|
||||||
@@ -106,21 +193,9 @@ 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):
|
||||||
paths = glob.glob(HOST_INC + '*')
|
HOST_INC += 'm'
|
||||||
if paths:
|
PYTHON_INC += 'm'
|
||||||
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
|
||||||
@@ -158,6 +233,9 @@ def relocate_python(python=None, appdir=None):
|
|||||||
f.write(body)
|
f.write(body)
|
||||||
shutil.copymode(pip_source, target)
|
shutil.copymode(pip_source, target)
|
||||||
|
|
||||||
|
relpath = os.path.relpath(target, APPDIR_BIN)
|
||||||
|
os.symlink(relpath, APPDIR_BIN + '/' + PIP_X_Y)
|
||||||
|
|
||||||
|
|
||||||
# Remove unrelevant files
|
# Remove unrelevant files
|
||||||
log('PRUNE', '%s packages', PYTHON_X_Y)
|
log('PRUNE', '%s packages', PYTHON_X_Y)
|
||||||
@@ -169,6 +247,17 @@ def relocate_python(python=None, appdir=None):
|
|||||||
for path in matches:
|
for path in matches:
|
||||||
remove_tree(path)
|
remove_tree(path)
|
||||||
|
|
||||||
|
# Add a runtime patch for sys.executable, before site.main() execution
|
||||||
|
log('PATCH', '%s sys.executable', PYTHON_X_Y)
|
||||||
|
set_executable_patch(VERSION, PYTHON_PKG, PREFIX + '/data/_initappimage.py')
|
||||||
|
|
||||||
|
# Set a hook for cleaning sys.path, after site.main() execution
|
||||||
|
log('HOOK', '%s sys.path', PYTHON_X_Y)
|
||||||
|
|
||||||
|
sitepkgs = PYTHON_PKG + '/site-packages'
|
||||||
|
make_tree(sitepkgs)
|
||||||
|
copy_file(PREFIX + '/data/sitecustomize.py', sitepkgs)
|
||||||
|
|
||||||
|
|
||||||
# Set RPATHs and bundle external libraries
|
# Set RPATHs and bundle external libraries
|
||||||
log('LINK', '%s C-extensions', PYTHON_X_Y)
|
log('LINK', '%s C-extensions', PYTHON_X_Y)
|
||||||
@@ -191,14 +280,23 @@ def relocate_python(python=None, appdir=None):
|
|||||||
tcltkdir = APPDIR_SHARE + '/tcltk'
|
tcltkdir = APPDIR_SHARE + '/tcltk'
|
||||||
if (not os.path.exists(tcltkdir + '/tcl' + tk_version)) or \
|
if (not os.path.exists(tcltkdir + '/tcl' + tk_version)) or \
|
||||||
(not os.path.exists(tcltkdir + '/tk' + tk_version)):
|
(not os.path.exists(tcltkdir + '/tk' + tk_version)):
|
||||||
libdir = _get_tk_libdir(tk_version)
|
hostdir = '/usr/share/tcltk'
|
||||||
log('INSTALL', 'Tcl/Tk' + tk_version)
|
if os.path.exists(hostdir):
|
||||||
|
make_tree(APPDIR_SHARE)
|
||||||
|
copy_tree(hostdir, tcltkdir)
|
||||||
|
else:
|
||||||
make_tree(tcltkdir)
|
make_tree(tcltkdir)
|
||||||
tclpath = libdir + '/tcl' + tk_version
|
tclpath = '/usr/share/tcl' + tk_version
|
||||||
|
if not tclpath:
|
||||||
|
raise ValueError('could not find ' + tclpath)
|
||||||
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
|
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
|
||||||
tkpath = libdir + '/tk' + tk_version
|
|
||||||
|
tkpath = '/usr/share/tk' + tk_version
|
||||||
|
if not tkpath:
|
||||||
|
raise ValueError('could not find ' + tkpath)
|
||||||
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
|
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
|
||||||
|
|
||||||
|
|
||||||
# Copy any SSL certificate
|
# Copy any SSL certificate
|
||||||
cert_file = os.getenv('SSL_CERT_FILE')
|
cert_file = os.getenv('SSL_CERT_FILE')
|
||||||
if cert_file:
|
if cert_file:
|
||||||
@@ -209,35 +307,111 @@ def relocate_python(python=None, appdir=None):
|
|||||||
copy_file(cert_file, 'AppDir' + cert_file)
|
copy_file(cert_file, 'AppDir' + cert_file)
|
||||||
log('INSTALL', basename)
|
log('INSTALL', basename)
|
||||||
|
|
||||||
# Bundle AppImage specific files.
|
|
||||||
appifier = Appifier(
|
|
||||||
appdir = APPDIR,
|
|
||||||
appdir_bin = APPDIR_BIN,
|
|
||||||
python_bin = PYTHON_BIN,
|
|
||||||
python_pkg = PYTHON_PKG,
|
|
||||||
tk_version = tk_version,
|
|
||||||
version = PythonVersion.from_str(FULLVERSION)
|
|
||||||
)
|
|
||||||
appifier.appify()
|
|
||||||
|
|
||||||
|
# Bundle the python wrapper
|
||||||
|
wrapper = APPDIR_BIN + '/' + PYTHON_X_Y
|
||||||
|
if not os.path.exists(wrapper):
|
||||||
|
log('INSTALL', '%s wrapper', PYTHON_X_Y)
|
||||||
|
entrypoint_path = PREFIX + '/data/entrypoint.sh'
|
||||||
|
entrypoint = load_template(entrypoint_path, python=PYTHON_X_Y)
|
||||||
|
dictionary = {'entrypoint': entrypoint,
|
||||||
|
'shebang': '#! /bin/bash',
|
||||||
|
'tcltk-env': tcltk_env_string(PYTHON_PKG),
|
||||||
|
'cert-file': cert_file_env_string(cert_file)}
|
||||||
|
_copy_template('python-wrapper.sh', wrapper, **dictionary)
|
||||||
|
|
||||||
def _get_tk_version(python_pkg):
|
# Set or update symlinks to python
|
||||||
tkinter = glob.glob(python_pkg + '/lib-dynload/_tkinter*.so')
|
pythons = glob.glob(APPDIR_BIN + '/python?.*')
|
||||||
if tkinter:
|
versions = [os.path.basename(python)[6:] for python in pythons]
|
||||||
tkinter = tkinter[0]
|
latest2, latest3 = '0.0', '0.0'
|
||||||
for dep in ldd(tkinter):
|
for version in versions:
|
||||||
name = os.path.basename(dep)
|
if version.startswith('2') and version >= latest2:
|
||||||
if name.startswith('libtk'):
|
latest2 = version
|
||||||
match = re.search('libtk([0-9]+[.][0-9]+)', name)
|
elif version.startswith('3') and version >= latest3:
|
||||||
return match.group(1)
|
latest3 = version
|
||||||
|
if latest2 == VERSION:
|
||||||
|
python2 = APPDIR_BIN + '/python2'
|
||||||
|
remove_file(python2)
|
||||||
|
os.symlink(PYTHON_X_Y, python2)
|
||||||
|
has_pip = os.path.exists(APPDIR_BIN + '/' + PIP_X_Y)
|
||||||
|
if has_pip:
|
||||||
|
pip2 = APPDIR_BIN + '/pip2'
|
||||||
|
remove_file(pip2)
|
||||||
|
os.symlink(PIP_X_Y, pip2)
|
||||||
|
if latest3 == '0.0':
|
||||||
|
log('SYMLINK', 'python, python2 to ' + PYTHON_X_Y)
|
||||||
|
python = APPDIR_BIN + '/python'
|
||||||
|
remove_file(python)
|
||||||
|
os.symlink('python2', python)
|
||||||
|
if has_pip:
|
||||||
|
log('SYMLINK', 'pip, pip2 to ' + PIP_X_Y)
|
||||||
|
pip = APPDIR_BIN + '/pip'
|
||||||
|
remove_file(pip)
|
||||||
|
os.symlink('pip2', pip)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError('could not guess Tcl/Tk version')
|
log('SYMLINK', 'python2 to ' + PYTHON_X_Y)
|
||||||
|
if has_pip:
|
||||||
|
log('SYMLINK', 'pip2 to ' + PIP_X_Y)
|
||||||
|
elif latest3 == VERSION:
|
||||||
|
log('SYMLINK', 'python, python3 to ' + PYTHON_X_Y)
|
||||||
|
python3 = APPDIR_BIN + '/python3'
|
||||||
|
remove_file(python3)
|
||||||
|
os.symlink(PYTHON_X_Y, python3)
|
||||||
|
python = APPDIR_BIN + '/python'
|
||||||
|
remove_file(python)
|
||||||
|
os.symlink('python3', python)
|
||||||
|
if os.path.exists(APPDIR_BIN + '/' + PIP_X_Y):
|
||||||
|
log('SYMLINK', 'pip, pip3 to ' + PIP_X_Y)
|
||||||
|
pip3 = APPDIR_BIN + '/pip3'
|
||||||
|
remove_file(pip3)
|
||||||
|
os.symlink(PIP_X_Y, pip3)
|
||||||
|
pip = APPDIR_BIN + '/pip'
|
||||||
|
remove_file(pip)
|
||||||
|
os.symlink('pip3', pip)
|
||||||
|
|
||||||
|
# Bundle the entry point
|
||||||
|
apprun = APPDIR + '/AppRun'
|
||||||
|
if not os.path.exists(apprun):
|
||||||
|
log('INSTALL', 'AppRun')
|
||||||
|
|
||||||
|
relpath = os.path.relpath(wrapper, APPDIR)
|
||||||
|
os.symlink(relpath, APPDIR + '/AppRun')
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
def _get_tk_libdir(version):
|
# Bundle icons
|
||||||
try:
|
icons = 'usr/share/icons/hicolor/256x256/apps'
|
||||||
library = system(('tclsh' + version,), stdin='puts [info library]')
|
icon = os.path.join(APPDIR, 'python.png')
|
||||||
except SystemError:
|
if not os.path.exists(icon):
|
||||||
raise RuntimeError('could not locate Tcl/Tk' + version + ' library')
|
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)
|
||||||
|
|
||||||
return os.path.dirname(library)
|
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)
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from ...appimage import build_appimage
|
from ...appimage import build_appimage
|
||||||
from ...utils.compat import find_spec
|
from ...utils.compat import decode
|
||||||
from ...utils.deps import PREFIX
|
from ...utils.deps import PREFIX
|
||||||
from ...utils.fs import copy_file, copy_tree, make_tree, remove_file, remove_tree
|
from ...utils.fs import copy_file, make_tree, remove_file, remove_tree
|
||||||
from ...utils.log import log
|
from ...utils.log import log
|
||||||
from ...utils.system import system
|
from ...utils.system import system
|
||||||
from ...utils.template import copy_template, load_template
|
from ...utils.template import copy_template, load_template
|
||||||
@@ -26,17 +26,14 @@ 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, args.no_packaging
|
|
||||||
|
|
||||||
|
|
||||||
_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, no_packaging=None):
|
|
||||||
'''Build a Python application using a base AppImage
|
'''Build a Python application using a base AppImage
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -256,49 +253,18 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
|
|||||||
'WARNING: Running pip as'
|
'WARNING: Running pip as'
|
||||||
)
|
)
|
||||||
|
|
||||||
git_warnings = (
|
system(('./AppDir/AppRun', '-m', 'pip', 'install', '-U', in_tree_build,
|
||||||
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'
|
|
||||||
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)
|
||||||
for requirement in requirements_list:
|
for requirement in requirements_list:
|
||||||
if requirement.startswith('git+'):
|
if requirement.startswith('git+'):
|
||||||
url, name = os.path.split(requirement)
|
url, name = os.path.split(requirement)
|
||||||
log('BUNDLE', name + ' from ' + url[4:])
|
log('BUNDLE', name + ' from ' + url[4:])
|
||||||
elif requirement.startswith('local+'):
|
|
||||||
name = requirement[6:]
|
|
||||||
source = find_spec(name).origin
|
|
||||||
if source.endswith('/__init__.py'):
|
|
||||||
source = os.path.dirname(source)
|
|
||||||
elif source.endswith('/'):
|
|
||||||
source = source[:-1]
|
|
||||||
log('BUNDLE', name + ' from ' + source)
|
|
||||||
if os.path.isfile(source):
|
|
||||||
destination = 'AppDir/opt/python{0:}/lib/python{0:}/site-packages/'.format(python_version)
|
|
||||||
copy_file(source, destination)
|
|
||||||
else:
|
|
||||||
destination = 'AppDir/opt/python{0:}/lib/python{0:}/site-packages/{1:}'.format(python_version, name)
|
|
||||||
copy_tree(source, destination)
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
log('BUNDLE', requirement)
|
log('BUNDLE', requirement)
|
||||||
system(('./AppDir/AppRun', isolation_flag, '-m', 'pip', 'install', '-U', in_tree_build,
|
system(('./AppDir/AppRun', '-m', 'pip', 'install', '-U', in_tree_build,
|
||||||
'--no-warn-script-location', requirement),
|
'--no-warn-script-location', requirement),
|
||||||
exclude=(deprecation + git_warnings))
|
exclude=(deprecation, ' Running command git clone'))
|
||||||
|
|
||||||
# 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.*')
|
||||||
@@ -312,6 +278,8 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
|
|||||||
shebang = '#! /bin/bash'
|
shebang = '#! /bin/bash'
|
||||||
|
|
||||||
entrypoint = load_template(entrypoint_path, **dictionary)
|
entrypoint = load_template(entrypoint_path, **dictionary)
|
||||||
|
python_pkg = 'AppDir/opt/python{0:}/lib/python{0:}'.format(
|
||||||
|
python_version)
|
||||||
dictionary = {'entrypoint': entrypoint,
|
dictionary = {'entrypoint': entrypoint,
|
||||||
'shebang': shebang}
|
'shebang': shebang}
|
||||||
if os.path.exists('AppDir/AppRun'):
|
if os.path.exists('AppDir/AppRun'):
|
||||||
@@ -321,10 +289,7 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
|
|||||||
|
|
||||||
|
|
||||||
# Build the new AppImage
|
# Build the new AppImage
|
||||||
fullname = '{:}-{:}'.format(application_name, platform.machine())
|
destination = '{:}-{:}.AppImage'.format(application_name,
|
||||||
if no_packaging:
|
platform.machine())
|
||||||
copy_tree('AppDir', Path(pwd) / fullname)
|
|
||||||
else:
|
|
||||||
destination = f'{fullname}.AppImage'
|
|
||||||
build_appimage(destination=destination)
|
build_appimage(destination=destination)
|
||||||
copy_file(destination, os.path.join(pwd, destination))
|
shutil.move(destination, os.path.join(pwd, destination))
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
import platform
|
||||||
import tarfile
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
from ...appimage import build_appimage
|
from ...appimage import build_appimage, relocate_python
|
||||||
from ...manylinux import ensure_image, PythonExtractor
|
from ...utils.docker import docker_run
|
||||||
from ...utils.fs import copy_file, copy_tree
|
from ...utils.fs import copy_tree
|
||||||
from ...utils.log import log
|
|
||||||
from ...utils.tmp import TemporaryDirectory
|
from ...utils.tmp import TemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
@@ -15,55 +16,85 @@ __all__ = ['execute']
|
|||||||
def _unpack_args(args):
|
def _unpack_args(args):
|
||||||
'''Unpack command line arguments
|
'''Unpack command line arguments
|
||||||
'''
|
'''
|
||||||
return args.tag, args.abi, args.bare, args.clean, args.no_packaging
|
return args.tag, args.abi, args.contained
|
||||||
|
|
||||||
|
|
||||||
def execute(tag, abi, bare=False, clean=False, no_packaging=False):
|
def _get_appimage_name(abi, tag):
|
||||||
'''Build a Python AppImage using a Manylinux image
|
'''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 execute(tag, abi, contained=False):
|
||||||
|
'''Build a Python AppImage using a manylinux docker image
|
||||||
'''
|
'''
|
||||||
|
|
||||||
image = ensure_image(tag, clean=clean)
|
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()
|
pwd = os.getcwd()
|
||||||
|
dirname = os.path.abspath(os.path.dirname(__file__) + '/../..')
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
python_extractor = PythonExtractor(
|
copy_tree(dirname, 'python_appimage')
|
||||||
arch = image.arch,
|
|
||||||
prefix = image.path,
|
|
||||||
tag = abi
|
|
||||||
)
|
|
||||||
appdir = Path(tmpdir) / 'AppDir'
|
|
||||||
appify = not bare
|
|
||||||
python_extractor.extract(appdir, appify=appify)
|
|
||||||
|
|
||||||
fullname = '-'.join((
|
argv = ' '.join(sys.argv[1:])
|
||||||
f'{python_extractor.impl}{python_extractor.version.long()}',
|
if tag.startswith("1_"):
|
||||||
abi,
|
# On manylinux1 tk is not installed
|
||||||
f'{image.tag}_{image.arch}'
|
script = [
|
||||||
))
|
'yum --disablerepo="*" --enablerepo=base install -q -y tk']
|
||||||
|
|
||||||
if no_packaging:
|
|
||||||
copy_tree(
|
|
||||||
Path(tmpdir) / 'AppDir',
|
|
||||||
Path(pwd) / fullname
|
|
||||||
)
|
|
||||||
elif bare:
|
|
||||||
log('COMPRESS', fullname)
|
|
||||||
destination = f'{fullname}.tar.gz'
|
|
||||||
tar_path = Path(tmpdir) / destination
|
|
||||||
with tarfile.open(tar_path, "w:gz") as tar:
|
|
||||||
tar.add(appdir, arcname=fullname)
|
|
||||||
copy_file(
|
|
||||||
tar_path,
|
|
||||||
Path(pwd) / destination
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
destination = f'{fullname}.AppImage'
|
# tk is already installed on other platforms
|
||||||
build_appimage(
|
script = []
|
||||||
appdir = str(appdir),
|
script += [
|
||||||
arch = str(image.arch),
|
python + ' -m python_appimage ' + argv + ' --contained',
|
||||||
destination = destination
|
''
|
||||||
)
|
]
|
||||||
copy_file(
|
docker_run(image, script)
|
||||||
Path(tmpdir) / destination,
|
|
||||||
Path(pwd) / destination
|
appimage_name = _get_appimage_name(abi, tag)
|
||||||
|
|
||||||
|
if tag.startswith('1_') or tag.startswith('2010_'):
|
||||||
|
# appimagetool does not run on manylinux1 (CentOS 5) or
|
||||||
|
# manylinux2010 (CentOS 6). Below is a patch for these specific
|
||||||
|
# cases.
|
||||||
|
arch = tag.split('_', 1)[-1]
|
||||||
|
if arch == platform.machine():
|
||||||
|
# Pack the image directly from the host
|
||||||
|
build_appimage(destination=appimage_name)
|
||||||
|
else:
|
||||||
|
# Use a manylinux2014 Docker image (CentOS 7) in order to
|
||||||
|
# pack the image.
|
||||||
|
script = (
|
||||||
|
'python -m python_appimage ' + argv + ' --contained',
|
||||||
|
''
|
||||||
)
|
)
|
||||||
|
docker_run('quay.io/pypa/manylinux2014_' + arch, script)
|
||||||
|
|
||||||
|
shutil.move(appimage_name, os.path.join(pwd, appimage_name))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We are running within a manylinux Docker image
|
||||||
|
is_manylinux_old = tag.startswith('1_') or tag.startswith('2010_')
|
||||||
|
|
||||||
|
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_manylinux_old = False
|
||||||
|
|
||||||
|
if is_manylinux_old:
|
||||||
|
# Build only the AppDir when running within a manylinux1 Docker
|
||||||
|
# image because appimagetool does not support CentOS 5 or CentOS 6.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
build_appimage(destination=_get_appimage_name(abi, tag))
|
||||||
|
|||||||
64
python_appimage/commands/cache/clean.py
vendored
64
python_appimage/commands/cache/clean.py
vendored
@@ -1,64 +0,0 @@
|
|||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from ...utils.deps import CACHE_DIR
|
|
||||||
from ...utils.fs import remove_file, remove_tree
|
|
||||||
from ...utils.log import log
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['execute']
|
|
||||||
|
|
||||||
|
|
||||||
def _unpack_args(args):
|
|
||||||
'''Unpack command line arguments
|
|
||||||
'''
|
|
||||||
return (args.tags, args.all)
|
|
||||||
|
|
||||||
|
|
||||||
def execute(images, all_):
|
|
||||||
'''Clean cached image(s)
|
|
||||||
'''
|
|
||||||
|
|
||||||
cache = Path(CACHE_DIR)
|
|
||||||
|
|
||||||
if not images:
|
|
||||||
images = [image[9:] for image in sorted(os.listdir(cache /
|
|
||||||
'share/images'))]
|
|
||||||
|
|
||||||
for image in images:
|
|
||||||
try:
|
|
||||||
image, tag = image.rsplit(':', 1)
|
|
||||||
except ValueError:
|
|
||||||
tag = None
|
|
||||||
|
|
||||||
if not image.replace('_', '').isalnum():
|
|
||||||
raise ValueError(f'bad image tag ({image})')
|
|
||||||
|
|
||||||
path = cache / f'share/images/manylinux{image}'
|
|
||||||
if not path.exists():
|
|
||||||
raise ValueError(f'no such image ({image})')
|
|
||||||
|
|
||||||
if tag is None:
|
|
||||||
if not all_:
|
|
||||||
path = path / 'extracted'
|
|
||||||
remove_tree(str(path))
|
|
||||||
else:
|
|
||||||
tag_file = path / f'tags/{tag}.json'
|
|
||||||
if not tag_file.exists():
|
|
||||||
raise ValueError(f'no such image ({image}:{tag})')
|
|
||||||
|
|
||||||
if all_:
|
|
||||||
with tag_file.open() as f:
|
|
||||||
layers = json.load(f)["layers"]
|
|
||||||
for layer in layers:
|
|
||||||
layer = path / f'layers/{layer}.tar.gz'
|
|
||||||
if layer.exists():
|
|
||||||
remove_file(str(layer))
|
|
||||||
remove_file(str(tag_file))
|
|
||||||
else:
|
|
||||||
path = cache / f'share/images/{image}/extracted/{tag}'
|
|
||||||
if path.exists():
|
|
||||||
remove_tree(str(path))
|
|
||||||
18
python_appimage/commands/cache/get.py
vendored
18
python_appimage/commands/cache/get.py
vendored
@@ -1,18 +0,0 @@
|
|||||||
from ...manylinux import ensure_image
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['execute']
|
|
||||||
|
|
||||||
|
|
||||||
def _unpack_args(args):
|
|
||||||
'''Unpack command line arguments
|
|
||||||
'''
|
|
||||||
return (args.tags, args.extract)
|
|
||||||
|
|
||||||
|
|
||||||
def execute(images, extract):
|
|
||||||
'''Download image(s) to the cache
|
|
||||||
'''
|
|
||||||
|
|
||||||
for image in images:
|
|
||||||
ensure_image(image, extract=extract)
|
|
||||||
41
python_appimage/commands/cache/list.py
vendored
41
python_appimage/commands/cache/list.py
vendored
@@ -1,41 +0,0 @@
|
|||||||
import glob
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from ...utils.deps import CACHE_DIR
|
|
||||||
from ...utils.log import log
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['execute']
|
|
||||||
|
|
||||||
|
|
||||||
def _unpack_args(args):
|
|
||||||
'''Unpack command line arguments
|
|
||||||
'''
|
|
||||||
return tuple()
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
'''List cached image(s)
|
|
||||||
'''
|
|
||||||
|
|
||||||
cache = Path(CACHE_DIR)
|
|
||||||
|
|
||||||
images = sorted(os.listdir(cache / 'share/images'))
|
|
||||||
for image in images:
|
|
||||||
tags = ', '.join((
|
|
||||||
tag[:-5] for tag in \
|
|
||||||
sorted(os.listdir(cache / f'share/images/{image}/tags'))
|
|
||||||
))
|
|
||||||
if not tags:
|
|
||||||
continue
|
|
||||||
path = cache / f'share/images/{image}'
|
|
||||||
memory = _getsize(path)
|
|
||||||
log('LIST', f'{image} ({tags}) [{memory}]')
|
|
||||||
|
|
||||||
|
|
||||||
def _getsize(path: Path):
|
|
||||||
r = subprocess.run(f'du -sh {path}', capture_output=True, check=True,
|
|
||||||
shell=True)
|
|
||||||
return r.stdout.decode().split(None, 1)[0]
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import glob
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from ..manylinux import ensure_image, PythonVersion
|
|
||||||
from ..utils.log import log
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['execute']
|
|
||||||
|
|
||||||
|
|
||||||
def _unpack_args(args):
|
|
||||||
'''Unpack command line arguments
|
|
||||||
'''
|
|
||||||
return (args.tag,)
|
|
||||||
|
|
||||||
|
|
||||||
def execute(tag):
|
|
||||||
'''List python versions installed in a manylinux image
|
|
||||||
'''
|
|
||||||
|
|
||||||
image = ensure_image(tag)
|
|
||||||
|
|
||||||
pythons = []
|
|
||||||
for path in glob.glob(str(image.path / 'opt/python/cp*')):
|
|
||||||
path = Path(path)
|
|
||||||
version = PythonVersion.from_str(path.readlink().name[8:]).long()
|
|
||||||
pythons.append((path.name, version))
|
|
||||||
pythons = sorted(pythons)
|
|
||||||
|
|
||||||
n = max(len(version) for (_, version) in pythons)
|
|
||||||
for (abi, version) in pythons:
|
|
||||||
log('LIST', "{:{n}} -> /opt/python/{:}".format(version, abi, n=n))
|
|
||||||
|
|
||||||
return pythons
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from ..utils import deps
|
from ..utils import deps
|
||||||
|
from ..utils.log import log
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['execute']
|
__all__ = ['execute']
|
||||||
@@ -15,9 +16,6 @@ def _unpack_args(args):
|
|||||||
def execute(binary):
|
def execute(binary):
|
||||||
'''Print the location of a binary dependency
|
'''Print the location of a binary dependency
|
||||||
'''
|
'''
|
||||||
if binary == 'appimagetool':
|
|
||||||
path = deps.ensure_appimagetool(dry=True)
|
|
||||||
else:
|
|
||||||
path = os.path.join(os.path.dirname(deps.PATCHELF), binary)
|
path = os.path.join(os.path.dirname(deps.PATCHELF), binary)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
print(path)
|
print(path)
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
Copyright (c) Université Clermont Auvergne, CNRS/IN2P3, LPC
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
# If running from an extracted image, then export ARGV0 and APPDIR
|
# If running from an extracted image, then export ARGV0 and APPDIR
|
||||||
if [ -z "${APPIMAGE}" ]; then
|
if [ -z "${APPIMAGE}" ]; then
|
||||||
export ARGV0="$0"
|
export ARGV0=$0
|
||||||
|
|
||||||
self=$(readlink -f -- "$0") # Protect spaces (issue 55)
|
self="$(readlink -f -- $0)"
|
||||||
here="${self%/*}"
|
here="${self%/*}"
|
||||||
tmp="${here%/*}"
|
tmp="${here%/*}"
|
||||||
export APPDIR="${tmp%/*}"
|
export APPDIR="${tmp%/*}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve the calling command (preserving symbolic links).
|
# Resolve the calling command (preserving symbolic links).
|
||||||
export APPIMAGE_COMMAND=$(command -v -- "$ARGV0")
|
export APPIMAGE_COMMAND="$(command -v -- $ARGV0)"
|
||||||
{{ tcltk-env }}
|
{{ tcltk-env }}
|
||||||
{{ cert-file }}
|
{{ cert-file }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
from .config import Arch, LinuxTag, PythonImpl, PythonVersion
|
|
||||||
from .download import Downloader
|
|
||||||
from .extract import ImageExtractor, PythonExtractor
|
|
||||||
from .patch import Patcher
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Arch', 'Downloader', 'ensure_image', 'ImageExtractor', 'LinuxTag',
|
|
||||||
'Patcher', 'PythonExtractor', 'PythonImpl', 'PythonVersion']
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_image(tag, *, clean=False, extract=True):
|
|
||||||
'''Download a manylinux image to the cache'''
|
|
||||||
|
|
||||||
try:
|
|
||||||
tag, image_tag = tag.rsplit(':', 1)
|
|
||||||
except ValueError:
|
|
||||||
image_tag = 'latest'
|
|
||||||
|
|
||||||
if tag.startswith('2_'):
|
|
||||||
tag, arch = tag[2:].split('_', 1)
|
|
||||||
tag = f'2_{tag}'
|
|
||||||
else:
|
|
||||||
tag, arch = tag.split('_', 1)
|
|
||||||
tag = LinuxTag.from_brief(tag)
|
|
||||||
arch = Arch.from_str(arch)
|
|
||||||
|
|
||||||
downloader = Downloader(tag=tag, arch=arch)
|
|
||||||
downloader.download(tag=image_tag)
|
|
||||||
|
|
||||||
if extract:
|
|
||||||
image_extractor = ImageExtractor(
|
|
||||||
prefix = downloader.default_destination(),
|
|
||||||
tag = image_tag
|
|
||||||
)
|
|
||||||
image_extractor.extract(clean=clean)
|
|
||||||
|
|
||||||
patcher = Patcher(tag=tag, arch=arch)
|
|
||||||
patcher.patch(destination = image_extractor.default_destination())
|
|
||||||
|
|
||||||
return SimpleNamespace(
|
|
||||||
arch = arch,
|
|
||||||
tag = tag,
|
|
||||||
path = image_extractor.default_destination(),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return SimpleNamespace(
|
|
||||||
arch = arch,
|
|
||||||
tag = tag,
|
|
||||||
path = downloader.default_destination(),
|
|
||||||
)
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
from enum import auto, Enum
|
|
||||||
import platform
|
|
||||||
from typing import NamedTuple, Optional, Union
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Arch', 'PythonImpl', 'PythonVersion']
|
|
||||||
|
|
||||||
|
|
||||||
class Arch(Enum):
|
|
||||||
'''Supported platform architectures.'''
|
|
||||||
AARCH64 = auto()
|
|
||||||
I686 = auto()
|
|
||||||
X86_64 = auto()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name.lower()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_host(cls) -> 'Arch':
|
|
||||||
return cls.from_str(platform.machine())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_str(cls, value) -> 'Arch':
|
|
||||||
for arch in cls:
|
|
||||||
if value == str(arch):
|
|
||||||
return arch
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(value)
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxTag(Enum):
|
|
||||||
'''Supported platform tags.'''
|
|
||||||
MANYLINUX_1 = auto()
|
|
||||||
MANYLINUX_2010 = auto()
|
|
||||||
MANYLINUX_2014 = auto()
|
|
||||||
MANYLINUX_2_24 = auto()
|
|
||||||
MANYLINUX_2_28 = auto()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
tag = self.name.lower()
|
|
||||||
if self in (LinuxTag.MANYLINUX_1, LinuxTag.MANYLINUX_2010,
|
|
||||||
LinuxTag.MANYLINUX_2014):
|
|
||||||
return tag.replace('_', '')
|
|
||||||
else:
|
|
||||||
return tag
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_str(cls, value) -> 'LinuxTag':
|
|
||||||
for tag in cls:
|
|
||||||
if value == str(tag):
|
|
||||||
return tag
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_brief(cls, value) -> 'LinuxTag':
|
|
||||||
if value.startswith('2_'):
|
|
||||||
return cls.from_str('manylinux_' + value)
|
|
||||||
else:
|
|
||||||
return cls.from_str('manylinux' + value)
|
|
||||||
|
|
||||||
|
|
||||||
class PythonImpl(Enum):
|
|
||||||
'''Supported Python implementations.'''
|
|
||||||
CPYTHON = auto()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'python'
|
|
||||||
|
|
||||||
|
|
||||||
class PythonVersion(NamedTuple):
|
|
||||||
''''''
|
|
||||||
|
|
||||||
major: int
|
|
||||||
minor: int
|
|
||||||
patch: Union[int, str]
|
|
||||||
flavour: Optional[str]=None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_str(cls, value: str) -> 'PythonVersion':
|
|
||||||
major, minor, patch = value.split('.', 2)
|
|
||||||
try:
|
|
||||||
patch, flavour = patch.split('-', 1)
|
|
||||||
except ValueError:
|
|
||||||
flavour = None
|
|
||||||
else:
|
|
||||||
if flavour == 'nogil':
|
|
||||||
flavour = 't'
|
|
||||||
elif flavour == 'ucs2':
|
|
||||||
flavour = 'm'
|
|
||||||
elif flavour == 'ucs4':
|
|
||||||
flavour = 'mu'
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(value)
|
|
||||||
try:
|
|
||||||
patch = int(patch)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return cls(int(major), int(minor), patch, flavour)
|
|
||||||
|
|
||||||
def flavoured(self) -> str:
|
|
||||||
flavour = self.flavour if self.flavour == 't' else ''
|
|
||||||
return f'{self.major}.{self.minor}{flavour}'
|
|
||||||
|
|
||||||
def long(self) -> str:
|
|
||||||
return f'{self.major}.{self.minor}.{self.patch}'
|
|
||||||
|
|
||||||
def short(self) -> str:
|
|
||||||
return f'{self.major}.{self.minor}'
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
import glob
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import requests
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .config import Arch, LinuxTag
|
|
||||||
from ..utils.deps import CACHE_DIR
|
|
||||||
from ..utils.log import debug, log
|
|
||||||
|
|
||||||
|
|
||||||
CHUNK_SIZE = 8189
|
|
||||||
|
|
||||||
SUCCESS = 200
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Downloader:
|
|
||||||
|
|
||||||
'''Manylinux tag.'''
|
|
||||||
tag: LinuxTag
|
|
||||||
|
|
||||||
'''Platform architecture.'''
|
|
||||||
arch: Optional[Arch] = None
|
|
||||||
|
|
||||||
'''Docker image.'''
|
|
||||||
image: str = field(init=False)
|
|
||||||
|
|
||||||
'''Authentication token.'''
|
|
||||||
token: str = field(init=False)
|
|
||||||
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
# Set host arch if not explictly specified.
|
|
||||||
if self.arch is None:
|
|
||||||
arch = Arch.from_host()
|
|
||||||
object.__setattr__(self, 'arch', arch)
|
|
||||||
|
|
||||||
# Set image name.
|
|
||||||
image = f'{self.tag}_{self.arch}'
|
|
||||||
object.__setattr__(self, 'image', image)
|
|
||||||
|
|
||||||
|
|
||||||
def default_destination(self):
|
|
||||||
return Path(CACHE_DIR) / f'share/images/{self.image}'
|
|
||||||
|
|
||||||
|
|
||||||
def download(
|
|
||||||
self,
|
|
||||||
destination: Optional[Path]=None,
|
|
||||||
*,
|
|
||||||
tag: Optional[str] = 'latest'
|
|
||||||
):
|
|
||||||
'''Download Manylinux image'''
|
|
||||||
|
|
||||||
destination = destination or self.default_destination()
|
|
||||||
|
|
||||||
# Authenticate to quay.io.
|
|
||||||
repository = f'pypa/{self.image}'
|
|
||||||
log('PULL', f'{self.image}:{tag}')
|
|
||||||
url = 'https://quay.io/v2/auth'
|
|
||||||
url = f'{url}?service=quay.io&scope=repository:{repository}:pull'
|
|
||||||
debug('GET', url)
|
|
||||||
r = requests.request('GET', url)
|
|
||||||
if r.status_code == SUCCESS:
|
|
||||||
object.__setattr__(self, 'token', r.json()['token'])
|
|
||||||
else:
|
|
||||||
raise DownloadError(r.status_code, r.text, r.headers)
|
|
||||||
|
|
||||||
# Fetch image manifest.
|
|
||||||
repository = f'pypa/{self.image}'
|
|
||||||
url = f'https://quay.io/v2/{repository}/manifests/{tag}'
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'Bearer {self.token}',
|
|
||||||
'Accept': 'application/vnd.docker.distribution.manifest.v2+json'
|
|
||||||
}
|
|
||||||
debug('GET', url)
|
|
||||||
r = requests.request('GET', url, headers=headers)
|
|
||||||
if r.status_code == SUCCESS:
|
|
||||||
image_digest = r.headers['Docker-Content-Digest'].split(':', 1)[-1]
|
|
||||||
manifest = r.json()
|
|
||||||
else:
|
|
||||||
raise DownloadError(r.status_code, r.text, r.headers)
|
|
||||||
|
|
||||||
# Check missing layers to download.
|
|
||||||
required = [layer['digest'].split(':', 1)[-1] for layer in
|
|
||||||
manifest['layers']]
|
|
||||||
|
|
||||||
missing = []
|
|
||||||
for hash_ in required:
|
|
||||||
path = destination / f'layers/{hash_}.tar.gz'
|
|
||||||
if path.exists():
|
|
||||||
hasher = hashlib.sha256()
|
|
||||||
with path.open('rb') as f:
|
|
||||||
while True:
|
|
||||||
chunk = f.read(CHUNK_SIZE)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
hasher.update(chunk)
|
|
||||||
h = hasher.hexdigest()
|
|
||||||
if h != hash_:
|
|
||||||
missing.append(hash_)
|
|
||||||
else:
|
|
||||||
debug('FOUND', f'{hash_}.tar.gz')
|
|
||||||
else:
|
|
||||||
missing.append(hash_)
|
|
||||||
|
|
||||||
# Fetch missing layers.
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
workdir = Path(tmpdir)
|
|
||||||
for i, hash_ in enumerate(missing):
|
|
||||||
debug('DOWNLOAD', f'{self.image}:{tag} '
|
|
||||||
f'[{i + 1} / {len(missing)}]')
|
|
||||||
|
|
||||||
filename = f'{hash_}.tar.gz'
|
|
||||||
url = f'https://quay.io/v2/{repository}/blobs/sha256:{hash_}'
|
|
||||||
debug('GET', url)
|
|
||||||
r = requests.request('GET', url, headers=headers, stream=True)
|
|
||||||
if r.status_code == SUCCESS:
|
|
||||||
debug('STREAM', filename)
|
|
||||||
else:
|
|
||||||
raise DownloadError(r.status_code, r.text, r.headers)
|
|
||||||
|
|
||||||
hasher = hashlib.sha256()
|
|
||||||
tmp = workdir / 'layer.tgz'
|
|
||||||
with open(tmp, "wb") as f:
|
|
||||||
for chunk in r.iter_content(CHUNK_SIZE):
|
|
||||||
if chunk:
|
|
||||||
f.write(chunk)
|
|
||||||
hasher.update(chunk)
|
|
||||||
|
|
||||||
h = hasher.hexdigest()
|
|
||||||
if h != hash_:
|
|
||||||
raise DownloadError(
|
|
||||||
f'bad hash (expected {hash_}, found {h})'
|
|
||||||
)
|
|
||||||
layers_dir = destination / 'layers'
|
|
||||||
layers_dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
shutil.move(tmp, layers_dir / filename)
|
|
||||||
|
|
||||||
tags_dir = destination / 'tags'
|
|
||||||
tags_dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
with open(tags_dir / f'{tag}.json', "w") as f:
|
|
||||||
json.dump({'digest': image_digest, 'layers': required}, f)
|
|
||||||
|
|
||||||
# Remove unused layers.
|
|
||||||
required = set(required)
|
|
||||||
for tag in glob.glob(str(destination / 'tags/*.json')):
|
|
||||||
with open(tag) as f:
|
|
||||||
tag = json.load(f)
|
|
||||||
required |= set(tag["layers"])
|
|
||||||
required = [f'{hash_}.tar.gz' for hash_ in required]
|
|
||||||
|
|
||||||
for layer in glob.glob(str(destination / 'layers/*.tar.gz')):
|
|
||||||
layer = Path(layer)
|
|
||||||
if layer.name not in required:
|
|
||||||
debug('REMOVE', f'{self.image} [layer/{layer.stem}]')
|
|
||||||
layer.unlink()
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
import atexit
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
import stat
|
|
||||||
import subprocess
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from .config import Arch, PythonImpl, PythonVersion
|
|
||||||
from ..appimage import Appifier
|
|
||||||
from ..utils.deps import ensure_excludelist, ensure_patchelf, EXCLUDELIST, \
|
|
||||||
PATCHELF
|
|
||||||
from ..utils.log import debug, log
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class PythonExtractor:
|
|
||||||
'''Python extractor from an extracted Manylinux image.'''
|
|
||||||
|
|
||||||
arch: Arch
|
|
||||||
'''Target architecture'''
|
|
||||||
|
|
||||||
prefix: Path
|
|
||||||
'''Target image path'''
|
|
||||||
|
|
||||||
tag: str
|
|
||||||
'''Python binary tag'''
|
|
||||||
|
|
||||||
|
|
||||||
excludelist: Optional[Path] = None
|
|
||||||
'''Exclude list for shared libraries.'''
|
|
||||||
|
|
||||||
patchelf: Optional[Path] = None
|
|
||||||
'''Patchelf executable.'''
|
|
||||||
|
|
||||||
|
|
||||||
excluded: List[str] = field(init=False)
|
|
||||||
'''Excluded shared libraries.'''
|
|
||||||
|
|
||||||
impl: PythonImpl = field(init=False)
|
|
||||||
'''Python implementation'''
|
|
||||||
|
|
||||||
library_path: List[str] = field(init=False)
|
|
||||||
'''Search paths for libraries (LD_LIBRARY_PATH)'''
|
|
||||||
|
|
||||||
python_prefix: Path = field(init=False)
|
|
||||||
'''Python installation prefix'''
|
|
||||||
|
|
||||||
version: PythonVersion = field(init=False)
|
|
||||||
'''Python version'''
|
|
||||||
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
# Locate Python installation.
|
|
||||||
link = os.readlink(self.prefix / f'opt/python/{self.tag}')
|
|
||||||
if not link.startswith('/'):
|
|
||||||
raise NotImplementedError()
|
|
||||||
object.__setattr__(self, 'python_prefix', self.prefix / link[1:])
|
|
||||||
|
|
||||||
# Parse implementation and version.
|
|
||||||
head, tail = Path(link).name.split('-', 1)
|
|
||||||
if head == 'cpython':
|
|
||||||
impl = PythonImpl.CPYTHON
|
|
||||||
version = PythonVersion.from_str(tail)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError()
|
|
||||||
object.__setattr__(self, 'impl', impl)
|
|
||||||
object.__setattr__(self, 'version', version)
|
|
||||||
|
|
||||||
# Set libraries search path.
|
|
||||||
paths = []
|
|
||||||
if self.arch in (Arch.AARCH64, Arch.X86_64):
|
|
||||||
paths.append(self.prefix / 'lib64')
|
|
||||||
paths.append(self.prefix / 'usr/lib64')
|
|
||||||
if self.arch == Arch.X86_64:
|
|
||||||
paths.append(self.prefix / 'lib/x86_64-linux-gnu')
|
|
||||||
paths.append(self.prefix / 'usr/lib/x86_64-linux-gnu')
|
|
||||||
else:
|
|
||||||
paths.append(self.prefix / 'lib/aarch64-linux-gnu')
|
|
||||||
paths.append(self.prefix / 'usr/lib/aarch64-linux-gnu')
|
|
||||||
elif self.arch == Arch.I686:
|
|
||||||
paths.append(self.prefix / 'lib')
|
|
||||||
paths.append(self.prefix / 'usr/lib')
|
|
||||||
paths.append(self.prefix / 'lib/i386-linux-gnu')
|
|
||||||
paths.append(self.prefix / 'usr/lib/i386-linux-gnu')
|
|
||||||
else:
|
|
||||||
raise NotImplementedError()
|
|
||||||
paths.append(self.prefix / 'usr/local/lib')
|
|
||||||
|
|
||||||
patterns = (
|
|
||||||
'curl-*',
|
|
||||||
'mpdecimal-*',
|
|
||||||
'openssl-*',
|
|
||||||
'sqlite*',
|
|
||||||
)
|
|
||||||
for pattern in patterns:
|
|
||||||
pattern = str(self.prefix / f'opt/_internal/{pattern}/lib')
|
|
||||||
for match in glob.glob(pattern):
|
|
||||||
paths.append(Path(match))
|
|
||||||
|
|
||||||
object.__setattr__(self, 'library_path', paths)
|
|
||||||
|
|
||||||
# Set excluded libraries.
|
|
||||||
if self.excludelist:
|
|
||||||
excludelist = Path(self.excludelist)
|
|
||||||
else:
|
|
||||||
ensure_excludelist()
|
|
||||||
excludelist = Path(EXCLUDELIST)
|
|
||||||
excluded = set()
|
|
||||||
with excludelist.open() as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line and not line.startswith('#'):
|
|
||||||
excluded.add(line)
|
|
||||||
excluded.add('ld-linux-aarch64.so.1') # patch for aarch64.
|
|
||||||
object.__setattr__(self, 'excluded', excluded)
|
|
||||||
|
|
||||||
# Set patchelf, if not provided.
|
|
||||||
if self.patchelf is None:
|
|
||||||
ensure_patchelf()
|
|
||||||
object.__setattr__(self, 'patchelf', PATCHELF)
|
|
||||||
else:
|
|
||||||
assert(self.patchelf.exists())
|
|
||||||
|
|
||||||
|
|
||||||
def extract(
|
|
||||||
self,
|
|
||||||
destination: Path,
|
|
||||||
*,
|
|
||||||
appify: Optional[bool]=False,
|
|
||||||
python_prefix: Optional[str]=None,
|
|
||||||
system_prefix: Optional[str]=None,
|
|
||||||
):
|
|
||||||
'''Extract Python runtime.'''
|
|
||||||
|
|
||||||
python = f'python{self.version.short()}'
|
|
||||||
flavoured_python = f'python{self.version.flavoured()}'
|
|
||||||
runtime = f'bin/{flavoured_python}'
|
|
||||||
packages = f'lib/{flavoured_python}'
|
|
||||||
pip = f'bin/pip{self.version.short()}'
|
|
||||||
|
|
||||||
if python_prefix is None:
|
|
||||||
python_prefix = f'opt/{flavoured_python}'
|
|
||||||
|
|
||||||
if system_prefix is None:
|
|
||||||
system_prefix = 'usr'
|
|
||||||
|
|
||||||
python_dest = destination / python_prefix
|
|
||||||
system_dest = destination / system_prefix
|
|
||||||
|
|
||||||
# Locate include files.
|
|
||||||
include = glob.glob(str(self.python_prefix / 'include/*'))
|
|
||||||
if include:
|
|
||||||
include = Path(include[0]).name
|
|
||||||
include = f'include/{include}'
|
|
||||||
else:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
# Clone Python runtime.
|
|
||||||
log('CLONE',
|
|
||||||
f'{python} from {self.python_prefix.relative_to(self.prefix)}')
|
|
||||||
(python_dest / 'bin').mkdir(exist_ok=True, parents=True)
|
|
||||||
shutil.copy(self.python_prefix / runtime, python_dest / runtime)
|
|
||||||
|
|
||||||
# Clone pip wrapper.
|
|
||||||
with open(self.python_prefix / pip) as f:
|
|
||||||
f.readline() # Skip shebang.
|
|
||||||
body = f.read()
|
|
||||||
|
|
||||||
with open(python_dest / pip, 'w') as f:
|
|
||||||
f.write('#! /bin/sh\n')
|
|
||||||
f.write(' '.join((
|
|
||||||
'"exec"',
|
|
||||||
f'"$(dirname $(readlink -f ${0}))/{flavoured_python}"',
|
|
||||||
'"$0"',
|
|
||||||
'"$@"\n'
|
|
||||||
)))
|
|
||||||
f.write(body)
|
|
||||||
shutil.copymode(self.python_prefix / pip, python_dest / pip)
|
|
||||||
|
|
||||||
# Clone Python packages.
|
|
||||||
for folder in (packages, include):
|
|
||||||
shutil.copytree(self.python_prefix / folder, python_dest / folder,
|
|
||||||
symlinks=True, dirs_exist_ok=True)
|
|
||||||
|
|
||||||
# Remove some clutters.
|
|
||||||
log('PRUNE', '%s packages', python)
|
|
||||||
shutil.rmtree(python_dest / packages / 'test', ignore_errors=True)
|
|
||||||
for root, dirs, files in os.walk(python_dest / packages):
|
|
||||||
root = Path(root)
|
|
||||||
for d in dirs:
|
|
||||||
if d == '__pycache__':
|
|
||||||
shutil.rmtree(root / d, ignore_errors=True)
|
|
||||||
for f in files:
|
|
||||||
if f.endswith('.pyc'):
|
|
||||||
(root / f).unlink()
|
|
||||||
|
|
||||||
# Map binary dependencies.
|
|
||||||
libs = self.ldd(self.python_prefix / f'bin/{flavoured_python}')
|
|
||||||
path = Path(self.python_prefix / f'{packages}/lib-dynload')
|
|
||||||
for module in glob.glob(str(path / "*.so")):
|
|
||||||
l = self.ldd(module)
|
|
||||||
libs.update(l)
|
|
||||||
|
|
||||||
# Copy and patch binary dependencies.
|
|
||||||
libdir = system_dest / 'lib'
|
|
||||||
libdir.mkdir(exist_ok=True, parents=True)
|
|
||||||
|
|
||||||
for (name, src) in libs.items():
|
|
||||||
dst = libdir / name
|
|
||||||
shutil.copy(src, dst, follow_symlinks=True)
|
|
||||||
# Some libraries are read-only, which prevents overriding the
|
|
||||||
# destination directory. Below, we change the permission of
|
|
||||||
# destination files to read-write (for the owner).
|
|
||||||
mode = dst.stat().st_mode
|
|
||||||
if not (mode & stat.S_IWUSR):
|
|
||||||
mode = mode | stat.S_IWUSR
|
|
||||||
dst.chmod(mode)
|
|
||||||
|
|
||||||
self.set_rpath(dst, '$ORIGIN')
|
|
||||||
|
|
||||||
# Patch RPATHs of binary modules.
|
|
||||||
log('LINK', '%s C-extensions', python)
|
|
||||||
path = Path(python_dest / f'{packages}/lib-dynload')
|
|
||||||
for module in glob.glob(str(path / "*.so")):
|
|
||||||
src = Path(module)
|
|
||||||
dst = os.path.relpath(libdir, src.parent)
|
|
||||||
self.set_rpath(src, f'$ORIGIN/{dst}')
|
|
||||||
|
|
||||||
# Patch RPATHs of Python runtime.
|
|
||||||
src = python_dest / runtime
|
|
||||||
dst = os.path.relpath(libdir, src.parent)
|
|
||||||
self.set_rpath(src, f'$ORIGIN/{dst}')
|
|
||||||
|
|
||||||
# Copy SSL certificates (i.e. clone certifi).
|
|
||||||
certs = self.prefix / 'opt/_internal/certs.pem'
|
|
||||||
if certs.is_symlink():
|
|
||||||
dst = self.prefix / str(certs.readlink())[1:]
|
|
||||||
certifi = dst.parent
|
|
||||||
assert(certifi.name == 'certifi')
|
|
||||||
site_packages = certifi.parent
|
|
||||||
assert(site_packages.name == 'site-packages')
|
|
||||||
log('INSTALL', certifi.name)
|
|
||||||
|
|
||||||
matches = [
|
|
||||||
Path(src) for src in glob.glob(str(site_packages / 'certifi*'))
|
|
||||||
]
|
|
||||||
matches = sorted(matches, key=lambda src: src.name)
|
|
||||||
cert_src = None
|
|
||||||
for src in matches:
|
|
||||||
dst = python_dest / f'{packages}/site-packages/{src.name}'
|
|
||||||
if not dst.exists():
|
|
||||||
shutil.copytree(src, dst, symlinks=True)
|
|
||||||
if cert_src is None:
|
|
||||||
cacert_pem = dst / 'cacert.pem'
|
|
||||||
if cacert_pem.exists():
|
|
||||||
cert_src = cacert_pem
|
|
||||||
assert(cert_src is not None)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
# Copy Tcl & Tk data.
|
|
||||||
tx_version = []
|
|
||||||
for match in glob.glob(str(system_dest / 'lib/libtk*')):
|
|
||||||
path = system_dest / f'lib/{match}'
|
|
||||||
tx_version.append(LooseVersion(path.name[5:8]))
|
|
||||||
|
|
||||||
if tx_version:
|
|
||||||
tx_version.sort()
|
|
||||||
tx_version = tx_version[-1]
|
|
||||||
|
|
||||||
for location in ('usr/local/lib', 'usr/share', 'usr/share/tcltk'):
|
|
||||||
tcltk_src = self.prefix / location
|
|
||||||
path = tcltk_src / f'tk{tx_version}'
|
|
||||||
if path.exists() and path.is_dir():
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(f'could not locate Tcl/Tk{tx_version}')
|
|
||||||
|
|
||||||
log('INSTALL', f'Tcl/Tk{tx_version}')
|
|
||||||
tcltk_dir = Path(system_dest / 'share/tcltk')
|
|
||||||
tcltk_dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
|
|
||||||
for tx in ('tcl', 'tk'):
|
|
||||||
name = f'{tx}{tx_version}'
|
|
||||||
src = tcltk_src / name
|
|
||||||
dst = tcltk_dir / name
|
|
||||||
shutil.copytree(src, dst, symlinks=True, dirs_exist_ok=True)
|
|
||||||
|
|
||||||
if appify:
|
|
||||||
appifier = Appifier(
|
|
||||||
appdir = str(destination),
|
|
||||||
appdir_bin = str(system_dest / 'bin'),
|
|
||||||
python_bin = str(python_dest / 'bin'),
|
|
||||||
python_pkg = str(python_dest / packages),
|
|
||||||
version = self.version,
|
|
||||||
tk_version = tx_version,
|
|
||||||
cert_src = cert_src
|
|
||||||
)
|
|
||||||
appifier.appify()
|
|
||||||
|
|
||||||
|
|
||||||
def ldd(self, target: Path) -> Dict[str, Path]:
|
|
||||||
'''Cross-platform implementation of ldd, using readelf.'''
|
|
||||||
|
|
||||||
pattern = re.compile(r'[(]NEEDED[)]\s+Shared library:\s+\[([^\]]+)\]')
|
|
||||||
dependencies = dict()
|
|
||||||
|
|
||||||
def recurse(target: Path):
|
|
||||||
result = subprocess.run(f'readelf -d {target}', shell=True,
|
|
||||||
check=True, capture_output=True)
|
|
||||||
stdout = result.stdout.decode()
|
|
||||||
matches = pattern.findall(stdout)
|
|
||||||
|
|
||||||
for match in matches:
|
|
||||||
if (match not in dependencies) and (match not in self.excluded):
|
|
||||||
path = self.locate_library(match)
|
|
||||||
dependencies[match] = path
|
|
||||||
recurse(path)
|
|
||||||
|
|
||||||
recurse(target)
|
|
||||||
return dependencies
|
|
||||||
|
|
||||||
|
|
||||||
def locate_library(self, name: str) -> Path:
|
|
||||||
'''Locate a library given its qualified name.'''
|
|
||||||
|
|
||||||
for dirname in self.library_path:
|
|
||||||
path = dirname / name
|
|
||||||
if path.exists():
|
|
||||||
return path
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(name)
|
|
||||||
|
|
||||||
|
|
||||||
def set_rpath(self, target, rpath):
|
|
||||||
cmd = f'{self.patchelf} --print-rpath {target}'
|
|
||||||
result = subprocess.run(cmd, shell=True, check=True,
|
|
||||||
capture_output=True)
|
|
||||||
current_rpath = result.stdout.decode().strip()
|
|
||||||
if current_rpath != rpath:
|
|
||||||
cmd = f"{self.patchelf} --set-rpath '{rpath}' {target}"
|
|
||||||
subprocess.run(cmd, shell=True, check=True, capture_output=True)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ImageExtractor:
|
|
||||||
'''Manylinux image extractor from layers.'''
|
|
||||||
|
|
||||||
prefix: Path
|
|
||||||
'''Manylinux image prefix.'''
|
|
||||||
|
|
||||||
tag: Optional[str] = 'latest'
|
|
||||||
'''Manylinux image tag.'''
|
|
||||||
|
|
||||||
|
|
||||||
def default_destination(self):
|
|
||||||
return self.prefix / f'extracted/{self.tag}'
|
|
||||||
|
|
||||||
|
|
||||||
def extract(self, destination: Optional[Path]=None, *, clean=False):
|
|
||||||
'''Extract Manylinux image.'''
|
|
||||||
|
|
||||||
if destination is None:
|
|
||||||
destination = self.default_destination()
|
|
||||||
|
|
||||||
if clean:
|
|
||||||
def clean(destination):
|
|
||||||
shutil.rmtree(destination, ignore_errors=True)
|
|
||||||
atexit.register(clean, destination)
|
|
||||||
|
|
||||||
log('EXTRACT', f'{self.prefix.name}:{self.tag}')
|
|
||||||
|
|
||||||
with open(self.prefix / f'tags/{self.tag}.json') as f:
|
|
||||||
meta = json.load(f)
|
|
||||||
layers = meta['layers']
|
|
||||||
|
|
||||||
extracted = []
|
|
||||||
extracted_file = destination / '.extracted'
|
|
||||||
if destination.exists():
|
|
||||||
clean_destination = True
|
|
||||||
if extracted_file.exists():
|
|
||||||
with extracted_file.open() as f:
|
|
||||||
extracted = f.read().split(os.linesep)[:-1]
|
|
||||||
|
|
||||||
for a, b in zip(layers, extracted):
|
|
||||||
if a != b:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
clean_destination = False
|
|
||||||
|
|
||||||
if clean_destination:
|
|
||||||
shutil.rmtree(destination, ignore_errors=True)
|
|
||||||
|
|
||||||
for i, layer in enumerate(layers):
|
|
||||||
try:
|
|
||||||
if layer == extracted[i]:
|
|
||||||
continue
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
debug('EXTRACT', f'{layer}.tar.gz')
|
|
||||||
filename = self.prefix / f'layers/{layer}.tar.gz'
|
|
||||||
cmd = ''.join((
|
|
||||||
f'trap \'chmod u+rw -R {destination}\' EXIT ; ',
|
|
||||||
f'mkdir -p {destination} && ',
|
|
||||||
f'tar -xzf {filename} --exclude=dev -C {destination} && ',
|
|
||||||
f'echo \'{layer}\' >> {extracted_file}'
|
|
||||||
))
|
|
||||||
r = subprocess.run(f'/bin/bash -c "{cmd}"', shell=True,
|
|
||||||
capture_output=True)
|
|
||||||
if r.returncode != 0:
|
|
||||||
raise ValueError(r.stderr.decode())
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
import subprocess
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from .config import Arch, LinuxTag
|
|
||||||
from ..utils.deps import CACHE_DIR
|
|
||||||
from ..utils.log import debug, log
|
|
||||||
from ..utils.url import urlretrieve
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Patcher:
|
|
||||||
'''Manylinux tag.'''
|
|
||||||
tag: LinuxTag
|
|
||||||
|
|
||||||
'''Platform architecture.'''
|
|
||||||
arch: Optional[Arch] = None
|
|
||||||
|
|
||||||
|
|
||||||
def patch(self, destination: Path):
|
|
||||||
'''Apply any patch'''
|
|
||||||
|
|
||||||
cache = Path(CACHE_DIR) / f'share/patches/'
|
|
||||||
|
|
||||||
if self.tag == LinuxTag.MANYLINUX_1:
|
|
||||||
patch = f'tk-manylinux1_{self.arch}'
|
|
||||||
log('PATCH', patch)
|
|
||||||
tarfile = f'{patch}.tar.gz'
|
|
||||||
path = cache / tarfile
|
|
||||||
if not path.exists():
|
|
||||||
url = f'https://github.com/niess/python-appimage/releases/download/manylinux1/{tarfile}'
|
|
||||||
urlretrieve(url, path)
|
|
||||||
mode = os.stat(path)[stat.ST_MODE]
|
|
||||||
os.chmod(path, mode | stat.S_IWGRP | stat.S_IWOTH)
|
|
||||||
|
|
||||||
debug('EXTRACT', tarfile)
|
|
||||||
cmd = ''.join((
|
|
||||||
f'trap \'chmod u+rw -R {destination}\' EXIT ; ',
|
|
||||||
f'mkdir -p {destination} && ',
|
|
||||||
f'tar -xzf {path} -C {destination}',
|
|
||||||
))
|
|
||||||
r = subprocess.run(f'/bin/bash -c "{cmd}"', shell=True,
|
|
||||||
capture_output=True)
|
|
||||||
if r.returncode != 0:
|
|
||||||
raise ValueError(r.stderr.decode())
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import sys
|
__all__ = ['decode']
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['decode', 'encode', 'find_spec']
|
|
||||||
|
|
||||||
|
|
||||||
def decode(s):
|
def decode(s):
|
||||||
@@ -11,30 +8,3 @@ def decode(s):
|
|||||||
return s.decode()
|
return s.decode()
|
||||||
except Exception:
|
except Exception:
|
||||||
return str(s)
|
return str(s)
|
||||||
|
|
||||||
|
|
||||||
def encode(s):
|
|
||||||
'''Encode Python 3 str as bytes
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
return s.encode()
|
|
||||||
except Exception:
|
|
||||||
return str(s)
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
|
||||||
from collections import namedtuple
|
|
||||||
import imp
|
|
||||||
|
|
||||||
ModuleSpec = namedtuple('ModuleSpec', ('name', 'origin'))
|
|
||||||
|
|
||||||
def find_spec(name):
|
|
||||||
return ModuleSpec(name, imp.find_module(name)[1])
|
|
||||||
|
|
||||||
else:
|
|
||||||
import importlib
|
|
||||||
try:
|
|
||||||
find_spec = importlib.util.find_spec
|
|
||||||
except AttributeError:
|
|
||||||
import importlib.util
|
|
||||||
find_spec = importlib.util.find_spec
|
|
||||||
|
|||||||
@@ -9,58 +9,39 @@ from .tmp import TemporaryDirectory
|
|||||||
from .url import urlretrieve
|
from .url import urlretrieve
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['APPIMAGETOOL', 'EXCLUDELIST', 'PATCHELF', 'PREFIX',
|
||||||
|
'ensure_appimagetool', 'ensure_excludelist', 'ensure_patchelf']
|
||||||
|
|
||||||
|
|
||||||
_ARCH = platform.machine()
|
_ARCH = platform.machine()
|
||||||
|
|
||||||
CACHE_DIR = os.path.expanduser('~/.cache/python-appimage')
|
|
||||||
'''Package cache location'''
|
|
||||||
|
|
||||||
PREFIX = os.path.abspath(os.path.dirname(__file__) + '/..')
|
PREFIX = os.path.abspath(os.path.dirname(__file__) + '/..')
|
||||||
'''Package installation prefix'''
|
'''Package installation prefix'''
|
||||||
|
|
||||||
APPIMAGETOOL_DIR = os.path.join(CACHE_DIR, 'bin')
|
APPIMAGETOOL = os.path.expanduser('~/.local/bin/appimagetool')
|
||||||
'''Location of the appimagetool binary'''
|
'''Location of the appimagetool binary'''
|
||||||
|
|
||||||
APPIMAGETOOL_VERSION = 'continuous'
|
EXCLUDELIST = PREFIX + '/data/excludelist'
|
||||||
'''Version of the appimagetool binary'''
|
|
||||||
|
|
||||||
EXCLUDELIST = os.path.join(CACHE_DIR, 'share/excludelist')
|
|
||||||
'''AppImage exclusion list'''
|
'''AppImage exclusion list'''
|
||||||
|
|
||||||
PATCHELF = os.path.join(CACHE_DIR, 'bin/patchelf')
|
PATCHELF = os.path.expanduser('~/.local/bin/patchelf')
|
||||||
'''Location of the PatchELF binary'''
|
'''Location of the PatchELF binary'''
|
||||||
|
|
||||||
PATCHELF_VERSION = '0.14.3'
|
|
||||||
'''Version of the patchelf binary'''
|
|
||||||
|
|
||||||
|
def ensure_appimagetool():
|
||||||
def ensure_appimagetool(dry=False):
|
|
||||||
'''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
|
|
||||||
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/' \
|
||||||
if APPIMAGETOOL_VERSION in map(str, range(1, 14)):
|
'download/12'
|
||||||
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:
|
||||||
@@ -69,7 +50,10 @@ def ensure_appimagetool(dry=False):
|
|||||||
system(('./' + appimage, '--appimage-extract'))
|
system(('./' + appimage, '--appimage-extract'))
|
||||||
copy_tree('squashfs-root', appdir)
|
copy_tree('squashfs-root', appdir)
|
||||||
|
|
||||||
return apprun
|
if not os.path.exists(APPIMAGETOOL):
|
||||||
|
os.symlink(appdir_name + '/AppRun', APPIMAGETOOL)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Installers for dependencies
|
# Installers for dependencies
|
||||||
@@ -92,18 +76,19 @@ def ensure_patchelf():
|
|||||||
if os.path.exists(PATCHELF):
|
if os.path.exists(PATCHELF):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
tgz = '-'.join(('patchelf', PATCHELF_VERSION, _ARCH)) + '.tar.gz'
|
iarch = 'i386' if _ARCH == 'i686' else _ARCH
|
||||||
baseurl = 'https://github.com/NixOS/patchelf'
|
appimage = 'patchelf-{0:}.AppImage'.format(iarch)
|
||||||
|
baseurl = 'https://github.com/niess/patchelf.appimage/releases/download'
|
||||||
log('INSTALL', 'patchelf from %s', baseurl)
|
log('INSTALL', 'patchelf from %s', baseurl)
|
||||||
|
|
||||||
dirname = os.path.dirname(PATCHELF)
|
dirname = os.path.dirname(PATCHELF)
|
||||||
patchelf = dirname + '/patchelf'
|
patchelf = dirname + '/patchelf'
|
||||||
make_tree(dirname)
|
make_tree(dirname)
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
urlretrieve(os.path.join(baseurl, 'releases', 'download',
|
urlretrieve(os.path.join(baseurl, 'rolling', appimage), appimage)
|
||||||
PATCHELF_VERSION, tgz), tgz)
|
os.chmod(appimage, stat.S_IRWXU)
|
||||||
system(('tar', 'xzf', tgz))
|
system(('./' + appimage, '--appimage-extract'))
|
||||||
copy_file('bin/patchelf', patchelf)
|
copy_file('squashfs-root/usr/bin/patchelf', patchelf)
|
||||||
os.chmod(patchelf, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
os.chmod(patchelf, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
51
python_appimage/utils/docker.py
Normal file
51
python_appimage/utils/docker.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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:
|
||||||
|
if p.returncode == 139:
|
||||||
|
sys.stderr.write("segmentation fault when running Docker (139)\n")
|
||||||
|
sys.exit(p.returncode)
|
||||||
@@ -1,30 +1,8 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,25 +7,17 @@ __all__ = ['debug', 'log']
|
|||||||
# Configure the logger
|
# Configure the logger
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='[%(asctime)s] %(message)s',
|
format='[%(asctime)s] %(message)s',
|
||||||
level=logging.ERROR
|
level=logging.INFO
|
||||||
)
|
)
|
||||||
logging.getLogger('python-appimage').setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def log(task, fmt, *args):
|
def log(task, fmt, *args):
|
||||||
'''Log a standard message
|
'''Log a standard message
|
||||||
'''
|
'''
|
||||||
logging.getLogger('python-appimage').info('%-8s ' + fmt, task, *args)
|
logging.info('%-8s ' + fmt, task, *args)
|
||||||
|
|
||||||
|
|
||||||
def debug(task, fmt, *args):
|
def debug(task, fmt, *args):
|
||||||
'''Report some debug information
|
'''Report some debug information
|
||||||
'''
|
'''
|
||||||
logging.getLogger('python-appimage').debug('%-8s ' + fmt, task, *args)
|
logging.debug('%-8s ' + fmt, task, *args)
|
||||||
|
|
||||||
|
|
||||||
def set_level(level):
|
|
||||||
'''Set the threshold for logs
|
|
||||||
'''
|
|
||||||
level = getattr(logging, level)
|
|
||||||
logging.getLogger('python-appimage').setLevel(level)
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
def format_appimage_name(abi, version, tag):
|
|
||||||
'''Format the Python AppImage name using the ABI, python version and OS tags
|
|
||||||
'''
|
|
||||||
return 'python{:}-{:}-{:}.AppImage'.format(
|
|
||||||
version, abi, format_tag(tag))
|
|
||||||
|
|
||||||
|
|
||||||
def format_tag(tag):
|
|
||||||
'''Format Manylinux tag
|
|
||||||
'''
|
|
||||||
if tag.startswith('2_'):
|
|
||||||
return 'manylinux_' + tag
|
|
||||||
else:
|
|
||||||
return 'manylinux' + tag
|
|
||||||
@@ -2,8 +2,8 @@ import os
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from .compat import decode, encode
|
from .compat import decode
|
||||||
from .log import debug, log
|
from .log import debug
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['ldd', 'system']
|
__all__ = ['ldd', 'system']
|
||||||
@@ -15,7 +15,7 @@ except NameError:
|
|||||||
basestring = (str, bytes)
|
basestring = (str, bytes)
|
||||||
|
|
||||||
|
|
||||||
def system(args, exclude=None, stdin=None):
|
def system(args, exclude=None):
|
||||||
'''System call with capturing output
|
'''System call with capturing output
|
||||||
'''
|
'''
|
||||||
cmd = ' '.join(args)
|
cmd = ' '.join(args)
|
||||||
@@ -29,36 +29,17 @@ def system(args, exclude=None, stdin=None):
|
|||||||
exclude = list(exclude)
|
exclude = list(exclude)
|
||||||
exclude.append('fuse: warning:')
|
exclude.append('fuse: warning:')
|
||||||
|
|
||||||
if stdin:
|
|
||||||
in_arg = subprocess.PIPE
|
|
||||||
stdin = encode(stdin)
|
|
||||||
else:
|
|
||||||
in_arg = None
|
|
||||||
|
|
||||||
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE, stdin=in_arg)
|
stderr=subprocess.PIPE)
|
||||||
out, err = p.communicate(input=stdin)
|
out, err = p.communicate()
|
||||||
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 matches_pattern(line, pattern)]
|
if not line.startswith(pattern)]
|
||||||
|
|
||||||
if stripped:
|
if stripped:
|
||||||
# Tolerate single line warning(s)
|
|
||||||
for line in stripped:
|
|
||||||
if (len(line) < 8) or (line[:8].lower() != "warning:"):
|
|
||||||
raise RuntimeError(err)
|
raise RuntimeError(err)
|
||||||
else:
|
|
||||||
for line in stripped:
|
|
||||||
log('WARNING', line[8:].strip())
|
|
||||||
|
|
||||||
return str(decode(out).strip())
|
return str(decode(out).strip())
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,5 @@ 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)
|
||||||
|
|||||||
@@ -28,14 +28,10 @@ def urlretrieve(url, filename=None):
|
|||||||
'''
|
'''
|
||||||
if filename is None:
|
if filename is None:
|
||||||
filename = os.path.basename(url)
|
filename = os.path.basename(url)
|
||||||
debug('DOWNLOAD', '%s from %s', filename, os.path.dirname(url))
|
debug('DOWNLOAD', '%s from %s', name, os.path.dirname(url))
|
||||||
else:
|
else:
|
||||||
debug('DOWNLOAD', '%s as %s', url, filename)
|
debug('DOWNLOAD', '%s as %s', url, filename)
|
||||||
|
|
||||||
parent_directory = os.path.dirname(filename)
|
|
||||||
if parent_directory and not os.path.exists(parent_directory):
|
|
||||||
os.makedirs(parent_directory)
|
|
||||||
|
|
||||||
if _urlretrieve is None:
|
if _urlretrieve is None:
|
||||||
data = urllib2.urlopen(url).read()
|
data = urllib2.urlopen(url).read()
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
version = '1.4.5'
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
#! /usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
from enum import auto, Enum
|
|
||||||
import inspect
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
from types import FunctionType
|
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
|
|
||||||
from python_appimage.manylinux import PythonVersion
|
|
||||||
|
|
||||||
|
|
||||||
ARGS = None
|
|
||||||
|
|
||||||
|
|
||||||
def assert_eq(expected, found):
|
|
||||||
if expected != found:
|
|
||||||
raise AssertionError('expected "{}", found "{}"'.format(
|
|
||||||
expected, found))
|
|
||||||
|
|
||||||
|
|
||||||
class Script(NamedTuple):
|
|
||||||
'''Python script wrapper'''
|
|
||||||
|
|
||||||
content: str
|
|
||||||
|
|
||||||
def run(self, appimage: Path):
|
|
||||||
'''Run the script through an appimage'''
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
script = f'{tmpdir}/script.py'
|
|
||||||
with open(script, 'w') as f:
|
|
||||||
f.write(inspect.getsource(assert_eq))
|
|
||||||
f.write(os.linesep)
|
|
||||||
f.write(self.content)
|
|
||||||
return system(f'{appimage} {script}')
|
|
||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
|
||||||
'''Test exit status'''
|
|
||||||
FAILED = auto()
|
|
||||||
SKIPPED = auto()
|
|
||||||
SUCCESS = auto()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
def system(cmd):
|
|
||||||
'''Run a system command'''
|
|
||||||
|
|
||||||
r = subprocess.run(cmd, capture_output=True, shell=True)
|
|
||||||
|
|
||||||
if r.returncode != 0:
|
|
||||||
raise ValueError(r.stderr.decode())
|
|
||||||
else:
|
|
||||||
return r.stdout.decode()
|
|
||||||
|
|
||||||
|
|
||||||
class TestContext:
|
|
||||||
'''Context for testing an image'''
|
|
||||||
|
|
||||||
def __init__(self, appimage):
|
|
||||||
self.appimage = appimage
|
|
||||||
|
|
||||||
# Guess python version from appimage name.
|
|
||||||
version, _, abi, *_ = appimage.name.split('-', 3)
|
|
||||||
version = version[6:]
|
|
||||||
if abi.endswith('t'):
|
|
||||||
version += '-nogil'
|
|
||||||
self.version = PythonVersion.from_str(version)
|
|
||||||
|
|
||||||
# Get some specific AppImage env variables.
|
|
||||||
self.env = eval(Script('''
|
|
||||||
import os
|
|
||||||
appdir = os.environ['APPDIR']
|
|
||||||
env = {}
|
|
||||||
for var in ('SSL_CERT_FILE', 'TCL_LIBRARY', 'TK_LIBRARY', 'TKPATH'):
|
|
||||||
try:
|
|
||||||
env[var] = os.environ[var].replace(appdir, '$APPDIR')
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
print(env)
|
|
||||||
''').run(appimage))
|
|
||||||
|
|
||||||
# Extract the AppImage.
|
|
||||||
tmpdir = tempfile.TemporaryDirectory()
|
|
||||||
dst = Path(tmpdir.name) / appimage.name
|
|
||||||
shutil.copy(appimage, dst)
|
|
||||||
system(f'cd {tmpdir.name} && ./{appimage.name} --appimage-extract')
|
|
||||||
self.appdir = Path(tmpdir.name) / 'squashfs-root'
|
|
||||||
self.tmpdir = tmpdir
|
|
||||||
|
|
||||||
def list_content(self, path=None):
|
|
||||||
'''List the content of an extracted directory'''
|
|
||||||
|
|
||||||
path = self.appdir if path is None else self.appdir / path
|
|
||||||
return sorted(os.listdir(path))
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
'''Run all tests'''
|
|
||||||
|
|
||||||
tests = []
|
|
||||||
for key, value in self.__class__.__dict__.items():
|
|
||||||
if isinstance(value, FunctionType):
|
|
||||||
if key.startswith('test_'):
|
|
||||||
tests.append(value)
|
|
||||||
|
|
||||||
n = len(tests)
|
|
||||||
m = max(len(test.__doc__) for test in tests)
|
|
||||||
for i, test in enumerate(tests):
|
|
||||||
sys.stdout.write(
|
|
||||||
f'[ {self.appimage.name} | {i + 1:2}/{n} ] {test.__doc__:{m}}'
|
|
||||||
)
|
|
||||||
sys.stdout.flush()
|
|
||||||
try:
|
|
||||||
status = test(self)
|
|
||||||
except Exception as e:
|
|
||||||
status = Status.FAILED
|
|
||||||
sys.stdout.write(
|
|
||||||
f' -> {status} ({test.__name__}){os.linesep}')
|
|
||||||
sys.stdout.flush()
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
sys.stdout.write(f' -> {status}{os.linesep}')
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def test_root_content(self):
|
|
||||||
'''Check the appimage root content'''
|
|
||||||
|
|
||||||
content = self.list_content()
|
|
||||||
expected = ['.DirIcon', 'AppRun', 'opt', 'python.png',
|
|
||||||
f'python{self.version.long()}.desktop', 'usr']
|
|
||||||
assert_eq(expected, content)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_python_content(self):
|
|
||||||
'''Check the appimage python content'''
|
|
||||||
|
|
||||||
prefix = f'opt/python{self.version.flavoured()}'
|
|
||||||
content = self.list_content(prefix)
|
|
||||||
assert_eq(['bin', 'include', 'lib'], content)
|
|
||||||
content = self.list_content(f'{prefix}/bin')
|
|
||||||
assert_eq(
|
|
||||||
[f'pip{self.version.short()}', f'python{self.version.flavoured()}'],
|
|
||||||
content
|
|
||||||
)
|
|
||||||
content = self.list_content(f'{prefix}/include')
|
|
||||||
if (self.version.major == 3) and (self.version.minor <= 7):
|
|
||||||
expected = [f'python{self.version.short()}m']
|
|
||||||
else:
|
|
||||||
expected = [f'python{self.version.flavoured()}']
|
|
||||||
assert_eq(expected, content)
|
|
||||||
content = self.list_content(f'{prefix}/lib')
|
|
||||||
assert_eq([f'python{self.version.flavoured()}'], content)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_system_content(self):
|
|
||||||
'''Check the appimage system content'''
|
|
||||||
|
|
||||||
content = self.list_content('usr')
|
|
||||||
assert_eq(['bin', 'lib', 'share'], content)
|
|
||||||
content = self.list_content('usr/bin')
|
|
||||||
expected = [
|
|
||||||
'pip', f'pip{self.version.major}', f'pip{self.version.short()}',
|
|
||||||
'python', f'python{self.version.major}',
|
|
||||||
f'python{self.version.short()}'
|
|
||||||
]
|
|
||||||
assert_eq(expected, content)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_tcltk_bundling(self):
|
|
||||||
'''Check Tcl/Tk bundling'''
|
|
||||||
|
|
||||||
if 'TK_LIBRARY' not in self.env:
|
|
||||||
return Status.SKIPPED
|
|
||||||
else:
|
|
||||||
for var in ('TCL_LIBRARY', 'TK_LIBRARY', 'TKPATH'):
|
|
||||||
path = Path(self.env[var].replace('$APPDIR', str(self.appdir)))
|
|
||||||
assert path.exists()
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_ssl_bundling(self):
|
|
||||||
'''Check SSL certs bundling'''
|
|
||||||
|
|
||||||
var = 'SSL_CERT_FILE'
|
|
||||||
path = Path(self.env[var].replace('$APPDIR', str(self.appdir)))
|
|
||||||
assert path.exists()
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_bin_symlinks(self):
|
|
||||||
'''Check /usr/bin symlinks'''
|
|
||||||
|
|
||||||
assert_eq(
|
|
||||||
(self.appdir /
|
|
||||||
f'opt/python{self.version.flavoured()}/bin/pip{self.version.short()}'),
|
|
||||||
(self.appdir / f'usr/bin/pip{self.version.short()}').resolve()
|
|
||||||
)
|
|
||||||
assert_eq(
|
|
||||||
f'pip{self.version.short()}',
|
|
||||||
str((self.appdir / f'usr/bin/pip{self.version.major}').readlink())
|
|
||||||
)
|
|
||||||
assert_eq(
|
|
||||||
f'pip{self.version.major}',
|
|
||||||
str((self.appdir / 'usr/bin/pip').readlink())
|
|
||||||
)
|
|
||||||
assert_eq(
|
|
||||||
f'python{self.version.short()}',
|
|
||||||
str((self.appdir / f'usr/bin/python{self.version.major}').readlink())
|
|
||||||
)
|
|
||||||
assert_eq(
|
|
||||||
f'python{self.version.major}',
|
|
||||||
str((self.appdir / 'usr/bin/python').readlink())
|
|
||||||
)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_appimage_hook(self):
|
|
||||||
'''Test the appimage hook'''
|
|
||||||
|
|
||||||
Script(f'''
|
|
||||||
import os
|
|
||||||
assert_eq(os.environ['APPIMAGE_COMMAND'], '{self.appimage}')
|
|
||||||
|
|
||||||
import sys
|
|
||||||
assert_eq('{self.appimage}', sys.executable)
|
|
||||||
assert_eq('{self.appimage}', sys._base_executable)
|
|
||||||
''').run(self.appimage)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_python_prefix(self):
|
|
||||||
'''Test the python prefix'''
|
|
||||||
|
|
||||||
Script(f'''
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
expected = os.environ["APPDIR"] + '/opt/python{self.version.flavoured()}'
|
|
||||||
assert_eq(expected, sys.prefix)
|
|
||||||
''').run(self.appimage)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_ssl_request(self):
|
|
||||||
'''Test SSL request (see issue #24)'''
|
|
||||||
|
|
||||||
if self.version.major == 2:
|
|
||||||
return Status.SKIPPED
|
|
||||||
else:
|
|
||||||
Script('''
|
|
||||||
from http import HTTPStatus
|
|
||||||
import urllib.request
|
|
||||||
with urllib.request.urlopen('https://wikipedia.org') as r:
|
|
||||||
assert_eq(r.status, HTTPStatus.OK)
|
|
||||||
''').run(self.appimage)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_pip_install(self):
|
|
||||||
'''Test pip installing to an extracted AppImage'''
|
|
||||||
|
|
||||||
r = system(f'{self.appdir}/AppRun -m pip install pip-install-test')
|
|
||||||
assert('Successfully installed pip-install-test' in r)
|
|
||||||
path = self.appdir / f'opt/python{self.version.flavoured()}/lib/python{self.version.flavoured()}/site-packages/pip_install_test'
|
|
||||||
assert(path.exists())
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_tkinter_usage(self):
|
|
||||||
'''Test basic tkinter usage'''
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.environ['DISPLAY']
|
|
||||||
self.env['TK_LIBRARY']
|
|
||||||
except KeyError:
|
|
||||||
return Status.SKIPPED
|
|
||||||
else:
|
|
||||||
tkinter = 'tkinter' if self.version.major > 2 else 'Tkinter'
|
|
||||||
Script(f'''
|
|
||||||
import {tkinter} as tkinter
|
|
||||||
tkinter.Tk()
|
|
||||||
''').run(self.appimage)
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
def test_venv_usage(self):
|
|
||||||
'''Test venv creation'''
|
|
||||||
|
|
||||||
if self.version.major == 2:
|
|
||||||
return Status.SKIPPED
|
|
||||||
else:
|
|
||||||
system(' && '.join((
|
|
||||||
f'cd {self.tmpdir.name}',
|
|
||||||
f'./{self.appimage.name} -m venv ENV',
|
|
||||||
'. ENV/bin/activate',
|
|
||||||
)))
|
|
||||||
python = Path(f'{self.tmpdir.name}/ENV/bin/python')
|
|
||||||
assert_eq(self.appimage.name, str(python.readlink()))
|
|
||||||
return Status.SUCCESS
|
|
||||||
|
|
||||||
|
|
||||||
def test():
|
|
||||||
'''Test Python AppImage(s)'''
|
|
||||||
|
|
||||||
for appimage in ARGS.appimage:
|
|
||||||
context = TestContext(appimage)
|
|
||||||
context.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
parser = argparse.ArgumentParser(description = test.__doc__)
|
|
||||||
parser.add_argument('appimage',
|
|
||||||
help = 'path to appimage(s)',
|
|
||||||
nargs = '+',
|
|
||||||
type = lambda x: Path(x).absolute()
|
|
||||||
)
|
|
||||||
|
|
||||||
ARGS = parser.parse_args()
|
|
||||||
test()
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
#! /usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from github import Auth, Github
|
|
||||||
|
|
||||||
from python_appimage.commands.build.manylinux import execute as build_manylinux
|
|
||||||
from python_appimage.commands.list import execute as list_pythons
|
|
||||||
from python_appimage.utils.log import log
|
|
||||||
from python_appimage.utils.manylinux import format_appimage_name, format_tag
|
|
||||||
|
|
||||||
|
|
||||||
# Build matrix
|
|
||||||
ARCHS = ('x86_64', 'i686', 'aarch64')
|
|
||||||
MANYLINUSES = ('1', '2010', '2014', '2_24', '2_28')
|
|
||||||
EXCLUDES = ('2_28_i686', '1_aarch64', '2010_aarch64')
|
|
||||||
|
|
||||||
# Build directory for AppImages
|
|
||||||
APPIMAGES_DIR = 'build-appimages'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReleaseMeta:
|
|
||||||
'''Metadata relative to a GitHub release
|
|
||||||
'''
|
|
||||||
|
|
||||||
tag: str
|
|
||||||
|
|
||||||
ref: Optional["github.GitRef"] = None
|
|
||||||
release: Optional["github.GitRelease"] = None
|
|
||||||
|
|
||||||
def message(self):
|
|
||||||
'''Returns release message'''
|
|
||||||
return f'Appimage distributions of {self.title()} (see `Assets` below)'
|
|
||||||
|
|
||||||
def title(self):
|
|
||||||
'''Returns release title'''
|
|
||||||
version = self.tag[6:]
|
|
||||||
return f'Python {version}'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AssetMeta:
|
|
||||||
'''Metadata relative to a release Asset
|
|
||||||
'''
|
|
||||||
|
|
||||||
tag: str
|
|
||||||
abi: str
|
|
||||||
version: str
|
|
||||||
|
|
||||||
asset: Optional["github.GitReleaseAsset"] = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_appimage(cls, name):
|
|
||||||
'''Returns an instance from a Python AppImage name
|
|
||||||
'''
|
|
||||||
tmp = name[6:-9]
|
|
||||||
tmp, tag = tmp.split('-manylinux', 1)
|
|
||||||
if tag.startswith('_'):
|
|
||||||
tag = tag[1:]
|
|
||||||
version, abi = tmp.split('-', 1)
|
|
||||||
return cls(
|
|
||||||
tag = tag,
|
|
||||||
abi = abi,
|
|
||||||
version = version
|
|
||||||
)
|
|
||||||
|
|
||||||
def appimage_name(self):
|
|
||||||
'''Returns Python AppImage name'''
|
|
||||||
return format_appimage_name(self.abi, self.version, self.tag)
|
|
||||||
|
|
||||||
def formated_tag(self):
|
|
||||||
'''Returns formated manylinux tag'''
|
|
||||||
return format_tag(self.tag)
|
|
||||||
|
|
||||||
def previous_version(self):
|
|
||||||
'''Returns previous version'''
|
|
||||||
if self.asset:
|
|
||||||
return self.asset.name[6:-9].split('-', 1)[0]
|
|
||||||
|
|
||||||
def release_tag(self):
|
|
||||||
'''Returns release git tag'''
|
|
||||||
version = self.version.rsplit('.', 1)[0]
|
|
||||||
return f'python{version}'
|
|
||||||
|
|
||||||
|
|
||||||
def update(args):
|
|
||||||
'''Update Python AppImage GitHub releases
|
|
||||||
'''
|
|
||||||
|
|
||||||
sha = args.sha
|
|
||||||
if sha is None:
|
|
||||||
sha = os.getenv('GITHUB_SHA')
|
|
||||||
if sha is None:
|
|
||||||
p = subprocess.run(
|
|
||||||
'git rev-parse HEAD',
|
|
||||||
shell = True,
|
|
||||||
capture_output = True,
|
|
||||||
check = True
|
|
||||||
)
|
|
||||||
sha = p.stdout.decode().strip()
|
|
||||||
|
|
||||||
# Connect to GitHub
|
|
||||||
token = args.token
|
|
||||||
if token is None:
|
|
||||||
# First, check for token in env
|
|
||||||
token = os.getenv('GITHUB_TOKEN')
|
|
||||||
if token is None:
|
|
||||||
# Else try to get a token from gh app
|
|
||||||
p = subprocess.run(
|
|
||||||
'gh auth token',
|
|
||||||
shell = True,
|
|
||||||
capture_output = True,
|
|
||||||
check = True
|
|
||||||
)
|
|
||||||
token = p.stdout.decode().strip()
|
|
||||||
|
|
||||||
auth = Auth.Token(token)
|
|
||||||
session = Github(auth=auth)
|
|
||||||
repo = session.get_repo('niess/python-appimage')
|
|
||||||
|
|
||||||
# Fetch currently released AppImages
|
|
||||||
log('FETCH', 'currently released AppImages')
|
|
||||||
releases = {}
|
|
||||||
assets = defaultdict(dict)
|
|
||||||
n_assets = 0
|
|
||||||
for release in repo.get_releases():
|
|
||||||
if release.tag_name.startswith('python'):
|
|
||||||
meta = ReleaseMeta(
|
|
||||||
tag = release.tag_name,
|
|
||||||
release = release
|
|
||||||
)
|
|
||||||
ref = repo.get_git_ref(f'tags/{meta.tag}')
|
|
||||||
if (ref.ref is not None) and (ref.object.sha != sha):
|
|
||||||
meta.ref = ref
|
|
||||||
releases[release.tag_name] = meta
|
|
||||||
|
|
||||||
for asset in release.get_assets():
|
|
||||||
if asset.name.endswith('.AppImage'):
|
|
||||||
n_assets += 1
|
|
||||||
meta = AssetMeta.from_appimage(asset.name)
|
|
||||||
assert(meta.release_tag() == release.tag_name)
|
|
||||||
meta.asset = asset
|
|
||||||
assets[meta.tag][meta.abi] = meta
|
|
||||||
|
|
||||||
n_releases = len(releases)
|
|
||||||
log('FETCH', f'found {n_assets} AppImages in {n_releases} releases')
|
|
||||||
|
|
||||||
# Look for updates.
|
|
||||||
new_releases = set()
|
|
||||||
new_assets = []
|
|
||||||
|
|
||||||
for manylinux in MANYLINUSES:
|
|
||||||
for arch in ARCHS:
|
|
||||||
tag = f'{manylinux}_{arch}'
|
|
||||||
if tag in EXCLUDES:
|
|
||||||
continue
|
|
||||||
|
|
||||||
pythons = list_pythons(tag)
|
|
||||||
for (abi, version) in pythons:
|
|
||||||
try:
|
|
||||||
meta = assets[tag][abi]
|
|
||||||
except KeyError:
|
|
||||||
meta = None
|
|
||||||
|
|
||||||
if (meta is None) or (meta.version != version) or args.all:
|
|
||||||
new_meta = AssetMeta(
|
|
||||||
tag = tag,
|
|
||||||
abi = abi,
|
|
||||||
version = version
|
|
||||||
)
|
|
||||||
if meta is not None:
|
|
||||||
new_meta.asset = meta.asset
|
|
||||||
new_assets.append(new_meta)
|
|
||||||
|
|
||||||
rtag = new_meta.release_tag()
|
|
||||||
if rtag not in releases:
|
|
||||||
new_releases.add(rtag)
|
|
||||||
|
|
||||||
if args.dry:
|
|
||||||
# Log foreseen changes and exit
|
|
||||||
for tag in new_releases:
|
|
||||||
meta = ReleaseMeta(tag)
|
|
||||||
log('DRY', f'new release for {meta.title()}')
|
|
||||||
|
|
||||||
for meta in new_assets:
|
|
||||||
log('DRY', f'create asset {meta.appimage_name()}')
|
|
||||||
if meta.asset is not None:
|
|
||||||
log('DRY', f'remove asset {meta.asset.name}')
|
|
||||||
|
|
||||||
for meta in releases.values():
|
|
||||||
if meta.ref is not None:
|
|
||||||
log('DRY', f'refs/tags/{meta.tag} -> {sha}')
|
|
||||||
if meta.release is not None:
|
|
||||||
log('DRY', f'reformat release for {meta.title()}')
|
|
||||||
|
|
||||||
if new_assets:
|
|
||||||
log('DRY', f'new update summary with {len(new_assets)} entries')
|
|
||||||
|
|
||||||
if not args.build:
|
|
||||||
return
|
|
||||||
|
|
||||||
if new_assets:
|
|
||||||
# Build new AppImage(s)
|
|
||||||
cwd = os.getcwd()
|
|
||||||
os.makedirs(APPIMAGES_DIR, exist_ok=True)
|
|
||||||
try:
|
|
||||||
os.chdir(APPIMAGES_DIR)
|
|
||||||
for meta in new_assets:
|
|
||||||
build_manylinux(meta.tag, meta.abi)
|
|
||||||
finally:
|
|
||||||
os.chdir(cwd)
|
|
||||||
|
|
||||||
if args.dry:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create any new release(s).
|
|
||||||
for tag in new_releases:
|
|
||||||
meta = ReleaseMeta(tag)
|
|
||||||
title = meta.title()
|
|
||||||
meta.release = repo.create_git_release(
|
|
||||||
tag = meta.tag,
|
|
||||||
name = title,
|
|
||||||
message = meta.message(),
|
|
||||||
prerelease = True
|
|
||||||
)
|
|
||||||
releases[tag] = meta
|
|
||||||
log('UPDATE', f'new release for {title}')
|
|
||||||
|
|
||||||
# Update assets.
|
|
||||||
update_summary = []
|
|
||||||
for meta in new_assets:
|
|
||||||
release = releases[meta.release_tag()].release
|
|
||||||
appimage = meta.appimage_name()
|
|
||||||
if meta.asset and (meta.asset.name == appimage):
|
|
||||||
meta.asset.delete_asset()
|
|
||||||
update_summary.append(
|
|
||||||
f'- update {meta.formated_tag()}/{meta.abi} {meta.version}'
|
|
||||||
)
|
|
||||||
new_asset = release.upload_asset(
|
|
||||||
path = f'{APPIMAGES_DIR}/{appimage}',
|
|
||||||
name = appimage
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
new_asset = release.upload_asset(
|
|
||||||
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
|
|
||||||
assets[meta.tag][meta.abi] = meta
|
|
||||||
|
|
||||||
# Update git tags SHA
|
|
||||||
for meta in releases.values():
|
|
||||||
if meta.ref is not None:
|
|
||||||
meta.ref.edit(
|
|
||||||
sha = sha,
|
|
||||||
force = True
|
|
||||||
)
|
|
||||||
log('UPDATE', f'refs/tags/{meta.tag} -> {sha}')
|
|
||||||
|
|
||||||
if meta.release is not None:
|
|
||||||
title = meta.title()
|
|
||||||
meta.release.update_release(
|
|
||||||
name = title,
|
|
||||||
message = meta.message(),
|
|
||||||
prerelease = True,
|
|
||||||
tag_name = meta.tag
|
|
||||||
)
|
|
||||||
log('UPDATE', f'reformat release for {title}')
|
|
||||||
|
|
||||||
# Generate update summary
|
|
||||||
if update_summary:
|
|
||||||
for release in repo.get_releases():
|
|
||||||
if release.tag_name == 'update-summary':
|
|
||||||
release.delete_release()
|
|
||||||
break
|
|
||||||
|
|
||||||
message = os.linesep.join(update_summary)
|
|
||||||
repo.create_git_release(
|
|
||||||
tag = 'update-summary',
|
|
||||||
name = 'Update summary',
|
|
||||||
message = message,
|
|
||||||
prerelease = True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description = 'Update GitHub releases of Python AppImages'
|
|
||||||
)
|
|
||||||
parser.add_argument('-a', '--all',
|
|
||||||
help = 'force update of all available releases',
|
|
||||||
action = 'store_true',
|
|
||||||
default = False
|
|
||||||
)
|
|
||||||
parser.add_argument('-b', '--build',
|
|
||||||
help = 'build AppImages (in dry mode)',
|
|
||||||
action = 'store_true',
|
|
||||||
default = False
|
|
||||||
)
|
|
||||||
parser.add_argument('-d', '--dry',
|
|
||||||
help = 'dry run (only log changes)',
|
|
||||||
action = 'store_true',
|
|
||||||
default = False
|
|
||||||
)
|
|
||||||
parser.add_argument('-m', '--manylinux',
|
|
||||||
help = 'target specific manylinux tags',
|
|
||||||
nargs = "+"
|
|
||||||
)
|
|
||||||
parser.add_argument("-s", "--sha",
|
|
||||||
help = "reference commit SHA"
|
|
||||||
)
|
|
||||||
parser.add_argument('-t', '--token',
|
|
||||||
help = 'GitHub authentication token'
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.manylinux:
|
|
||||||
MANYLINUSES = args.manylinux
|
|
||||||
|
|
||||||
sys.argv = sys.argv[:1] # Empty args for fake call
|
|
||||||
update(args)
|
|
||||||
88
setup.py
Normal file
88
setup.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import setuptools
|
||||||
|
import ssl
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from python_appimage.utils.deps import ensure_excludelist
|
||||||
|
from python_appimage.utils.url import urlopen
|
||||||
|
|
||||||
|
|
||||||
|
CLASSIFIERS = '''\
|
||||||
|
Development Status :: 4 - Beta
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||||
|
Programming Language :: Python
|
||||||
|
Topic :: Software Development
|
||||||
|
Operating System :: POSIX :: Linux
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
with open('README.md') as f:
|
||||||
|
long_description = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
'''Get the next version number from PyPI
|
||||||
|
'''
|
||||||
|
with open('VERSION') as f:
|
||||||
|
version = f.read().strip()
|
||||||
|
|
||||||
|
p = subprocess.Popen(
|
||||||
|
'git describe --match=NeVeRmAtCh --always --dirty 2> /dev/null || '
|
||||||
|
'echo unknown',
|
||||||
|
shell=True, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
stdout, _ = p.communicate()
|
||||||
|
try:
|
||||||
|
stdout = stdout.decode()
|
||||||
|
except AttributeError:
|
||||||
|
stdout = str(stdout)
|
||||||
|
git_revision = stdout.strip()
|
||||||
|
|
||||||
|
with open('python_appimage/version.py', 'w+') as f:
|
||||||
|
f.write('''\
|
||||||
|
# This file was generated by setup.py
|
||||||
|
version = '{version:}'
|
||||||
|
git_revision = '{git_revision:}'
|
||||||
|
'''.format(version=version, git_revision=git_revision))
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_data():
|
||||||
|
'''Get the list of package data
|
||||||
|
'''
|
||||||
|
ensure_excludelist()
|
||||||
|
|
||||||
|
prefix = os.path.dirname(__file__) or '.'
|
||||||
|
return ['data/' + file_
|
||||||
|
for file_ in os.listdir(prefix + '/python_appimage/data')]
|
||||||
|
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name = 'python_appimage',
|
||||||
|
version = get_version(),
|
||||||
|
author = 'Valentin Niess',
|
||||||
|
author_email = 'valentin.niess@gmail.com',
|
||||||
|
description = 'Appimage releases of Python',
|
||||||
|
long_description = long_description,
|
||||||
|
long_description_content_type = 'text/markdown',
|
||||||
|
url = 'https://github.com/niess/python-appimage',
|
||||||
|
download_url = 'https://pypi.python.org/pypi/python-appimage',
|
||||||
|
project_urls = {
|
||||||
|
'Bug Tracker' : 'https://github.com/niess/python-appimage/issues',
|
||||||
|
'Source Code' : 'https://github.com/niess/python-appimage',
|
||||||
|
},
|
||||||
|
packages = setuptools.find_packages(),
|
||||||
|
classifiers = [s for s in CLASSIFIERS.split(os.linesep) if s.strip()],
|
||||||
|
license = 'GPLv3',
|
||||||
|
platforms = ['Linux'],
|
||||||
|
python_requires = '>=2.7',
|
||||||
|
include_package_data = True,
|
||||||
|
package_data = {'': get_package_data()},
|
||||||
|
entry_points = {
|
||||||
|
'console_scripts' : (
|
||||||
|
'python-appimage = python_appimage.__main__:main',)
|
||||||
|
}
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user