| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- #! /usr/bin/env python3
- import argparse
- from collections import defaultdict
- from dataclasses import dataclass
- import os
- import subprocess
- import sys
- from typing import Optional
- from github import Auth, Github
- from python_appimage.commands.build.manylinux import execute as build_manylinux
- from python_appimage.commands.list import execute as list_pythons
- from python_appimage.utils.log import log
- from python_appimage.utils.manylinux import format_appimage_name, format_tag
- # Build matrix
- ARCHS = ('x86_64', 'i686', 'aarch64')
- MANYLINUSES = ('1', '2010', '2014', '2_24', '2_28')
- EXCLUDES = ('2_28_i686', '1_aarch64', '2010_aarch64')
- # Build directory for AppImages
- APPIMAGES_DIR = 'build-appimages'
- @dataclass
- class ReleaseMeta:
- '''Metadata relative to a GitHub release
- '''
- tag: str
- ref: Optional["github.GitRef"] = None
- release: Optional["github.GitRelease"] = None
- def message(self):
- '''Returns release message'''
- return f'Appimage distributions of {self.title()} (see `Assets` below)'
- def title(self):
- '''Returns release title'''
- version = self.tag[6:]
- return f'Python {version}'
- @dataclass
- class AssetMeta:
- '''Metadata relative to a release Asset
- '''
- tag: str
- abi: str
- version: str
- asset: Optional["github.GitReleaseAsset"] = None
- @classmethod
- def from_appimage(cls, name):
- '''Returns an instance from a Python AppImage name
- '''
- tmp = name[6:-9]
- tmp, tag = tmp.split('-manylinux', 1)
- if tag.startswith('_'):
- tag = tag[1:]
- version, abi = tmp.split('-', 1)
- return cls(
- tag = tag,
- abi = abi,
- version = version
- )
- def appimage_name(self):
- '''Returns Python AppImage name'''
- return format_appimage_name(self.abi, self.version, self.tag)
- def formated_tag(self):
- '''Returns formated manylinux tag'''
- return format_tag(self.tag)
- def previous_version(self):
- '''Returns previous version'''
- if self.asset:
- return self.asset.name[6:-9].split('-', 1)[0]
- def release_tag(self):
- '''Returns release git tag'''
- version = self.version.rsplit('.', 1)[0]
- return f'python{version}'
- def update(args):
- '''Update Python AppImage GitHub releases
- '''
- sha = args.sha
- if sha is None:
- sha = os.getenv('GITHUB_SHA')
- if sha is None:
- p = subprocess.run(
- 'git rev-parse HEAD',
- shell = True,
- capture_output = True,
- check = True
- )
- sha = p.stdout.decode().strip()
- # Connect to GitHub
- token = args.token
- if token is None:
- # First, check for token in env
- token = os.getenv('GITHUB_TOKEN')
- if token is None:
- # Else try to get a token from gh app
- p = subprocess.run(
- 'gh auth token',
- shell = True,
- capture_output = True,
- check = True
- )
- token = p.stdout.decode().strip()
- auth = Auth.Token(token)
- session = Github(auth=auth)
- repo = session.get_repo('niess/python-appimage')
- # Fetch currently released AppImages
- log('FETCH', 'currently released AppImages')
- releases = {}
- assets = defaultdict(dict)
- n_assets = 0
- for release in repo.get_releases():
- if release.tag_name.startswith('python'):
- meta = ReleaseMeta(
- tag = release.tag_name,
- release = release
- )
- ref = repo.get_git_ref(f'tags/{meta.tag}')
- if (ref.ref is not None) and (ref.object.sha != sha):
- meta.ref = ref
- releases[release.tag_name] = meta
- for asset in release.get_assets():
- if asset.name.endswith('.AppImage'):
- n_assets += 1
- meta = AssetMeta.from_appimage(asset.name)
- assert(meta.release_tag() == release.tag_name)
- meta.asset = asset
- assets[meta.tag][meta.abi] = meta
- n_releases = len(releases)
- log('FETCH', f'found {n_assets} AppImages in {n_releases} releases')
- # Look for updates.
- new_releases = set()
- new_assets = []
- for manylinux in MANYLINUSES:
- for arch in ARCHS:
- tag = f'{manylinux}_{arch}'
- if tag in EXCLUDES:
- continue
- pythons = list_pythons(tag)
- for (abi, version) in pythons:
- try:
- meta = assets[tag][abi]
- except KeyError:
- meta = None
- if (meta is None) or (meta.version != version) or args.all:
- new_meta = AssetMeta(
- tag = tag,
- abi = abi,
- version = version
- )
- if meta is not None:
- new_meta.asset = meta.asset
- new_assets.append(new_meta)
- rtag = new_meta.release_tag()
- if rtag not in releases:
- new_releases.add(rtag)
- if args.dry:
- # Log foreseen changes and exit
- for tag in new_releases:
- meta = ReleaseMeta(tag)
- log('DRY', f'new release for {meta.title()}')
- for meta in new_assets:
- log('DRY', f'create asset {meta.appimage_name()}')
- if meta.asset is not None:
- log('DRY', f'remove asset {meta.asset.name}')
- for meta in releases.values():
- if meta.ref is not None:
- log('DRY', f'refs/tags/{meta.tag} -> {sha}')
- if meta.release is not None:
- log('DRY', f'reformat release for {meta.title()}')
- if new_assets:
- log('DRY', f'new update summary with {len(new_assets)} entries')
- if not args.build:
- return
- if new_assets:
- # Build new AppImage(s)
- cwd = os.getcwd()
- os.makedirs(APPIMAGES_DIR, exist_ok=True)
- try:
- os.chdir(APPIMAGES_DIR)
- for meta in new_assets:
- build_manylinux(meta.tag, meta.abi)
- finally:
- os.chdir(cwd)
- if args.dry:
- return
- # Create any new release(s).
- for tag in new_releases:
- meta = ReleaseMeta(tag)
- title = meta.title()
- meta.release = repo.create_git_release(
- tag = meta.tag,
- name = title,
- message = meta.message(),
- prerelease = True
- )
- releases[tag] = meta
- log('UPDATE', f'new release for {title}')
- # Update assets.
- update_summary = []
- for meta in new_assets:
- release = releases[meta.release_tag()].release
- appimage = meta.appimage_name()
- if meta.asset and (meta.asset.name == appimage):
- meta.asset.delete_asset()
- update_summary.append(
- f'- update {meta.formated_tag()}/{meta.abi} {meta.version}'
- )
- new_asset = release.upload_asset(
- path = f'{APPIMAGES_DIR}/{appimage}',
- name = appimage
- )
- else:
- new_asset = release.upload_asset(
- path = f'{APPIMAGES_DIR}/{appimage}',
- name = appimage
- )
- if meta.asset:
- meta.asset.delete_asset()
- update_summary.append(
- f'- update {meta.formated_tag()}/{meta.abi} '
- f'{meta.previous_version()} -> {meta.version}'
- )
- else:
- update_summary.append(
- f'- add {meta.formated_tag()}/{meta.abi} {meta.version}'
- )
- meta.asset = new_asset
- assets[meta.tag][meta.abi] = meta
- # Update git tags SHA
- for meta in releases.values():
- if meta.ref is not None:
- meta.ref.edit(
- sha = sha,
- force = True
- )
- log('UPDATE', f'refs/tags/{meta.tag} -> {sha}')
- if meta.release is not None:
- title = meta.title()
- meta.release.update_release(
- name = title,
- message = meta.message(),
- prerelease = True,
- tag_name = meta.tag
- )
- log('UPDATE', f'reformat release for {title}')
- # Generate update summary
- if update_summary:
- for release in repo.get_releases():
- if release.tag_name == 'update-summary':
- release.delete_release()
- break
- message = os.linesep.join(update_summary)
- repo.create_git_release(
- tag = 'update-summary',
- name = 'Update summary',
- message = message,
- prerelease = True
- )
- if __name__ == '__main__':
- parser = argparse.ArgumentParser(
- description = 'Update GitHub releases of Python AppImages'
- )
- parser.add_argument('-a', '--all',
- help = 'force update of all available releases',
- action = 'store_true',
- default = False
- )
- parser.add_argument('-b', '--build',
- help = 'build AppImages (in dry mode)',
- action = 'store_true',
- default = False
- )
- parser.add_argument('-d', '--dry',
- help = 'dry run (only log changes)',
- action = 'store_true',
- default = False
- )
- parser.add_argument('-m', '--manylinux',
- help = 'target specific manylinux tags',
- nargs = "+"
- )
- parser.add_argument("-s", "--sha",
- help = "reference commit SHA"
- )
- parser.add_argument('-t', '--token',
- help = 'GitHub authentication token'
- )
- args = parser.parse_args()
- if args.manylinux:
- MANYLINUSES = args.manylinux
- sys.argv = sys.argv[:1] # Empty args for fake call
- update(args)
|