mirror of
https://github.com/niess/python-appimage.git
synced 2025-07-21 04:41:14 +02:00
340 lines
9.8 KiB
Python
Executable File
340 lines
9.8 KiB
Python
Executable File
#! /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)
|