35 Commits

Author SHA1 Message Date
Valentin Niess
af59728145 Generate update summary 2023-11-12 23:41:03 +01:00
Valentin Niess
f5f7349f46 Update release messages 2023-11-12 22:56:40 +01:00
Valentin Niess
a2a075f9db Manual triggers for actions 2023-11-11 01:16:04 +01:00
Valentin Niess
4bc98f48d4 Option to force update 2023-11-10 23:44:55 +01:00
Valentin Niess
fd7e28817c Manage SHA of git tags 2023-11-10 22:43:46 +01:00
Valentin Niess
602f65c0e8 Merge branch 'master' into dev 2023-11-10 15:17:59 +01:00
Valentin Niess
4fcdf2cba1 Python AppImages updater 2023-11-10 15:04:24 +01:00
Valentin Niess
e249fdebdb Use a named logger 2023-11-10 10:18:12 +01:00
Valentin Niess
e596fec38b List command for manylinux images 2023-11-08 18:06:02 +01:00
Valentin Niess
818fe273c1 Add $ORIGIN/../lib to RPATH 2023-02-15 10:43:43 +01:00
Valentin Niess
db5d91e0dd Bump version to 1.2.5 2022-12-23 14:06:05 +01:00
Valentin Niess
c28641bd84 Change OS for app test 2022-12-23 13:56:36 +01:00
Valentin Niess
d5875464d0 Explicit licenses 2022-12-23 13:40:44 +01:00
Valentin Niess
6fbb227e3a Correct PyPI url 2022-11-28 20:14:40 +01:00
Valentin Niess
b5ad9a6dcf Bump version to 1.2.4 2022-11-14 22:36:11 +01:00
Valentin Niess
4ec94ba00e Document 2_24 and 2_28 2022-11-14 22:32:29 +01:00
Valentin Niess
6bfb15b186 Add Manylinux 2_24 and 2_28 2022-11-14 18:45:15 +01:00
Valentin Niess
2161858718 Bump version to 1.2.3 2022-09-12 10:31:07 +02:00
Valentin Niess
6dfa764573 Protect spaces (issue 55) 2022-09-12 10:24:14 +02:00
Valentin Niess
df67460a7c Bump version to 1.2.2 2022-09-08 11:17:55 +02:00
Valentin Niess
9706c81569 Explicit version for tasmotizer test 2022-09-08 11:12:48 +02:00
Valentin Niess
528f797ddf Locate Tcl/Tk using tclsh 2022-09-08 10:27:03 +02:00
Valentin
d7fe43facf Bump version to 1.2.1 2022-08-06 22:37:41 +02:00
Stanislav Dimitrov
5d085e38ee Use importlib.util, if importing importlib fails.
Issue reported for python>3.8
2022-08-06 22:29:16 +02:00
Valentin Niess
678aae1393 Bump version to 1.2.0 2022-07-21 10:53:47 +02:00
Valentin Niess
96a8cbbfab Document local requirements 2022-07-21 10:53:15 +02:00
Valentin Niess
e2efafa081 Patch isolation flag 2022-07-21 10:31:56 +02:00
Vladimir Ivan
061fd7414d Python 2 compat for local requirements
(cherry picked from commit c34610e63cc9b509d324bc0ffb09c100063af74e)
2022-07-21 10:17:28 +02:00
Vladimir Ivan
7b9b4f2b75 Pip in isolated environment 2022-07-21 10:17:28 +02:00
Vladimir Ivan
a99d31e661 Added local copy option to dependency check 2022-07-21 10:17:28 +02:00
Valentin Niess
984a1ccec0 Bump version to 1.1.4 2022-07-18 15:16:54 +02:00
Valentin Niess
bbd549c3a1 Patch log import in system 2022-07-18 15:14:40 +02:00
Valentin Niess
5513645e55 Bump version to 1.1.3 2022-07-18 15:02:38 +02:00
Valentin Niess
dc54fa8231 Remove 3.11 from manylinux 2010 2022-07-18 14:55:32 +02:00
Valentin Niess
d259ad4f49 Tolerate single line warning(s) 2022-07-18 14:48:26 +02:00
22 changed files with 621 additions and 161 deletions

View File

