update-appimages.py 7.4 KB

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