core.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import logging
  2. import time
  3. import os
  4. import fnmatch
  5. import ap_git
  6. import json
  7. import jsonschema
  8. from . import exceptions as ex
  9. from threading import Lock
  10. from utils import TaskRunner
  11. logger = logging.getLogger(__name__)
  12. class APSourceMetadataFetcher:
  13. """
  14. Class to fetch metadata like available boards, features etc.
  15. from the AP source code
  16. """
  17. __singleton = None
  18. def __init__(self, ap_repo: ap_git.GitRepo) -> None:
  19. """
  20. Initializes the APSourceMetadataFetcher instance
  21. with a given repository path.
  22. Parameters:
  23. ap_repo (GitRepo): ArduPilot local git repository containing
  24. the metadata generation scripts.
  25. Raises:
  26. TooManyInstancesError: If an instance of this class already exists,
  27. enforcing a singleton pattern.
  28. """
  29. # Enforce singleton pattern by raising an error if
  30. # an instance already exists.
  31. if APSourceMetadataFetcher.__singleton:
  32. raise ex.TooManyInstancesError()
  33. self.repo = ap_repo
  34. APSourceMetadataFetcher.__singleton = self
  35. def get_boards_at_commit(self, remote: str,
  36. commit_ref: str) -> tuple:
  37. """
  38. Retrieves a list of boards available for building at a
  39. specified commit and returns the list and the default board.
  40. Parameters:
  41. remote (str): The name of the remote repository.
  42. commit_ref (str): The commit reference to check out.
  43. Returns:
  44. tuple: A tuple containing:
  45. - boards (list): A list of boards available at the
  46. specified commit.
  47. - default_board (str): The first board in the sorted list,
  48. designated as the default.
  49. """
  50. tstart = time.time()
  51. import importlib.util
  52. with self.repo.get_checkout_lock():
  53. self.repo.checkout_remote_commit_ref(
  54. remote=remote,
  55. commit_ref=commit_ref,
  56. force=True,
  57. hard_reset=True,
  58. clean_working_tree=True
  59. )
  60. spec = importlib.util.spec_from_file_location(
  61. name="board_list.py",
  62. location=os.path.join(
  63. self.repo.get_local_path(),
  64. 'Tools', 'scripts',
  65. 'board_list.py')
  66. )
  67. mod = importlib.util.module_from_spec(spec)
  68. spec.loader.exec_module(mod)
  69. all_boards = mod.AUTOBUILD_BOARDS
  70. exclude_patterns = ['fmuv*', 'SITL*']
  71. boards = []
  72. for b in all_boards:
  73. excluded = False
  74. for p in exclude_patterns:
  75. if fnmatch.fnmatch(b.lower(), p.lower()):
  76. excluded = True
  77. break
  78. if not excluded:
  79. boards.append(b)
  80. logger.debug(
  81. f"Took {(time.time() - tstart)} seconds to get boards"
  82. )
  83. boards.sort()
  84. default_board = boards[0]
  85. return (boards, default_board)
  86. def get_build_options_at_commit(self, remote: str,
  87. commit_ref: str) -> list:
  88. """
  89. Retrieves a list of build options available at a specified commit.
  90. Parameters:
  91. remote (str): The name of the remote repository.
  92. commit_ref (str): The commit reference to check out.
  93. Returns:
  94. list: A list of build options available at the specified commit.
  95. """
  96. tstart = time.time()
  97. import importlib.util
  98. with self.repo.get_checkout_lock():
  99. self.repo.checkout_remote_commit_ref(
  100. remote=remote,
  101. commit_ref=commit_ref,
  102. force=True,
  103. hard_reset=True,
  104. clean_working_tree=True
  105. )
  106. spec = importlib.util.spec_from_file_location(
  107. name="build_options.py",
  108. location=os.path.join(
  109. self.repo.get_local_path(),
  110. 'Tools',
  111. 'scripts',
  112. 'build_options.py'
  113. )
  114. )
  115. mod = importlib.util.module_from_spec(spec)
  116. spec.loader.exec_module(mod)
  117. build_options = mod.BUILD_OPTIONS
  118. logger.debug(
  119. f"Took {(time.time() - tstart)} seconds to get build options"
  120. )
  121. return build_options
  122. @staticmethod
  123. def get_singleton():
  124. return APSourceMetadataFetcher.__singleton
  125. class VersionInfo:
  126. """
  127. Class to wrap version info properties inside a single object
  128. """
  129. def __init__(self,
  130. remote: str,
  131. commit_ref: str,
  132. release_type: str,
  133. version_number: str,
  134. ap_build_artifacts_url) -> None:
  135. self.remote = remote
  136. self.commit_ref = commit_ref
  137. self.release_type = release_type
  138. self.version_number = version_number
  139. self.ap_build_artifacts_url = ap_build_artifacts_url
  140. class RemoteInfo:
  141. """
  142. Class to wrap remote info properties inside a single object
  143. """
  144. def __init__(self,
  145. name: str,
  146. url: str) -> None:
  147. self.name = name
  148. self.url = url
  149. class VersionsFetcher:
  150. """
  151. Class to fetch the version-to-build metadata from remotes.json
  152. and provide methods to view the same
  153. """
  154. __singleton = None
  155. def __init__(self, remotes_json_path: str,
  156. ap_repo: ap_git.GitRepo):
  157. """
  158. Initializes the VersionsFetcher instance
  159. with a given remotes.json path.
  160. Parameters:
  161. remotes_json_path (str): Path to the remotes.json file.
  162. ap_repo (GitRepo): ArduPilot local git repository. This local
  163. repository is shared between the VersionsFetcher
  164. and the APSourceMetadataFetcher.
  165. Raises:
  166. TooManyInstancesError: If an instance of this class already exists,
  167. enforcing a singleton pattern.
  168. """
  169. # Enforce singleton pattern by raising an error if
  170. # an instance already exists.
  171. if VersionsFetcher.__singleton:
  172. raise ex.TooManyInstancesError()
  173. self.__remotes_json_path = remotes_json_path
  174. self.__access_lock_versions_metadata = Lock()
  175. self.__versions_metadata = []
  176. tasks = (
  177. (self.fetch_ap_releases, 1200),
  178. (self.fetch_whitelisted_tags, 1200),
  179. )
  180. self.__task__runner = TaskRunner(tasks=tasks)
  181. self.repo = ap_repo
  182. VersionsFetcher.__singleton = self
  183. def start(self) -> None:
  184. """
  185. Start auto-fetch jobs.
  186. """
  187. logger.info("Starting VersionsFetcher background auto-fetch jobs.")
  188. self.__task__runner.start()
  189. def get_all_remotes_info(self) -> list[RemoteInfo]:
  190. """
  191. Return the list of RemoteInfo objects constructed from the
  192. information in the remotes.json file
  193. Returns:
  194. list: RemoteInfo objects for all remotes mentioned in remotes.json
  195. """
  196. return [
  197. RemoteInfo(
  198. name=remote.get('name', None),
  199. url=remote.get('url', None)
  200. )
  201. for remote in self.__get_versions_metadata()
  202. ]
  203. def get_versions_for_vehicle(self, vehicle_name: str) -> list[VersionInfo]:
  204. """
  205. Return the list of dictionaries containing the info about the
  206. versions listed to be built for a particular vehicle.
  207. Parameters:
  208. vehicle_name (str): the vehicle to fetch versions list for
  209. Returns:
  210. list: VersionInfo objects for all versions allowed to be
  211. built for the said vehicle.
  212. """
  213. if vehicle_name is None:
  214. raise ValueError("Vehicle is a required parameter.")
  215. versions_list = []
  216. for remote in self.__get_versions_metadata():
  217. for vehicle in remote['vehicles']:
  218. if vehicle['name'] != vehicle_name:
  219. continue
  220. for release in vehicle['releases']:
  221. versions_list.append(VersionInfo(
  222. remote=remote.get('name', None),
  223. commit_ref=release.get('commit_reference', None),
  224. release_type=release.get('release_type', None),
  225. version_number=release.get('version_number', None),
  226. ap_build_artifacts_url=release.get(
  227. 'ap_build_artifacts_url',
  228. None
  229. )
  230. ))
  231. return versions_list
  232. def get_all_vehicles_sorted_uniq(self) -> list[str]:
  233. """
  234. Return a sorted list of all vehicles listed in remotes.json structure
  235. Returns:
  236. list: Vehicles listed in remotes.json
  237. """
  238. vehicles_set = set()
  239. for remote in self.__get_versions_metadata():
  240. for vehicle in remote['vehicles']:
  241. vehicles_set.add(vehicle['name'])
  242. return sorted(list(vehicles_set))
  243. def is_version_listed(self, vehicle: str, remote: str,
  244. commit_ref: str) -> bool:
  245. """
  246. Check if a version with given properties mentioned in remotes.json
  247. Parameters:
  248. vehicle (str): vehicle for which version is listed
  249. remote (str): remote under which the version is listed
  250. commit_ref(str): commit reference for the version
  251. Returns:
  252. bool: True if the said version is mentioned in remotes.json,
  253. False otherwise
  254. """
  255. if vehicle is None:
  256. raise ValueError("Vehicle is a required parameter.")
  257. if remote is None:
  258. raise ValueError("Remote is a required parameter.")
  259. if commit_ref is None:
  260. raise ValueError("Commit reference is a required parameter.")
  261. return (remote, commit_ref) in [
  262. (version_info.remote, version_info.commit_ref)
  263. for version_info in
  264. self.get_versions_for_vehicle(vehicle_name=vehicle)
  265. ]
  266. def get_version_info(self, vehicle: str, remote: str,
  267. commit_ref: str) -> VersionInfo:
  268. """
  269. Find first version matching the given properties in remotes.json
  270. Parameters:
  271. vehicle (str): vehicle for which version is listed
  272. remote (str): remote under which the version is listed
  273. commit_ref(str): commit reference for the version
  274. Returns:
  275. VersionInfo: Object for the version matching the properties,
  276. None if not found
  277. """
  278. return next(
  279. (
  280. version
  281. for version in self.get_versions_for_vehicle(
  282. vehicle_name=vehicle
  283. )
  284. if version.remote == remote and
  285. version.commit_ref == commit_ref
  286. ),
  287. None
  288. )
  289. def reload_remotes_json(self) -> None:
  290. """
  291. Read remotes.json, validate its structure against the schema
  292. and cache it in memory
  293. """
  294. # load file containing vehicles listed to be built for each
  295. # remote along with the branches/tags/commits on which the
  296. # firmware can be built
  297. remotes_json_schema_path = os.path.join(
  298. os.path.dirname(__file__),
  299. 'remotes.schema.json'
  300. )
  301. with open(self.__remotes_json_path, 'r') as f, \
  302. open(remotes_json_schema_path, 'r') as s:
  303. versions_metadata = json.loads(f.read())
  304. schema = json.loads(s.read())
  305. # validate schema
  306. jsonschema.validate(instance=versions_metadata, schema=schema)
  307. self.__set_versions_metadata(versions_metadata=versions_metadata)
  308. # update git repo with latest remotes list
  309. self.__sync_remotes_with_ap_repo()
  310. def __set_versions_metadata(self, versions_metadata: list) -> None:
  311. """
  312. Set versions metadata property with the one passed as parameter
  313. This requires to acquire the access lock to avoid overwriting the
  314. object while it is being read
  315. """
  316. if versions_metadata is None:
  317. raise ValueError("versions_metadata is a required parameter. "
  318. "Cannot be None.")
  319. with self.__access_lock_versions_metadata:
  320. self.__versions_metadata = versions_metadata
  321. def __get_versions_metadata(self) -> list:
  322. """
  323. Read versions metadata property
  324. This requires to acquire the access lock to avoid reading the list
  325. while it is being modified
  326. Returns:
  327. list: the versions metadata list
  328. """
  329. with self.__access_lock_versions_metadata:
  330. return self.__versions_metadata
  331. def __sync_remotes_with_ap_repo(self):
  332. """
  333. Update the remotes in ArduPilot local repository with the latest
  334. remotes list.
  335. """
  336. remotes = tuple(
  337. (remote.name, remote.url)
  338. for remote in self.get_all_remotes_info()
  339. )
  340. self.repo.remote_add_bulk(remotes=remotes, force=True)
  341. def fetch_ap_releases(self) -> None:
  342. """
  343. Execute the fetch_releases.py script to update remotes.json
  344. with Ardupilot's official releases
  345. """
  346. from scripts import fetch_releases
  347. fetch_releases.run(
  348. base_dir=os.path.join(
  349. os.path.dirname(self.__remotes_json_path),
  350. '..',
  351. ),
  352. remote_name="ardupilot",
  353. )
  354. self.reload_remotes_json()
  355. return
  356. def fetch_whitelisted_tags(self) -> None:
  357. """
  358. Execute the fetch_whitelisted_tags.py script to update
  359. remotes.json with tags from whitelisted repos
  360. """
  361. from scripts import fetch_whitelisted_tags
  362. fetch_whitelisted_tags.run(
  363. base_dir=os.path.join(
  364. os.path.dirname(self.__remotes_json_path),
  365. '..',
  366. )
  367. )
  368. self.reload_remotes_json()
  369. return
  370. @staticmethod
  371. def get_singleton():
  372. return VersionsFetcher.__singleton