@@ -1,89 +1,36 @@
name: AppImage
on:
push:
branches:
- master
paths:
- '.github/workflows/appimage.yml'
- 'python_appimage/**'
workflow_dispatch:
inputs:
dry:
description: 'Dry run'
required: true
type: boolean
all:
description: 'Update all'
required: true
type: boolean
schedule:
- cron: '0 3 * * 0'
jobs:
Build:
Update:
runs-on: ubuntu-latest
strategy:
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
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Build
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
- name: Install Dependencies
run: pip install PyGithub
- name: Run updater
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 }}
./scripts/update-appimages.py \
--token=${{ secrets.GITHUB_TOKEN }} \
--sha=${{ github.sha }} \
${{ inputs.all && '--all' || '' }} \
${{ inputs.dry && '--dry' || '' }}
env:
PYTHONPATH: ${{ github.workspace }}

View File

@@ -1,27 +1,39 @@
name: Applications
on:
push:
branches:
- master
paths:
- '.github/workflows/applications.yml'
- 'applications/**'
- 'python_appimage/**'
workflow_dispatch:
inputs:
scipy:
required: true
default: true
type: boolean
tasmotizer:
required: true
default: true
type: boolean
xonsh:
required: true
default: true
type: boolean
ssh-mitm:
required: true
default: true
type: boolean
jobs:
Test:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
version: ['2.7', '3.7', '3.9']
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.version }}
- name: Test scipy
if: ${{ inputs.scipy }}
run: |
python -m python_appimage build app applications/scipy \
--python-version=2.7 \
@@ -30,19 +42,22 @@ jobs:
./scipy-x86_64.AppImage -c 'import numpy, pandas, scipy'
- name: Test tasmotizer
if: ${{ inputs.tasmotizer }}
run: |
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
- name: Test xonsh
if: ${{ inputs.xonsh }}
run: |
python -m python_appimage build app applications/xonsh
test -e xonsh-x86_64.AppImage
./xonsh-x86_64.AppImage -c 'import xonsh'
- name: Test ssh-mitm
if: ${{ matrix.version == '3.9' }}
if: ${{ inputs.ssh_mitm && (matrix.version == '3.9') }}
run: |
python -m python_appimage build app applications/ssh-mitm
test -e ssh-mitm-x86_64.AppImage

View File

@@ -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

View File

@@ -8,7 +8,7 @@ on:
jobs:
Test:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
version: ['2.7', '3.9']

View File

@@ -31,11 +31,22 @@ Python apps, given an existing Python AppImage and a recipe folder.
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
[APPIMAGE]: https://appimage.org/
[GITHUB]: https://github.com/niess/python-appimage
[GRAND]: http://grand.cnrs.fr
[MANYLINUX]: https://github.com/pypa/manylinux
[PSF_LICENSE]: https://docs.python.org/3/license.html#psf-license
[PYPI]: https://pypi.org/project/python-appimage/
[READTHEDOCS]: https://python-appimage.readthedocs.io/en/latest/
[RELEASES]: https://github.com/niess/python-appimage/releases

View File

@@ -1 +1 @@
1.1.2
1.2.5

View File

