mirror of
https://github.com/niess/python-appimage.git
synced 2026-03-15 12:50:16 +01:00
Compare commits
59 Commits
paper
...
manylinux1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08ee36fc45 | ||
|
|
41e9b109d0 | ||
|
|
755dd91f45 | ||
|
|
1a777a00c4 | ||
|
|
fb54370a7e | ||
|
|
b8c443b27c | ||
|
|
72a52b6f34 | ||
|
|
583a61686a | ||
|
|
736f8dcd7c | ||
|
|
da067b4831 | ||
|
|
efc41b6079 | ||
|
|
223f35f757 | ||
|
|
bb2e178c2a | ||
|
|
ea671fe7ed | ||
|
|
c8fde2906a | ||
|
|
46b2efb359 | ||
|
|
0c66562ad4 | ||
|
|
03bab9b38d | ||
|
|
a6d0da5f0b | ||
|
|
77ae6c7d55 | ||
|
|
9de84d8b22 | ||
|
|
899f40102a | ||
|
|
48b28af040 | ||
|
|
955149ad6a | ||
|
|
af59728145 | ||
|
|
f5f7349f46 | ||
|
|
a2a075f9db | ||
|
|
4bc98f48d4 | ||
|
|
fd7e28817c | ||
|
|
602f65c0e8 | ||
|
|
4fcdf2cba1 | ||
|
|
e249fdebdb | ||
|
|
e596fec38b | ||
|
|
818fe273c1 | ||
|
|
db5d91e0dd | ||
|
|
c28641bd84 | ||
|
|
d5875464d0 | ||
|
|
6fbb227e3a | ||
|
|
b5ad9a6dcf | ||
|
|
4ec94ba00e | ||
|
|
6bfb15b186 | ||
|
|
2161858718 | ||
|
|
6dfa764573 | ||
|
|
df67460a7c | ||
|
|
9706c81569 | ||
|
|
528f797ddf | ||
|
|
d7fe43facf | ||
|
|
5d085e38ee | ||
|
|
678aae1393 | ||
|
|
96a8cbbfab | ||
|
|
e2efafa081 | ||
|
|
061fd7414d | ||
|
|
7b9b4f2b75 | ||
|
|
a99d31e661 | ||
|
|
984a1ccec0 | ||
|
|
bbd549c3a1 | ||
|
|
5513645e55 | ||
|
|
dc54fa8231 | ||
|
|
d259ad4f49 |
103
.github/workflows/appimage.yml
vendored
103
.github/workflows/appimage.yml
vendored
@@ -1,89 +1,36 @@
|
|||||||
name: AppImage
|
name: AppImage
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
inputs:
|
||||||
- master
|
dry:
|
||||||
paths:
|
description: 'Dry run'
|
||||||
- '.github/workflows/appimage.yml'
|
required: true
|
||||||
- 'python_appimage/**'
|
type: boolean
|
||||||
|
all:
|
||||||
|
description: 'Update all'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * 0'
|
- cron: '0 3 * * 0'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build:
|
Update:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
permissions:
|
||||||
matrix:
|
contents: write
|
||||||
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@v2
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build
|
- name: Install Dependencies
|
||||||
env:
|
run: pip install PyGithub
|
||||||
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
|
|
||||||
|
- name: Run updater
|
||||||
run: |
|
run: |
|
||||||
# Build the AppImage
|
./scripts/update-appimages.py \
|
||||||
python -m python_appimage build manylinux \
|
--token=${{ secrets.GITHUB_TOKEN }} \
|
||||||
${{ matrix.image }}_${{ matrix.arch }} \
|
--sha=${{ github.sha }} \
|
||||||
${{ matrix.tag }}
|
${{ inputs.all && '--all' || '' }} \
|
||||||
|
${{ inputs.dry && '--dry' || '' }}
|
||||||
# Export the AppImage name and the Python version
|
env:
|
||||||
appimage=$(ls python*.AppImage)
|
PYTHONPATH: ${{ github.workspace }}
|
||||||
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 }}
|
|
||||||
|
|||||||
39
.github/workflows/applications.yml
vendored
39
.github/workflows/applications.yml
vendored
@@ -1,27 +1,39 @@
|
|||||||
name: Applications
|
name: Applications
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
inputs:
|
||||||
- master
|
scipy:
|
||||||
paths:
|
required: true
|
||||||
- '.github/workflows/applications.yml'
|
default: true
|
||||||
- 'applications/**'
|
type: boolean
|
||||||
- 'python_appimage/**'
|
tasmotizer:
|
||||||
|
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-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
version: ['2.7', '3.7', '3.9']
|
version: ['2.7', '3.7', '3.9']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v1
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.version }}
|
python-version: ${{ matrix.version }}
|
||||||
|
|
||||||
- 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 \
|
||||||
@@ -30,19 +42,22 @@ 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=manylinux2014_x86_64
|
--linux-tag=manylinux1_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: ${{ matrix.version == '3.9' }}
|
if: ${{ inputs.ssh_mitm && (matrix.version == '3.9') }}
|
||||||
run: |
|
run: |
|
||||||
python -m python_appimage build app applications/ssh-mitm
|
python -m python_appimage build app applications/ssh-mitm
|
||||||
test -e ssh-mitm-x86_64.AppImage
|
test -e ssh-mitm-x86_64.AppImage
|
||||||
|
|||||||
13
.github/workflows/delete-artifacts.yml
vendored
13
.github/workflows/delete-artifacts.yml
vendored
@@ -1,13 +0,0 @@
|
|||||||
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
|
|
||||||
20
.github/workflows/pypi.yml
vendored
20
.github/workflows/pypi.yml
vendored
@@ -5,17 +5,23 @@ on:
|
|||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'VERSION'
|
- 'VERSION'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
description: 'Upload to PyPI'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Test:
|
Test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
version: ['2.7', '3.9']
|
version: ['3.11']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v1
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.version }}
|
python-version: ${{ matrix.version }}
|
||||||
|
|
||||||
@@ -31,10 +37,10 @@ jobs:
|
|||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v1
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.9'
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Build wheel
|
- name: Build wheel
|
||||||
run: |
|
run: |
|
||||||
@@ -43,7 +49,7 @@ jobs:
|
|||||||
python setup.py bdist_wheel --universal
|
python setup.py bdist_wheel --universal
|
||||||
|
|
||||||
- name: Upload to PyPI
|
- name: Upload to PyPI
|
||||||
if: github.ref == 'refs/heads/master'
|
if: (github.ref == 'refs/heads/master') && inputs.upload
|
||||||
uses: pypa/gh-action-pypi-publish@master
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
with:
|
with:
|
||||||
password: ${{ secrets.PYPI_TOKEN }}
|
password: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -31,11 +31,22 @@ Python apps, given an existing Python AppImage and a recipe folder.
|
|||||||
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
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
[LINUXDEPLOY]: https://github.com/linuxdeploy/linuxdeploy/
|
[LINUXDEPLOY]: https://github.com/linuxdeploy/linuxdeploy/
|
||||||
[MANYLINUX]: https://github.com/pypa/manylinux/
|
[MANYLINUX]: https://github.com/pypa/manylinux/
|
||||||
[PATCHELF]: https://github.com/NixOS/patchelf/
|
[PATCHELF]: https://github.com/NixOS/patchelf/
|
||||||
[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/
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ files. The `requirements.txt` file allows to specify additional site packages
|
|||||||
to be bundled in the AppImage, using `pip`.
|
to be bundled in the AppImage, using `pip`.
|
||||||
|
|
||||||
!!! Caution
|
!!! Caution
|
||||||
Site packages bundled in the AppImage, as well as their dependencies, must
|
For the application to be portable, site packages bundled in the AppImage,
|
||||||
either be pure python packages, or they must be available as portable binary
|
as well as their dependencies, must must be available as binary wheels, or
|
||||||
wheels.
|
be pure Python packages.
|
||||||
|
|
||||||
If a **C extension** is bundled from **source**, then it will likely **not
|
If a **C extension** is bundled from **source**, then it will likely **not
|
||||||
be portable**, as further discussed in the [Advanced
|
be portable**, as further discussed in the [Advanced
|
||||||
@@ -134,6 +134,13 @@ to be bundled in the AppImage, using `pip`.
|
|||||||
be cross-checked by browsing the `Download files` section on the package's
|
be cross-checked by browsing the `Download files` section on the package's
|
||||||
PyPI page.
|
PyPI page.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
Since version 1.2, `python-appimage` allows to specify local requirements as
|
||||||
|
well, using the `local+` tag (see
|
||||||
|
[PR49](https://github.com/niess/python-appimage/pull/49)). Note however that
|
||||||
|
this performs a direct copy of the local package, which has several
|
||||||
|
limitations.
|
||||||
|
|
||||||
{{ begin(".capsule") }}
|
{{ begin(".capsule") }}
|
||||||
### Entry point script
|
### Entry point script
|
||||||
|
|
||||||
@@ -175,6 +182,36 @@ example, `$APPDIR` points to the AppImage mount point at runtime.
|
|||||||
`{{ python-executable }} -I` starts a fully isolated Python instance.
|
`{{ python-executable }} -I` starts a fully isolated Python instance.
|
||||||
{% endraw %}
|
{% endraw %}
|
||||||
|
|
||||||
|
### Bundling data files
|
||||||
|
|
||||||
|
`python-appimage` is also capable of bundling in auxilliary data files directly
|
||||||
|
into the resulting AppImage. `-x/--extra-data` switch exists for that task.
|
||||||
|
Consider following example.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo -n "foo" > foo
|
||||||
|
mkdir bar
|
||||||
|
echo -n "baz" > bar/baz
|
||||||
|
python-appimage [your regular parameters] -x foo bar/*
|
||||||
|
```
|
||||||
|
|
||||||
|
User data included in such a way becomes accessible to the Python code
|
||||||
|
contained within the AppImage in a form of regular files under the directory
|
||||||
|
pointed to by `APPDIR` environment variable. Example of Python 3 script
|
||||||
|
that reads these exemplary files is presented below.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os, pathlib
|
||||||
|
for fileName in ("foo", "baz"):
|
||||||
|
print((pathlib.Path(os.getenv("APPDIR")) / fileName).read_text())
|
||||||
|
```
|
||||||
|
|
||||||
|
Above code, when executed, would print following output.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
foo
|
||||||
|
baz
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced packaging
|
## Advanced packaging
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,23 @@ $.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(/_(.+)/);
|
const tmp1 = tag.split(/_(.+)/, 2);
|
||||||
const linux = tmp1[0]
|
var linux = undefined;
|
||||||
const arch = tmp1[1]
|
var arch = undefined;
|
||||||
const tmp2 = tmp0[0].split("-")
|
if (tmp1[0] == "") {
|
||||||
const python = tmp2[1] + "-" + tmp2[2]
|
const tmp3 = tmp1[1].split("_");
|
||||||
|
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,
|
||||||
@@ -25,8 +37,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,6 +174,17 @@ $.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}
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -8,6 +7,11 @@ import sys
|
|||||||
__all__ = ['main']
|
__all__ = ['main']
|
||||||
|
|
||||||
|
|
||||||
|
def exists(path):
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise argparse.ArgumentTypeError("could not find: {}".format(path))
|
||||||
|
return os.path.abspath(path)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
'''Entry point for the CLI
|
'''Entry point for the CLI
|
||||||
'''
|
'''
|
||||||
@@ -23,10 +27,12 @@ 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=logging.ERROR)
|
dest='verbosity', action='store_const', const='ERROR')
|
||||||
parser.add_argument('-v', '--verbose', help='print extra information',
|
parser.add_argument('-v', '--verbose', help='print extra information',
|
||||||
dest='verbosity', action='store_const', const=logging.DEBUG)
|
dest='verbosity', action='store_const', const='DEBUG')
|
||||||
|
|
||||||
install_parser = subparsers.add_parser('install',
|
install_parser = subparsers.add_parser('install',
|
||||||
description='Install binary dependencies')
|
description='Install binary dependencies')
|
||||||
@@ -74,6 +80,13 @@ 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')
|
||||||
@@ -84,7 +97,12 @@ def main():
|
|||||||
|
|
||||||
# Configure the verbosity
|
# Configure the verbosity
|
||||||
if args.verbosity:
|
if args.verbosity:
|
||||||
logging.getLogger().setLevel(args.verbosity)
|
from .utils import log
|
||||||
|
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:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ..utils.compat import decode
|
from ..utils.compat import decode
|
||||||
from ..utils.deps import APPIMAGETOOL, ensure_appimagetool
|
from ..utils.deps import ensure_appimagetool
|
||||||
from ..utils.docker import docker_run
|
from ..utils.docker import docker_run
|
||||||
from ..utils.fs import copy_tree
|
from ..utils.fs import copy_tree
|
||||||
from ..utils.log import debug, log
|
from ..utils.log import debug, log
|
||||||
@@ -22,10 +22,10 @@ def build_appimage(appdir=None, destination=None):
|
|||||||
appdir = 'AppDir'
|
appdir = 'AppDir'
|
||||||
|
|
||||||
log('BUILD', appdir)
|
log('BUILD', appdir)
|
||||||
ensure_appimagetool()
|
appimagetool = ensure_appimagetool()
|
||||||
|
|
||||||
arch = platform.machine()
|
arch = platform.machine()
|
||||||
cmd = ['ARCH=' + arch, APPIMAGETOOL, '--no-appstream', appdir]
|
cmd = ['ARCH=' + arch, appimagetool, '--no-appstream', appdir]
|
||||||
if destination is not None:
|
if destination is not None:
|
||||||
cmd.append(destination)
|
cmd.append(destination)
|
||||||
cmd = ' '.join(cmd)
|
cmd = ' '.join(cmd)
|
||||||
@@ -45,7 +45,8 @@ def build_appimage(appdir=None, destination=None):
|
|||||||
elif out:
|
elif out:
|
||||||
out = out.replace('%', '%%')[:-1]
|
out = out.replace('%', '%%')[:-1]
|
||||||
for line in out.split(os.linesep):
|
for line in out.split(os.linesep):
|
||||||
if line.startswith('WARNING'):
|
if line.startswith('WARNING') and \
|
||||||
|
not line[9:].startswith('zsyncmake command is missing'):
|
||||||
log('WARNING', line[9:])
|
log('WARNING', line[9:])
|
||||||
elif line.startswith('Error'):
|
elif line.startswith('Error'):
|
||||||
raise RuntimeError(line)
|
raise RuntimeError(line)
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ def _get_tk_version(python_pkg):
|
|||||||
raise RuntimeError('could not guess Tcl/Tk version')
|
raise RuntimeError('could not guess Tcl/Tk version')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tk_libdir(version):
|
||||||
|
try:
|
||||||
|
library = system(('tclsh' + version,), stdin='puts [info library]')
|
||||||
|
except SystemError:
|
||||||
|
raise RuntimeError('could not locate Tcl/Tk' + version + ' library')
|
||||||
|
|
||||||
|
return os.path.dirname(library)
|
||||||
|
|
||||||
|
|
||||||
def tcltk_env_string(python_pkg):
|
def tcltk_env_string(python_pkg):
|
||||||
'''Environment for using AppImage's TCl/Tk
|
'''Environment for using AppImage's TCl/Tk
|
||||||
'''
|
'''
|
||||||
@@ -85,15 +94,16 @@ 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 + '\''
|
expected = '\'$ORIGIN' + relpath + ':$ORIGIN/../lib\''
|
||||||
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:
|
||||||
@@ -162,11 +172,11 @@ def relocate_python(python=None, appdir=None):
|
|||||||
|
|
||||||
# Set some key variables & paths
|
# Set some key variables & paths
|
||||||
if python:
|
if python:
|
||||||
FULLVERSION = system((python, '-c',
|
FULLVERSION = system((python, '-c', '"import sys; print(sys.version)"'))
|
||||||
'"import sys; print(\'{:}.{:}.{:}\'.format(*sys.version_info[:3]))"'))
|
|
||||||
FULLVERSION = FULLVERSION.strip()
|
FULLVERSION = FULLVERSION.strip()
|
||||||
else:
|
else:
|
||||||
FULLVERSION = '{:}.{:}.{:}'.format(*sys.version_info[:3])
|
FULLVERSION = sys.version
|
||||||
|
FULLVERSION = FULLVERSION.split(None, 1)[0]
|
||||||
VERSION = '.'.join(FULLVERSION.split('.')[:2])
|
VERSION = '.'.join(FULLVERSION.split('.')[:2])
|
||||||
PYTHON_X_Y = 'python' + VERSION
|
PYTHON_X_Y = 'python' + VERSION
|
||||||
PIP_X_Y = 'pip' + VERSION
|
PIP_X_Y = 'pip' + VERSION
|
||||||
@@ -193,9 +203,21 @@ def relocate_python(python=None, appdir=None):
|
|||||||
PYTHON_LIB = PYTHON_PREFIX + '/lib'
|
PYTHON_LIB = PYTHON_PREFIX + '/lib'
|
||||||
PYTHON_PKG = PYTHON_LIB + '/' + PYTHON_X_Y
|
PYTHON_PKG = PYTHON_LIB + '/' + PYTHON_X_Y
|
||||||
|
|
||||||
|
if not os.path.exists(HOST_PKG):
|
||||||
|
paths = glob.glob(HOST_PKG + '*')
|
||||||
|
if paths:
|
||||||
|
HOST_PKG = paths[0]
|
||||||
|
PYTHON_PKG = PYTHON_LIB + '/' + os.path.basename(HOST_PKG)
|
||||||
|
else:
|
||||||
|
raise ValueError('could not find {0:}'.format(HOST_PKG))
|
||||||
|
|
||||||
if not os.path.exists(HOST_INC):
|
if not os.path.exists(HOST_INC):
|
||||||
HOST_INC += 'm'
|
paths = glob.glob(HOST_INC + '*')
|
||||||
PYTHON_INC += 'm'
|
if paths:
|
||||||
|
HOST_INC = paths[0]
|
||||||
|
PYTHON_INC = PYTHON_INC + '/' + os.path.basename(HOST_INC)
|
||||||
|
else:
|
||||||
|
raise ValueError('could not find {0:}'.format(HOST_INC))
|
||||||
|
|
||||||
|
|
||||||
# Copy the running Python's install
|
# Copy the running Python's install
|
||||||
@@ -280,22 +302,13 @@ 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)):
|
||||||
hostdir = '/usr/share/tcltk'
|
libdir = _get_tk_libdir(tk_version)
|
||||||
if os.path.exists(hostdir):
|
log('INSTALL', 'Tcl/Tk' + tk_version)
|
||||||
make_tree(APPDIR_SHARE)
|
make_tree(tcltkdir)
|
||||||
copy_tree(hostdir, tcltkdir)
|
tclpath = libdir + '/tcl' + tk_version
|
||||||
else:
|
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
|
||||||
make_tree(tcltkdir)
|
tkpath = libdir + '/tk' + tk_version
|
||||||
tclpath = '/usr/share/tcl' + tk_version
|
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
|
||||||
if not tclpath:
|
|
||||||
raise ValueError('could not find ' + tclpath)
|
|
||||||
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
|
|
||||||
|
|
||||||
tkpath = '/usr/share/tk' + tk_version
|
|
||||||
if not tkpath:
|
|
||||||
raise ValueError('could not find ' + tkpath)
|
|
||||||
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
|
|
||||||
|
|
||||||
|
|
||||||
# Copy any SSL certificate
|
# Copy any SSL certificate
|
||||||
cert_file = os.getenv('SSL_CERT_FILE')
|
cert_file = os.getenv('SSL_CERT_FILE')
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import stat
|
|||||||
import struct
|
import struct
|
||||||
|
|
||||||
from ...appimage import build_appimage
|
from ...appimage import build_appimage
|
||||||
from ...utils.compat import decode
|
from ...utils.compat import decode, find_spec
|
||||||
from ...utils.deps import PREFIX
|
from ...utils.deps import PREFIX
|
||||||
from ...utils.fs import copy_file, make_tree, remove_file, remove_tree
|
from ...utils.fs import copy_file, copy_tree, 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,14 +26,16 @@ def _unpack_args(args):
|
|||||||
'''Unpack command line arguments
|
'''Unpack command line arguments
|
||||||
'''
|
'''
|
||||||
return args.appdir, args.name, args.python_version, args.linux_tag, \
|
return args.appdir, args.name, args.python_version, args.linux_tag, \
|
||||||
args.python_tag, args.base_image, args.in_tree_build
|
args.python_tag, args.base_image, args.in_tree_build, \
|
||||||
|
args.extra_data
|
||||||
|
|
||||||
|
|
||||||
_tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage')
|
_tag_pattern = re.compile('python([^-]+)[-]([^.]+)[.]AppImage')
|
||||||
_linux_pattern = re.compile('manylinux([0-9]+)_' + platform.machine())
|
_linux_pattern = re.compile('manylinux([0-9]+)_' + platform.machine())
|
||||||
|
|
||||||
def execute(appdir, name=None, python_version=None, linux_tag=None,
|
def execute(appdir, name=None, python_version=None, linux_tag=None,
|
||||||
python_tag=None, base_image=None, in_tree_build=False):
|
python_tag=None, base_image=None, in_tree_build=False,
|
||||||
|
extra_data=None):
|
||||||
'''Build a Python application using a base AppImage
|
'''Build a Python application using a base AppImage
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -253,18 +255,49 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
|
|||||||
'WARNING: Running pip as'
|
'WARNING: Running pip as'
|
||||||
)
|
)
|
||||||
|
|
||||||
system(('./AppDir/AppRun', '-m', 'pip', 'install', '-U', in_tree_build,
|
git_warnings = (
|
||||||
|
re.compile(r'\s+Running command git (clone|checkout) '),
|
||||||
|
re.compile(r"\s+Branch '.*' set up to track remote"),
|
||||||
|
re.compile(r"\s+Switched to a new branch '.*'"),
|
||||||
|
)
|
||||||
|
|
||||||
|
isolation_flag = '-sE' if python_version[0] == '2' else '-I'
|
||||||
|
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', '-m', 'pip', 'install', '-U', in_tree_build,
|
system(('./AppDir/AppRun', isolation_flag, '-m', 'pip', 'install', '-U', in_tree_build,
|
||||||
'--no-warn-script-location', requirement),
|
'--no-warn-script-location', requirement),
|
||||||
exclude=(deprecation, ' Running command git clone'))
|
exclude=(deprecation + git_warnings))
|
||||||
|
|
||||||
|
# Bundle auxilliary application data
|
||||||
|
if extra_data is not None:
|
||||||
|
for path in extra_data:
|
||||||
|
basename = os.path.basename(path)
|
||||||
|
log('BUNDLE', basename)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
copy_tree(path, 'AppDir/' + basename)
|
||||||
|
else:
|
||||||
|
copy_file(path, 'AppDir/')
|
||||||
|
|
||||||
# Bundle the entry point
|
# Bundle the entry point
|
||||||
entrypoint_path = glob.glob(appdir + '/entrypoint.*')
|
entrypoint_path = glob.glob(appdir + '/entrypoint.*')
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import sys
|
|||||||
from ...appimage import build_appimage, relocate_python
|
from ...appimage import build_appimage, relocate_python
|
||||||
from ...utils.docker import docker_run
|
from ...utils.docker import docker_run
|
||||||
from ...utils.fs import copy_tree
|
from ...utils.fs import copy_tree
|
||||||
|
from ...utils.manylinux import format_appimage_name, format_tag
|
||||||
from ...utils.tmp import TemporaryDirectory
|
from ...utils.tmp import TemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
@@ -27,8 +28,7 @@ def _get_appimage_name(abi, tag):
|
|||||||
fullversion = desktop[13:-8]
|
fullversion = desktop[13:-8]
|
||||||
|
|
||||||
# Finish building the AppImage on the host. See below.
|
# Finish building the AppImage on the host. See below.
|
||||||
return 'python{:}-{:}-manylinux{:}.AppImage'.format(
|
return format_appimage_name(abi, fullversion, tag)
|
||||||
fullversion, abi, tag)
|
|
||||||
|
|
||||||
|
|
||||||
def execute(tag, abi, contained=False):
|
def execute(tag, abi, contained=False):
|
||||||
@@ -37,7 +37,7 @@ def execute(tag, abi, contained=False):
|
|||||||
|
|
||||||
if not contained:
|
if not contained:
|
||||||
# Forward the build to a Docker image
|
# Forward the build to a Docker image
|
||||||
image = 'quay.io/pypa/manylinux' + tag
|
image = 'quay.io/pypa/' + format_tag(tag)
|
||||||
python = '/opt/python/' + abi + '/bin/python'
|
python = '/opt/python/' + abi + '/bin/python'
|
||||||
|
|
||||||
pwd = os.getcwd()
|
pwd = os.getcwd()
|
||||||
@@ -45,7 +45,11 @@ def execute(tag, abi, contained=False):
|
|||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
copy_tree(dirname, 'python_appimage')
|
copy_tree(dirname, 'python_appimage')
|
||||||
|
|
||||||
argv = ' '.join(sys.argv[1:])
|
argv = sys.argv[1:]
|
||||||
|
if argv:
|
||||||
|
argv = ' '.join(argv)
|
||||||
|
else:
|
||||||
|
argv = 'build manylinux {:} {:}'.format(tag, abi)
|
||||||
if tag.startswith("1_"):
|
if tag.startswith("1_"):
|
||||||
# On manylinux1 tk is not installed
|
# On manylinux1 tk is not installed
|
||||||
script = [
|
script = [
|
||||||
|
|||||||
43
python_appimage/commands/list.py
Normal file
43
python_appimage/commands/list.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from ..utils.docker import docker_run
|
||||||
|
from ..utils.log import log
|
||||||
|
from ..utils.tmp import TemporaryDirectory
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['execute']
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_args(args):
|
||||||
|
'''Unpack command line arguments
|
||||||
|
'''
|
||||||
|
return (args.tag,)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(tag):
|
||||||
|
'''List python versions installed in a manylinux image
|
||||||
|
'''
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmpdir:
|
||||||
|
script = (
|
||||||
|
'for dir in $(ls /opt/python | grep "^cp[0-9]"); do',
|
||||||
|
' version=$(/opt/python/$dir/bin/python -c "import sys; ' \
|
||||||
|
'sys.stdout.write(sys.version.split()[0])")',
|
||||||
|
' echo "$dir $version"',
|
||||||
|
'done',
|
||||||
|
)
|
||||||
|
if tag.startswith('2_'):
|
||||||
|
image = 'manylinux_' + tag
|
||||||
|
else:
|
||||||
|
image = 'manylinux' + tag
|
||||||
|
result = docker_run(
|
||||||
|
'quay.io/pypa/' + image,
|
||||||
|
script,
|
||||||
|
capture = True
|
||||||
|
)
|
||||||
|
pythons = [line.split() for line in result.split(os.linesep) if line]
|
||||||
|
|
||||||
|
for (abi, version) in pythons:
|
||||||
|
log('LIST', "{:7} -> /opt/python/{:}".format(version, abi))
|
||||||
|
|
||||||
|
return pythons
|
||||||
@@ -16,6 +16,9 @@ def _unpack_args(args):
|
|||||||
def execute(binary):
|
def execute(binary):
|
||||||
'''Print the location of a binary dependency
|
'''Print the location of a binary dependency
|
||||||
'''
|
'''
|
||||||
path = os.path.join(os.path.dirname(deps.PATCHELF), binary)
|
if binary == 'appimagetool':
|
||||||
|
path = deps.ensure_appimagetool(dry=True)
|
||||||
|
else:
|
||||||
|
path = os.path.join(os.path.dirname(deps.PATCHELF), binary)
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
print(path)
|
print(path)
|
||||||
|
|||||||
19
python_appimage/data/LICENSE
Normal file
19
python_appimage/data/LICENSE
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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)"
|
self=$(readlink -f -- "$0") # Protect spaces (issue 55)
|
||||||
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,4 +1,7 @@
|
|||||||
__all__ = ['decode']
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['decode', 'encode', 'find_spec']
|
||||||
|
|
||||||
|
|
||||||
def decode(s):
|
def decode(s):
|
||||||
@@ -8,3 +11,30 @@ 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
|
||||||
|
|||||||
@@ -19,29 +19,47 @@ _ARCH = platform.machine()
|
|||||||
PREFIX = os.path.abspath(os.path.dirname(__file__) + '/..')
|
PREFIX = os.path.abspath(os.path.dirname(__file__) + '/..')
|
||||||
'''Package installation prefix'''
|
'''Package installation prefix'''
|
||||||
|
|
||||||
APPIMAGETOOL = os.path.expanduser('~/.local/bin/appimagetool')
|
APPIMAGETOOL_DIR = os.path.expanduser('~/.local/bin')
|
||||||
'''Location of the appimagetool binary'''
|
'''Location of the appimagetool binary'''
|
||||||
|
|
||||||
|
APPIMAGETOOL_VERSION = '12'
|
||||||
|
'''Version of the appimagetool binary'''
|
||||||
|
|
||||||
EXCLUDELIST = PREFIX + '/data/excludelist'
|
EXCLUDELIST = PREFIX + '/data/excludelist'
|
||||||
'''AppImage exclusion list'''
|
'''AppImage exclusion list'''
|
||||||
|
|
||||||
PATCHELF = os.path.expanduser('~/.local/bin/patchelf')
|
PATCHELF = os.path.expanduser('~/.local/bin/patchelf')
|
||||||
'''Location of the PatchELF binary'''
|
'''Location of the PatchELF binary'''
|
||||||
|
|
||||||
|
def ensure_appimagetool(dry=False):
|
||||||
def ensure_appimagetool():
|
|
||||||
'''Fetch appimagetool from the web if not available locally
|
'''Fetch appimagetool from the web if not available locally
|
||||||
'''
|
'''
|
||||||
if os.path.exists(APPIMAGETOOL):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
if APPIMAGETOOL_VERSION == '12':
|
||||||
|
appimagetool_name = 'appimagetool'
|
||||||
|
else:
|
||||||
|
appimagetool_name = 'appimagetool-' + APPIMAGETOOL_VERSION
|
||||||
|
appimagetool = os.path.join(APPIMAGETOOL_DIR, appimagetool_name)
|
||||||
|
appdir_name = '.'.join(('', appimagetool_name, 'appdir', _ARCH))
|
||||||
|
appdir = os.path.join(APPIMAGETOOL_DIR, appdir_name)
|
||||||
|
apprun = os.path.join(appdir, 'AppRun')
|
||||||
|
|
||||||
|
if dry or os.path.exists(apprun):
|
||||||
|
return apprun
|
||||||
appimage = 'appimagetool-{0:}.AppImage'.format(_ARCH)
|
appimage = 'appimagetool-{0:}.AppImage'.format(_ARCH)
|
||||||
baseurl = 'https://github.com/AppImage/AppImageKit/releases/' \
|
|
||||||
'download/12'
|
if APPIMAGETOOL_VERSION in map(str, range(1, 14)):
|
||||||
|
repository = 'AppImageKit'
|
||||||
|
else:
|
||||||
|
repository = 'appimagetool'
|
||||||
|
baseurl = os.path.join(
|
||||||
|
'https://github.com/AppImage',
|
||||||
|
repository,
|
||||||
|
'releases/download',
|
||||||
|
APPIMAGETOOL_VERSION
|
||||||
|
)
|
||||||
log('INSTALL', 'appimagetool from %s', baseurl)
|
log('INSTALL', 'appimagetool from %s', baseurl)
|
||||||
|
|
||||||
appdir_name = '.appimagetool.appdir'.format(_ARCH)
|
|
||||||
appdir = os.path.join(os.path.dirname(APPIMAGETOOL), appdir_name)
|
|
||||||
if not os.path.exists(appdir):
|
if not os.path.exists(appdir):
|
||||||
make_tree(os.path.dirname(appdir))
|
make_tree(os.path.dirname(appdir))
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
@@ -50,10 +68,7 @@ def ensure_appimagetool():
|
|||||||
system(('./' + appimage, '--appimage-extract'))
|
system(('./' + appimage, '--appimage-extract'))
|
||||||
copy_tree('squashfs-root', appdir)
|
copy_tree('squashfs-root', appdir)
|
||||||
|
|
||||||
if not os.path.exists(APPIMAGETOOL):
|
return apprun
|
||||||
os.symlink(appdir_name + '/AppRun', APPIMAGETOOL)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# Installers for dependencies
|
# Installers for dependencies
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import stat
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from .compat import decode
|
||||||
from .log import log
|
from .log import log
|
||||||
from .system import system
|
from .system import system
|
||||||
|
|
||||||
|
|
||||||
def docker_run(image, extra_cmds):
|
def docker_run(image, extra_cmds, capture=False):
|
||||||
'''Execute commands within a docker container
|
'''Execute commands within a docker container
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -42,10 +43,16 @@ def docker_run(image, extra_cmds):
|
|||||||
'type=bind,source={:},target=/pwd'.format(os.getcwd()),
|
'type=bind,source={:},target=/pwd'.format(os.getcwd()),
|
||||||
image, '/bin/bash', bash_arg))
|
image, '/bin/bash', bash_arg))
|
||||||
|
|
||||||
|
if capture:
|
||||||
|
opts = {'stderr': subprocess.PIPE, 'stdout': subprocess.PIPE}
|
||||||
|
else:
|
||||||
|
opts = {}
|
||||||
log('RUN', image)
|
log('RUN', image)
|
||||||
p = subprocess.Popen(cmd, shell=True)
|
p = subprocess.Popen(cmd, shell=True, **opts)
|
||||||
p.communicate()
|
r = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
if p.returncode == 139:
|
if p.returncode == 139:
|
||||||
sys.stderr.write("segmentation fault when running Docker (139)\n")
|
sys.stderr.write("segmentation fault when running Docker (139)\n")
|
||||||
sys.exit(p.returncode)
|
sys.exit(p.returncode)
|
||||||
|
if capture:
|
||||||
|
return decode(r[0])
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
from distutils.dir_util import mkpath as _mkpath, remove_tree as _remove_tree
|
|
||||||
from distutils.file_util import copy_file as _copy_file
|
|
||||||
import errno
|
import errno
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
from distutils.dir_util import mkpath as _mkpath
|
||||||
|
from distutils.dir_util import remove_tree as _remove_tree
|
||||||
|
from distutils.file_util import copy_file as _copy_file
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def _mkpath(path):
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
|
||||||
|
def _remove_tree(path):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
def _copy_file(source, destination, update=0):
|
||||||
|
if os.path.exists(source) and (
|
||||||
|
not update
|
||||||
|
or (
|
||||||
|
(not os.path.exists(destination))
|
||||||
|
or (os.path.getmtime(source) > os.path.getmtime(destination))
|
||||||
|
)
|
||||||
|
):
|
||||||
|
shutil.copy(source, destination)
|
||||||
|
|
||||||
from .log import debug
|
from .log import debug
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,25 @@ __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.INFO
|
level=logging.ERROR
|
||||||
)
|
)
|
||||||
|
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.info('%-8s ' + fmt, task, *args)
|
logging.getLogger('python-appimage').info('%-8s ' + fmt, task, *args)
|
||||||
|
|
||||||
|
|
||||||
def debug(task, fmt, *args):
|
def debug(task, fmt, *args):
|
||||||
'''Report some debug information
|
'''Report some debug information
|
||||||
'''
|
'''
|
||||||
logging.debug('%-8s ' + fmt, task, *args)
|
logging.getLogger('python-appimage').debug('%-8s ' + fmt, task, *args)
|
||||||
|
|
||||||
|
|
||||||
|
def set_level(level):
|
||||||
|
'''Set the threshold for logs
|
||||||
|
'''
|
||||||
|
level = getattr(logging, level)
|
||||||
|
logging.getLogger('python-appimage').setLevel(level)
|
||||||
|
|||||||
14
python_appimage/utils/manylinux.py
Normal file
14
python_appimage/utils/manylinux.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
from .compat import decode, encode
|
||||||
from .log import debug
|
from .log import debug, log
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['ldd', 'system']
|
__all__ = ['ldd', 'system']
|
||||||
@@ -15,7 +15,7 @@ except NameError:
|
|||||||
basestring = (str, bytes)
|
basestring = (str, bytes)
|
||||||
|
|
||||||
|
|
||||||
def system(args, exclude=None):
|
def system(args, exclude=None, stdin=None):
|
||||||
'''System call with capturing output
|
'''System call with capturing output
|
||||||
'''
|
'''
|
||||||
cmd = ' '.join(args)
|
cmd = ' '.join(args)
|
||||||
@@ -29,17 +29,36 @@ def system(args, exclude=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)
|
stderr=subprocess.PIPE, stdin=in_arg)
|
||||||
out, err = p.communicate()
|
out, err = p.communicate(input=stdin)
|
||||||
if err:
|
if err:
|
||||||
err = decode(err)
|
err = decode(err)
|
||||||
stripped = [line for line in err.split(os.linesep) if line]
|
stripped = [line for line in err.split(os.linesep) if line]
|
||||||
|
|
||||||
|
def matches_pattern(line, pattern):
|
||||||
|
if isinstance(pattern, re.Pattern):
|
||||||
|
return bool(pattern.match(line))
|
||||||
|
return line.startswith(pattern)
|
||||||
|
|
||||||
for pattern in exclude:
|
for pattern in exclude:
|
||||||
stripped = [line for line in stripped
|
stripped = [line for line in stripped
|
||||||
if not line.startswith(pattern)]
|
if not matches_pattern(line, pattern)]
|
||||||
|
|
||||||
if stripped:
|
if stripped:
|
||||||
raise RuntimeError(err)
|
# Tolerate single line warning(s)
|
||||||
|
for line in stripped:
|
||||||
|
if (len(line) < 8) or (line[:8].lower() != "warning:"):
|
||||||
|
raise RuntimeError(err)
|
||||||
|
else:
|
||||||
|
for line in stripped:
|
||||||
|
log('WARNING', line[8:].strip())
|
||||||
|
|
||||||
return str(decode(out).strip())
|
return str(decode(out).strip())
|
||||||
|
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ def TemporaryDirectory():
|
|||||||
try:
|
try:
|
||||||
yield tmpdir
|
yield tmpdir
|
||||||
finally:
|
finally:
|
||||||
|
debug('REMOVE', tmpdir)
|
||||||
os.chdir(pwd)
|
os.chdir(pwd)
|
||||||
remove_tree(tmpdir)
|
remove_tree(tmpdir)
|
||||||
|
|||||||
330
scripts/update-appimages.py
Executable file
330
scripts/update-appimages.py
Executable file
@@ -0,0 +1,330 @@
|
|||||||
|
#! /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')
|
||||||
|
MANYLINUSES = ('1', '2010', '2014', '2_24', '2_28')
|
||||||
|
EXCLUDES = ('2_28_i686',)
|
||||||
|
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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('-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)
|
||||||
Reference in New Issue
Block a user