diff --git a/python_appimage/utils/manylinux.py b/python_appimage/utils/manylinux.py new file mode 100644 index 0000000..9b45882 --- /dev/null +++ b/python_appimage/utils/manylinux.py @@ -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 diff --git a/scripts/update-appimages.py b/scripts/update-appimages.py new file mode 100755 index 0000000..729530a --- /dev/null +++ b/scripts/update-appimages.py @@ -0,0 +1,202 @@ +#! /usr/bin/env python3 +import argparse +from collections import defaultdict +from dataclasses import dataclass +import os +import subprocess +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 + + +# 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 + + release: Optional["github.GitRelease"] = None + + 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 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 + ''' + + # Connect to GitHub + if args.token is None: + # Get token from gh app (e.g. for local runs) + 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'): + releases[release.tag_name] = ReleaseMeta( + tag = release.tag_name, + release = release + ) + 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: + 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 not new_assets: + return + + # 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). + repo = session.get_repo('niess/test-releases') # XXX + + for tag in new_releases: + meta = ReleaseMeta(tag) + title = meta.title() + meta.release = repo.create_git_release( + tag = meta.tag, + name = title, + message = f'Appimage distributions of {title} (see `Assets` below)', + prerelease = True + ) + releases[tag] = meta + + # Update assets. + 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}', + label = appimage + ) + if meta.asset: + meta.asset.delete_asset() + meta.asset = new_asset + assets[meta.tag][meta.abi] = meta + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description = "Update GitHub releases of Python AppImages" + ) + parser.add_argument("-t", "--token", + help = "GitHub authentication token" + ) + parser.add_argument("-s", "--sha", + help = "Current commit SHA" + ) + + args = parser.parse_args() + update(args)