@@ -13,7 +13,7 @@
[LINUXDEPLOY]: https://github.com/linuxdeploy/linuxdeploy/
[MANYLINUX]: https://github.com/pypa/manylinux/
[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/
[SHEBANG]: https://en.wikipedia.org/wiki/Shebang_(Unix)/
[VENV]: https://docs.python.org/3/library/venv.html/

View File

@@ -121,9 +121,9 @@ files. The `requirements.txt` file allows to specify additional site packages
to be bundled in the AppImage, using `pip`.
!!! Caution
Site packages bundled in the AppImage, as well as their dependencies, must
either be pure python packages, or they must be available as portable binary
wheels.
For the application to be portable, site packages bundled in the AppImage,
as well as their dependencies, must must be available as binary wheels, or
be pure Python packages.
If a **C extension** is bundled from **source**, then it will likely **not
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
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") }}
### Entry point script

View File

@@ -11,11 +11,23 @@ $.getJSON("https://api.github.com/repos/niess/python-appimage/releases").done(fu
/* Parse AppImage metadata */
const tmp0 = asset.name.split("manylinux")
const tag = tmp0[1].slice(0,-9);
const tmp1 = tag.split(/_(.+)/);
const linux = tmp1[0]
const arch = tmp1[1]
const tmp2 = tmp0[0].split("-")
const python = tmp2[1] + "-" + tmp2[2]
const tmp1 = tag.split(/_(.+)/, 2);
var linux = undefined;
var arch = undefined;
if (tmp1[0] == "") {
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({
name: asset.name,
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) {
const index = asset.name.indexOf("-")
full_version = asset.name.slice(6, index)
const index = asset.name.indexOf("-");
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}
`);
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}
`);
}

View File

@@ -1,6 +1,5 @@
import argparse
from importlib import import_module
import logging
import os
import sys
@@ -24,9 +23,9 @@ def main():
dest='command')
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',
dest='verbosity', action='store_const', const=logging.DEBUG)
dest='verbosity', action='store_const', const='DEBUG')
install_parser = subparsers.add_parser('install',
description='Install binary dependencies')
@@ -75,6 +74,11 @@ def main():
action='store_true',
default=False)
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',
description='Locate a binary dependency')
which_parser.add_argument('binary', choices=binaries,
@@ -84,7 +88,8 @@ def main():
# Configure the verbosity
if args.verbosity:
logging.getLogger().setLevel(args.verbosity)
from .utils import log
log.set_level(args.verbosity)
# check if no arguments are passed
if args.command is None:

View File

@@ -34,6 +34,15 @@ def _get_tk_version(python_pkg):
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):
'''Environment for using AppImage's TCl/Tk
'''
@@ -89,7 +98,7 @@ def patch_binary(path, libdir, recursive=True):
rpath = '\'' + system((PATCHELF, '--print-rpath', path)) + '\''
relpath = os.path.relpath(libdir, os.path.dirname(path))
relpath = '' if relpath == '.' else '/' + relpath
expected = '\'$ORIGIN' + relpath + '\''
expected = '\'$ORIGIN' + relpath + ':$ORIGIN/../lib\''
if rpath != expected:
system((PATCHELF, '--set-rpath', expected, path))
@@ -280,22 +289,13 @@ def relocate_python(python=None, appdir=None):
tcltkdir = APPDIR_SHARE + '/tcltk'
if (not os.path.exists(tcltkdir + '/tcl' + tk_version)) or \
(not os.path.exists(tcltkdir + '/tk' + tk_version)):
hostdir = '/usr/share/tcltk'
if os.path.exists(hostdir):
make_tree(APPDIR_SHARE)
copy_tree(hostdir, tcltkdir)
else:
make_tree(tcltkdir)
tclpath = '/usr/share/tcl' + tk_version
if not tclpath:
raise ValueError('could not find ' + tclpath)
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
tkpath = '/usr/share/tk' + tk_version
if not tkpath:
raise ValueError('could not find ' + tkpath)
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
libdir = _get_tk_libdir(tk_version)
log('INSTALL', 'Tcl/Tk' + tk_version)
make_tree(tcltkdir)
tclpath = libdir + '/tcl' + tk_version
copy_tree(tclpath, tcltkdir + '/tcl' + tk_version)
tkpath = libdir + '/tk' + tk_version
copy_tree(tkpath, tcltkdir + '/tk' + tk_version)
# Copy any SSL certificate
cert_file = os.getenv('SSL_CERT_FILE')

View File

@@ -8,9 +8,9 @@ import stat
import struct
from ...appimage import build_appimage
from ...utils.compat import decode
from ...utils.compat import decode, find_spec
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.system import system
from ...utils.template import copy_template, load_template
@@ -253,15 +253,31 @@ def execute(appdir, name=None, python_version=None, linux_tag=None,
'WARNING: Running pip as'
)
system(('./AppDir/AppRun', '-m', 'pip', 'install', '-U', in_tree_build,
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)
for requirement in requirements_list:
if requirement.startswith('git+'):
url, name = os.path.split(requirement)
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:
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),
exclude=(deprecation, ' Running command git clone'))

View File

