appcast_download.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import os
  2. import sys
  3. import plistlib
  4. import glob
  5. import subprocess
  6. import argparse
  7. import requests
  8. import xmltodict
  9. def download_build(url):
  10. print(f'Downloading build "{url}"...')
  11. filename = url.rpartition("/")[2]
  12. r = requests.get(url)
  13. if r.status_code == 200:
  14. with open(f"artifacts/{filename}", "wb") as f:
  15. f.write(r.content)
  16. else:
  17. print(f"Build download failed, status code: {r.status_code}")
  18. sys.exit(1)
  19. def read_appcast(url):
  20. print(f"Downloading feed {url} ...")
  21. r = requests.get(url)
  22. if r.status_code != 200:
  23. print(f"Appcast download failed, status code: {r.status_code}")
  24. sys.exit(1)
  25. filename = url.rpartition("/")[2]
  26. with open(f"builds/{filename}", "wb") as f:
  27. f.write(r.content)
  28. appcast = xmltodict.parse(r.content, force_list=("item",))
  29. dl = 0
  30. for item in appcast["rss"]["channel"]["item"]:
  31. channel = item.get("sparkle:channel", "stable")
  32. if channel != target_branch:
  33. continue
  34. if dl == max_old_vers:
  35. break
  36. download_build(item["enclosure"]["@url"])
  37. dl += 1
  38. def get_appcast_url(artifact_dir):
  39. dmgs = glob.glob(artifact_dir + "/*.dmg")
  40. if not dmgs:
  41. raise ValueError("No artifacts!")
  42. elif len(dmgs) > 1:
  43. raise ValueError("Too many artifacts!")
  44. dmg = dmgs[0]
  45. print(f"Mounting {dmg} ...")
  46. out = subprocess.check_output(
  47. [
  48. "hdiutil",
  49. "attach",
  50. "-readonly",
  51. "-noverify",
  52. "-noautoopen",
  53. "-plist",
  54. dmg,
  55. ]
  56. )
  57. d = plistlib.loads(out)
  58. mountpoint = ""
  59. for item in d["system-entities"]:
  60. if "mount-point" not in item:
  61. continue
  62. mountpoint = item["mount-point"]
  63. break
  64. url = None
  65. plist_files = glob.glob(mountpoint + "/*.app/Contents/Info.plist")
  66. if plist_files:
  67. plist_file = plist_files[0]
  68. print(f"Reading plist {plist_file} ...")
  69. plist = plistlib.load(open(plist_file, "rb"))
  70. url = plist.get("SUFeedURL", None)
  71. else:
  72. print("No Plist file found!")
  73. print(f"Unmounting {mountpoint}")
  74. subprocess.check_call(["hdiutil", "detach", mountpoint])
  75. return url
  76. if __name__ == "__main__":
  77. parser = argparse.ArgumentParser()
  78. parser.add_argument(
  79. "--artifacts-dir",
  80. dest="artifacts",
  81. action="store",
  82. default="artifacts",
  83. help="Folder containing artifact",
  84. )
  85. parser.add_argument(
  86. "--branch",
  87. dest="branch",
  88. action="store",
  89. default="stable",
  90. help="Channel/Branch",
  91. )
  92. parser.add_argument(
  93. "--max-old-versions",
  94. dest="max_old_ver",
  95. action="store",
  96. type=int,
  97. default=1,
  98. help="Maximum old versions to download",
  99. )
  100. args = parser.parse_args()
  101. target_branch = args.branch
  102. max_old_vers = args.max_old_ver
  103. url = get_appcast_url(args.artifacts)
  104. if not url:
  105. raise ValueError("Failed to get Sparkle URL from DMG!")
  106. read_appcast(url)