core.py 12 KB

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