@@ -7,6 +7,7 @@ import sys
from ...appimage import build_appimage, relocate_python
from ...utils.docker import docker_run
from ...utils.fs import copy_tree
from ...utils.manylinux import format_appimage_name, format_tag
from ...utils.tmp import TemporaryDirectory
@@ -27,8 +28,7 @@ def _get_appimage_name(abi, tag):
fullversion = desktop[13:-8]
# Finish building the AppImage on the host. See below.
return 'python{:}-{:}-manylinux{:}.AppImage'.format(
fullversion, abi, tag)
return format_appimage_name(abi, fullversion, tag)
def execute(tag, abi, contained=False):
@@ -37,7 +37,7 @@ def execute(tag, abi, contained=False):
if not contained:
# 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'
pwd = os.getcwd()
@@ -45,7 +45,11 @@ def execute(tag, abi, contained=False):
with TemporaryDirectory() as tmpdir:
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_"):
# On manylinux1 tk is not installed
script = [

View 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

View 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.

View File

@@ -2,16 +2,16 @@
# If running from an extracted image, then export ARGV0 and APPDIR
if [ -z "${APPIMAGE}" ]; then
export ARGV0=$0
export ARGV0="$0"
self="$(readlink -f -- $0)"
self=$(readlink -f -- "$0") # Protect spaces (issue 55)
here="${self%/*}"
tmp="${here%/*}"
export APPDIR="${tmp%/*}"
fi
# Resolve the calling command (preserving symbolic links).
export APPIMAGE_COMMAND="$(command -v -- $ARGV0)"
export APPIMAGE_COMMAND=$(command -v -- "$ARGV0")
{{ tcltk-env }}
{{ cert-file }}

View File

@@ -1,4 +1,7 @@
__all__ = ['decode']
import sys
__all__ = ['decode', 'encode', 'find_spec']
def decode(s):
@@ -8,3 +11,30 @@ def decode(s):
return s.decode()
except Exception:
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

View File

@@ -4,11 +4,12 @@ import stat
import subprocess
import sys
from .compat import decode
from .log import log
from .system import system
def docker_run(image, extra_cmds):
def docker_run(image, extra_cmds, capture=False):
'''Execute commands within a docker container
'''
@@ -42,10 +43,16 @@ def docker_run(image, extra_cmds):
'type=bind,source={:},target=/pwd'.format(os.getcwd()),
image, '/bin/bash', bash_arg))
if capture:
opts = {'stderr': subprocess.PIPE, 'stdout': subprocess.PIPE}
else:
opts = {}
log('RUN', image)
p = subprocess.Popen(cmd, shell=True)
p.communicate()
p = subprocess.Popen(cmd, shell=True, **opts)
r = p.communicate()
if p.returncode != 0:
if p.returncode == 139:
sys.stderr.write("segmentation fault when running Docker (139)\n")
sys.exit(p.returncode)
if capture:
return decode(r[0])

View File

@@ -7,17 +7,25 @@ __all__ = ['debug', 'log']
# Configure the logger
logging.basicConfig(
format='[%(asctime)s] %(message)s',
level=logging.INFO
level=logging.ERROR
)
logging.getLogger('python-appimage').setLevel(logging.INFO)
def log(task, fmt, *args):
'''Log a standard message
'''
logging.info('%-8s ' + fmt, task, *args)
logging.getLogger('python-appimage').info('%-8s ' + fmt, task, *args)
def debug(task, fmt, *args):
'''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)

View 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

View File

@@ -2,8 +2,8 @@ import os
import re
import subprocess
from .compat import decode
from .log import debug
from .compat import decode, encode
from .log import debug, log
__all__ = ['ldd', 'system']
@@ -15,7 +15,7 @@ except NameError:
basestring = (str, bytes)
def system(args, exclude=None):
def system(args, exclude=None, stdin=None):
'''System call with capturing output
'''
cmd = ' '.join(args)
@@ -29,9 +29,15 @@ def system(args, exclude=None):
exclude = list(exclude)
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,
stderr=subprocess.PIPE)
out, err = p.communicate()
stderr=subprocess.PIPE, stdin=in_arg)
out, err = p.communicate(input=stdin)
if err:
err = decode(err)
stripped = [line for line in err.split(os.linesep) if line]
@@ -39,7 +45,13 @@ def system(args, exclude=None):
stripped = [line for line in stripped
if not line.startswith(pattern)]
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())

312
scripts/update-appimages.py Executable file
View File

@@ -0,0 +1,312 @@
#! /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()
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("-s", "--sha",
help = "reference commit SHA"
)
parser.add_argument('-t', '--token',
help = 'GitHub authentication token'
)
args = parser.parse_args()
sys.argv = sys.argv[:1] # Empty args for fake call
update(args)