update-appimages.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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 TagMeta:
  31. '''Metadata relative to a git tag
  32. '''
  33. tag: str
  34. ref: "github.GitRef"
  35. @dataclass
  36. class AssetMeta:
  37. '''Metadata relative to a release Asset
  38. '''
  39. tag: str
  40. abi: str
  41. version: str
  42. asset: Optional["github.GitReleaseAsset"] = None
  43. @classmethod
  44. def from_appimage(cls, name):
  45. '''Returns an instance from a Python AppImage name
  46. '''
  47. tmp = name[6:-9]
  48. tmp, tag = tmp.split('-manylinux', 1)
  49. if tag.startswith('_'):
  50. tag = tag[1:]
  51. version, abi = tmp.split('-', 1)
  52. return cls(
  53. tag = tag,
  54. abi = abi,
  55. version = version
  56. )
  57. def appimage_name(self):
  58. '''Returns Python AppImage name'''
  59. return format_appimage_name(self.abi, self.version, self.tag)
  60. def release_tag(self):
  61. '''Returns release git tag'''
  62. version = self.version.rsplit('.', 1)[0]
  63. return f'python{version}'
  64. def update(args):
  65. '''Update Python AppImage GitHub releases
  66. '''
  67. # Connect to GitHub
  68. if args.token is None:
  69. # First, check for token in env
  70. token = os.getenv('GITHUB_TOKEN')
  71. if token is None:
  72. # Else try to get a token from gh app
  73. p = subprocess.run(
  74. 'gh auth token',
  75. shell = True,
  76. capture_output = True,
  77. check = True
  78. )
  79. token = p.stdout.decode().strip()
  80. auth = Auth.Token(token)
  81. session = Github(auth=auth)
  82. repo = session.get_repo('niess/python-appimage')
  83. # Fetch currently released AppImages
  84. log('FETCH', 'currently released AppImages')
  85. releases = {}
  86. assets = defaultdict(dict)
  87. n_assets = 0
  88. for release in repo.get_releases():
  89. if release.tag_name.startswith('python'):
  90. releases[release.tag_name] = ReleaseMeta(
  91. tag = release.tag_name,
  92. release = release
  93. )
  94. for asset in release.get_assets():
  95. if asset.name.endswith('.AppImage'):
  96. n_assets += 1
  97. meta = AssetMeta.from_appimage(asset.name)
  98. assert(meta.release_tag() == release.tag_name)
  99. meta.asset = asset
  100. assets[meta.tag][meta.abi] = meta
  101. n_releases = len(releases)
  102. log('FETCH', f'found {n_assets} AppImages in {n_releases} releases')
  103. # Look for updates.
  104. new_releases = set()
  105. new_assets = []
  106. new_sha = []
  107. for manylinux in MANYLINUSES:
  108. for arch in ARCHS:
  109. tag = f'{manylinux}_{arch}'
  110. if tag in EXCLUDES:
  111. continue
  112. pythons = list_pythons(tag)
  113. for (abi, version) in pythons:
  114. try:
  115. meta = assets[tag][abi]
  116. except KeyError:
  117. meta = None
  118. if (meta is None) or (meta.version != version) or args.all:
  119. new_meta = AssetMeta(
  120. tag = tag,
  121. abi = abi,
  122. version = version
  123. )
  124. if meta is not None:
  125. new_meta.asset = meta.asset
  126. new_assets.append(new_meta)
  127. rtag = new_meta.release_tag()
  128. if rtag not in releases:
  129. new_releases.add(rtag)
  130. # Check SHA of tags.
  131. sha = os.getenv('GITHUB_SHA')
  132. if sha is None:
  133. p = subprocess.run(
  134. 'git rev-parse HEAD',
  135. shell = True,
  136. capture_output = True,
  137. check = True
  138. )
  139. sha = p.stdout.decode().strip()
  140. for tag in releases.keys():
  141. ref = repo.get_git_ref(f'tags/{tag}')
  142. if ref.ref is not None:
  143. if ref.object.sha != sha:
  144. meta = TagMeta(
  145. tag = tag,
  146. ref = ref
  147. )
  148. new_sha.append(meta)
  149. if args.dry:
  150. # Log foreseen changes and exit
  151. for tag in new_releases:
  152. meta = ReleaseMeta(tag)
  153. log('DRY', f'new release for {meta.title()}')
  154. for meta in new_assets:
  155. log('DRY', f'create asset {meta.appimage_name()}')
  156. if meta.asset:
  157. log('DRY', f'remove asset {meta.asset.name}')
  158. for meta in new_sha:
  159. log('DRY', f'refs/tags/{meta.tag} -> {sha}')
  160. return
  161. if new_assets:
  162. # Build new AppImage(s)
  163. cwd = os.getcwd()
  164. os.makedirs(APPIMAGES_DIR, exist_ok=True)
  165. try:
  166. os.chdir(APPIMAGES_DIR)
  167. for meta in new_assets:
  168. build_manylinux(meta.tag, meta.abi)
  169. finally:
  170. os.chdir(cwd)
  171. # Create any new release(s).
  172. for tag in new_releases:
  173. meta = ReleaseMeta(tag)
  174. title = meta.title()
  175. meta.release = repo.create_git_release(
  176. tag = meta.tag,
  177. name = title,
  178. message = f'Appimage distributions of {title} (see `Assets` below)',
  179. prerelease = True
  180. )
  181. releases[tag] = meta
  182. log('UPDATE', f'new release for {title}')
  183. # Update assets.
  184. for meta in new_assets:
  185. release = releases[meta.release_tag()].release
  186. appimage = meta.appimage_name()
  187. new_asset = release.upload_asset(
  188. path = f'{APPIMAGES_DIR}/{appimage}',
  189. name = appimage
  190. )
  191. if meta.asset:
  192. meta.asset.delete_asset()
  193. meta.asset = new_asset
  194. assets[meta.tag][meta.abi] = meta
  195. # Update git tags SHA.
  196. for meta in new_sha:
  197. meta.ref.edit(
  198. sha = sha,
  199. force = True
  200. )
  201. log('UPDATE', f'refs/tags/{meta.tag} -> {sha}')
  202. if __name__ == '__main__':
  203. parser = argparse.ArgumentParser(
  204. description = 'Update GitHub releases of Python AppImages'
  205. )
  206. parser.add_argument('-a', '--all',
  207. help = 'force update of all available releases',
  208. action = 'store_true',
  209. default = False
  210. )
  211. parser.add_argument('-d', '--dry',
  212. help = 'dry run (only log changes)',
  213. action = 'store_true',
  214. default = False
  215. )
  216. parser.add_argument('-t', '--token',
  217. help = 'GitHub authentication token'
  218. )
  219. args = parser.parse_args()
  220. update(args)