update-appimages.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. # Get token from gh app (e.g. for local runs)
  70. p = subprocess.run(
  71. 'gh auth token',
  72. shell = True,
  73. capture_output = True,
  74. check = True
  75. )
  76. token = p.stdout.decode().strip()
  77. auth = Auth.Token(token)
  78. session = Github(auth=auth)
  79. repo = session.get_repo('niess/python-appimage')
  80. # Fetch currently released AppImages
  81. log('FETCH', 'currently released AppImages')
  82. releases = {}
  83. assets = defaultdict(dict)
  84. n_assets = 0
  85. for release in repo.get_releases():
  86. if release.tag_name.startswith('python'):
  87. releases[release.tag_name] = ReleaseMeta(
  88. tag = release.tag_name,
  89. release = release
  90. )
  91. for asset in release.get_assets():
  92. if asset.name.endswith('.AppImage'):
  93. n_assets += 1
  94. meta = AssetMeta.from_appimage(asset.name)
  95. assert(meta.release_tag() == release.tag_name)
  96. meta.asset = asset
  97. assets[meta.tag][meta.abi] = meta
  98. n_releases = len(releases)
  99. log('FETCH', f'found {n_assets} AppImages in {n_releases} releases')
  100. # Look for updates.
  101. new_releases = set()
  102. new_assets = []
  103. new_sha = []
  104. for manylinux in MANYLINUSES:
  105. for arch in ARCHS:
  106. tag = f'{manylinux}_{arch}'
  107. if tag in EXCLUDES:
  108. continue
  109. pythons = list_pythons(tag)
  110. for (abi, version) in pythons:
  111. try:
  112. meta = assets[tag][abi]
  113. except KeyError:
  114. meta = None
  115. if meta is None or meta.version != version:
  116. new_meta = AssetMeta(
  117. tag = tag,
  118. abi = abi,
  119. version = version
  120. )
  121. if meta is not None:
  122. new_meta.asset = meta.asset
  123. new_assets.append(new_meta)
  124. rtag = new_meta.release_tag()
  125. if rtag not in releases:
  126. new_releases.add(rtag)
  127. # Check SHA of tags.
  128. p = subprocess.run(
  129. 'git rev-parse HEAD',
  130. shell = True,
  131. capture_output = True,
  132. check = True
  133. )
  134. sha = p.stdout.decode().strip()
  135. for tag in releases.keys():
  136. ref = repo.get_git_ref(f'tags/{tag}')
  137. if ref.ref is not None:
  138. if ref.object.sha != sha:
  139. meta = TagMeta(
  140. tag = tag,
  141. ref = ref
  142. )
  143. new_sha.append(meta)
  144. # Log foreseen changes.
  145. for tag in new_releases:
  146. meta = ReleaseMeta(tag)
  147. log('FORESEEN', f'create new release for {meta.title()}')
  148. for meta in new_assets:
  149. log('FORESEEN', f'create asset {meta.appimage_name()}')
  150. if meta.asset:
  151. log('FORESEEN', f'remove asset {meta.asset.name}')
  152. for meta in new_sha:
  153. log('FORESEEN', f'update git SHA for refs/tags/{meta.tag}')
  154. if args.dry:
  155. return
  156. if new_assets:
  157. # Build new AppImage(s)
  158. cwd = os.getcwd()
  159. os.makedirs(APPIMAGES_DIR, exist_ok=True)
  160. try:
  161. os.chdir(APPIMAGES_DIR)
  162. for meta in new_assets:
  163. build_manylinux(meta.tag, meta.abi)
  164. finally:
  165. os.chdir(cwd)
  166. # Create any new release(s).
  167. for tag in new_releases:
  168. meta = ReleaseMeta(tag)
  169. title = meta.title()
  170. meta.release = repo.create_git_release(
  171. tag = meta.tag,
  172. name = title,
  173. message = f'Appimage distributions of {title} (see `Assets` below)',
  174. prerelease = True
  175. )
  176. releases[tag] = meta
  177. # Update assets.
  178. for meta in new_assets:
  179. release = releases[meta.release_tag()].release
  180. appimage = meta.appimage_name()
  181. new_asset = release.upload_asset(
  182. path = f'{APPIMAGES_DIR}/{appimage}',
  183. name = appimage
  184. )
  185. if meta.asset:
  186. meta.asset.delete_asset()
  187. meta.asset = new_asset
  188. assets[meta.tag][meta.abi] = meta
  189. # Update git tags SHA.
  190. for meta in new_sha:
  191. meta.ref.edit(
  192. sha = sha,
  193. force = True
  194. )
  195. if __name__ == '__main__':
  196. parser = argparse.ArgumentParser(
  197. description = 'Update GitHub releases of Python AppImages'
  198. )
  199. parser.add_argument('-d', '--dry',
  200. help = 'dry run (only log changes)',
  201. action = 'store_true',
  202. default = False
  203. )
  204. parser.add_argument('-t', '--token',
  205. help = 'GitHub authentication token'
  206. )
  207. # XXX Add --all arg
  208. args = parser.parse_args()
  209. update(args)