index.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import logging
  2. import re
  3. from collections import defaultdict
  4. from typing import Optional
  5. from packaging.version import InvalidVersion, Version
  6. from .models import ReleaseRecord
  7. # Minimum version for a vehicle to expose from manifest entries.
  8. MIN_VERSION_BY_VEHICLE_ID = {
  9. "copter": "4.3",
  10. "plane": "4.3",
  11. "rover": "4.3",
  12. "sub": "4.3",
  13. "tracker": "4.3",
  14. "blimp": "4.3",
  15. "heli": "4.3",
  16. "ap-periph": "1.8.1",
  17. }
  18. def vehicle_id_for_manifest_entry(entry: dict) -> Optional[str]:
  19. match entry.get("vehicletype", ""):
  20. case "Copter":
  21. return "heli" if entry.get("mav-type") == "HELICOPTER" else "copter"
  22. case "Plane":
  23. return "plane"
  24. case "Rover":
  25. return "rover"
  26. case "Sub":
  27. return "sub"
  28. case "Blimp":
  29. return "blimp"
  30. case "AntennaTracker":
  31. return "tracker"
  32. case "AP_Periph":
  33. return "ap-periph"
  34. case _:
  35. return None
  36. def _should_skip_version(
  37. vehicle_id: str, release_type: str, manifest_version: str
  38. ) -> bool:
  39. if release_type == "latest":
  40. return False
  41. min_version = MIN_VERSION_BY_VEHICLE_ID.get(vehicle_id)
  42. if not min_version:
  43. return False
  44. try:
  45. return Version(manifest_version) < Version(min_version)
  46. except InvalidVersion:
  47. return True
  48. def map_manifest_release_type(mav_firmware_version_type: str) -> Optional[str]:
  49. if not mav_firmware_version_type:
  50. return None
  51. if mav_firmware_version_type.startswith("STABLE-"):
  52. return "stable"
  53. if mav_firmware_version_type == "OFFICIAL":
  54. return "stable"
  55. if mav_firmware_version_type == "BETA":
  56. return "beta"
  57. if mav_firmware_version_type == "DEV":
  58. return "latest"
  59. return mav_firmware_version_type.lower()
  60. def parse_artifacts_base_url(url: str) -> Optional[str]:
  61. match = re.match(
  62. r"(https://firmware\.ardupilot\.org/[^/]+/[^/]+)/",
  63. url or "",
  64. )
  65. if match:
  66. return match.group(1)
  67. return None
  68. def _release_fields_from_entry(
  69. entry: dict,
  70. ) -> Optional[tuple[str, str, str, str, str]]:
  71. vehicle_id = vehicle_id_for_manifest_entry(entry)
  72. if vehicle_id is None:
  73. return None
  74. release_type = map_manifest_release_type(
  75. entry.get("mav-firmware-version-type", "")
  76. )
  77. manifest_version = entry.get("mav-firmware-version")
  78. if not release_type or not manifest_version:
  79. return None
  80. if _should_skip_version(vehicle_id, release_type, manifest_version):
  81. return None
  82. base_url = parse_artifacts_base_url(entry.get("url", ""))
  83. if not base_url:
  84. return None
  85. git_sha = entry.get("git-sha")
  86. if not git_sha:
  87. return None
  88. version_number = "NA" if release_type == "latest" else manifest_version
  89. return vehicle_id, release_type, version_number, base_url, git_sha
  90. def _record_release_meta(
  91. release_meta: dict[tuple, dict],
  92. vehicle_id: str,
  93. release_type: str,
  94. version_number: str,
  95. base_url: str,
  96. git_sha: str,
  97. logger: logging.Logger,
  98. ) -> None:
  99. key = (vehicle_id, release_type, version_number)
  100. if key not in release_meta:
  101. release_meta[key] = {
  102. "vehicle_id": vehicle_id,
  103. "release_type": release_type,
  104. "version_number": version_number,
  105. "ap_build_artifacts_url": base_url,
  106. "git_sha": git_sha,
  107. }
  108. return
  109. if release_meta[key]["git_sha"] != git_sha:
  110. logger.debug(
  111. "Conflicting git-sha for %s: keeping %s, ignoring %s",
  112. key,
  113. release_meta[key]["git_sha"][:8],
  114. git_sha[:8],
  115. )
  116. def _releases_from_meta(
  117. release_meta: dict[tuple, dict],
  118. ) -> dict[str, list[ReleaseRecord]]:
  119. releases_by_vehicle: dict[str, list[ReleaseRecord]] = defaultdict(list)
  120. for meta in release_meta.values():
  121. releases_by_vehicle[meta["vehicle_id"]].append(
  122. ReleaseRecord(
  123. vehicle_id=meta["vehicle_id"],
  124. release_type=meta["release_type"],
  125. version_number=meta["version_number"],
  126. commit_reference=meta["git_sha"],
  127. ap_build_artifacts_url=meta["ap_build_artifacts_url"],
  128. )
  129. )
  130. for vehicle_id in releases_by_vehicle:
  131. releases_by_vehicle[vehicle_id].sort(
  132. key=lambda r: (
  133. 0 if r.release_type == "latest" else 1,
  134. r.release_type,
  135. _version_sort_key(r.version_number),
  136. )
  137. )
  138. return dict(releases_by_vehicle)
  139. class ManifestIndex:
  140. """In-memory index built from a parsed manifest document."""
  141. def __init__(self, releases_by_vehicle: dict[str, list[ReleaseRecord]]):
  142. self.releases_by_vehicle = releases_by_vehicle
  143. @classmethod
  144. def build(cls, manifest: dict) -> "ManifestIndex":
  145. logger = logging.getLogger(__name__)
  146. release_meta: dict[tuple, dict] = {}
  147. for entry in manifest.get("firmware") or []:
  148. fields = _release_fields_from_entry(entry)
  149. if fields is None:
  150. continue
  151. vehicle_id, release_type, version_number, base_url, git_sha = fields
  152. _record_release_meta(
  153. release_meta,
  154. vehicle_id,
  155. release_type,
  156. version_number,
  157. base_url,
  158. git_sha,
  159. logger,
  160. )
  161. return cls(_releases_from_meta(release_meta))
  162. def get_releases(self, vehicle_id: str) -> list[ReleaseRecord]:
  163. return list(self.releases_by_vehicle.get(vehicle_id, []))
  164. def _version_sort_key(version_number: str) -> tuple:
  165. if version_number == "NA":
  166. return (0,)
  167. try:
  168. parsed = Version(version_number)
  169. return (1, parsed.major, parsed.minor, parsed.micro)
  170. except InvalidVersion:
  171. return (2, version_number)