update-appimages.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #! /usr/bin/env python3
  2. import argparse
  3. from collections import defaultdict
  4. from dataclasses import dataclass
  5. import os
  6. import subprocess
  7. from typing import Optional
  8. from github import Auth, Github
  9. from python_appimage.commands.build.manylinux import execute as build_manylinux
  10. from python_appimage.commands.list import execute as list_pythons
  11. from python_appimage.utils.log import log
  12. from python_appimage.utils.manylinux import format_appimage_name
  13. # Build matrix
  14. ARCHS = ('x86_64', 'i686')
  15. MANYLINUSES = ('1', '2010', '2014', '2_24', '2_28')
  16. EXCLUDES = ('2_28_i686',)
  17. # Build directory for AppImages
  18. APPIMAGES_DIR = 'build-appimages'
  19. @dataclass
  20. class ReleaseMeta:
  21. '''Metadata relative to a GitHub release
  22. '''
  23. tag: str
  24. release: Optional["github.GitRelease"] = None
  25. def title(self):
  26. '''Returns release title'''
  27. version = self.tag[6:]
  28. return f'Python {version}'
  29. @dataclass
  30. class AssetMeta:
  31. '''Metadata relative to a release Asset
  32. '''
  33. tag: str
  34. abi: str
  35. version: str
  36. asset: Optional["github.GitReleaseAsset"] = None
  37. @classmethod
  38. def from_appimage(cls, name):
  39. '''Returns an instance from a Python AppImage name
  40. '''
  41. tmp = name[6:-9]
  42. tmp, tag = tmp.split('-manylinux', 1)
  43. if tag.startswith('_'):
  44. tag = tag[1:]
  45. version, abi = tmp.split('-', 1)
  46. return cls(
  47. tag = tag,
  48. abi = abi,
  49. version = version
  50. )
  51. def appimage_name(self):
  52. '''Returns Python AppImage name'''
  53. return format_appimage_name(self.abi, self.version, self.tag)
  54. def release_tag(self):
  55. '''Returns release git tag'''
  56. version = self.version.rsplit('.', 1)[0]
  57. return f'python{version}'
  58. def update(args):
  59. '''Update Python AppImage GitHub releases
  60. '''
  61. # Connect to GitHub
  62. if args.token is None:
  63. # Get token from gh app (e.g. for local runs)
  64. p = subprocess.run(
  65. 'gh auth token',
  66. shell = True,
  67. capture_output = True,
  68. check = True
  69. )
  70. token = p.stdout.decode().strip()
  71. auth = Auth.Token(token)
  72. session = Github(auth=auth)
  73. repo = session.get_repo('niess/python-appimage')
  74. # Fetch currently released AppImages
  75. log('FETCH', 'Currently released AppImages')
  76. releases = {}
  77. assets = defaultdict(dict)
  78. n_assets = 0
  79. for release in repo.get_releases():
  80. if release.tag_name.startswith('python'):
  81. releases[release.tag_name] = ReleaseMeta(
  82. tag = release.tag_name,
  83. release = release
  84. )
  85. for asset in release.get_assets():
  86. if asset.name.endswith('.AppImage'):
  87. n_assets += 1
  88. meta = AssetMeta.from_appimage(asset.name)
  89. assert(meta.release_tag() == release.tag_name)
  90. meta.asset = asset
  91. assets[meta.tag][meta.abi] = meta
  92. n_releases = len(releases)
  93. log('FETCH', f'Found {n_assets} AppImages in {n_releases} releases')
  94. # Look for updates.
  95. new_releases = set()
  96. new_assets = []
  97. for manylinux in MANYLINUSES:
  98. for arch in ARCHS:
  99. tag = f'{manylinux}_{arch}'
  100. if tag in EXCLUDES:
  101. continue
  102. pythons = list_pythons(tag)
  103. for (abi, version) in pythons:
  104. try:
  105. meta = assets[tag][abi]
  106. except KeyError:
  107. meta = None
  108. if meta is None or meta.version != version:
  109. new_meta = AssetMeta(
  110. tag = tag,
  111. abi = abi,
  112. version = version
  113. )
  114. if meta is not None:
  115. new_meta.asset = meta.asset
  116. new_assets.append(new_meta)
  117. rtag = new_meta.release_tag()
  118. if rtag not in releases:
  119. new_releases.add(rtag)
  120. if not new_assets:
  121. return
  122. # Build new AppImage(s)
  123. cwd = os.getcwd()
  124. os.makedirs(APPIMAGES_DIR, exist_ok=True)
  125. try:
  126. os.chdir(APPIMAGES_DIR)
  127. for meta in new_assets:
  128. build_manylinux(meta.tag, meta.abi)
  129. finally:
  130. os.chdir(cwd)
  131. # Create any new release(s).
  132. repo = session.get_repo('niess/test-releases') # XXX
  133. for tag in new_releases:
  134. meta = ReleaseMeta(tag)
  135. title = meta.title()
  136. meta.release = repo.create_git_release(
  137. tag = meta.tag,
  138. name = title,
  139. message = f'Appimage distributions of {title} (see `Assets` below)',
  140. prerelease = True
  141. )
  142. releases[tag] = meta
  143. # Update assets.
  144. for meta in new_assets:
  145. release = releases[meta.release_tag()].release
  146. appimage = meta.appimage_name()
  147. new_asset = release.upload_asset(
  148. path = f'{APPIMAGES_DIR}/{appimage}',
  149. label = appimage
  150. )
  151. if meta.asset:
  152. meta.asset.delete_asset()
  153. meta.asset = new_asset
  154. assets[meta.tag][meta.abi] = meta
  155. if __name__ == '__main__':
  156. parser = argparse.ArgumentParser(
  157. description = "Update GitHub releases of Python AppImages"
  158. )
  159. parser.add_argument("-t", "--token",
  160. help = "GitHub authentication token"
  161. )
  162. parser.add_argument("-s", "--sha",
  163. help = "Current commit SHA"
  164. )
  165. args = parser.parse_args()
  166. update(args)