builder.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. import ap_git
  2. from build_manager import (
  3. BuildManager as bm,
  4. )
  5. import subprocess
  6. import os
  7. import shutil
  8. import logging
  9. import tarfile
  10. from metadata_manager import (
  11. APSourceMetadataFetcher as apfetch,
  12. RemoteInfo,
  13. )
  14. from pathlib import Path
  15. class Builder:
  16. """
  17. Processes build requests, perform builds and ship build artifacts
  18. to the destination directory shared by BuildManager.
  19. """
  20. def __init__(self, workdir: str, source_repo: ap_git.GitRepo) -> None:
  21. """
  22. Initialises the Builder class.
  23. Parameters:
  24. workdir (str): Workspace for the builder.
  25. source_repo (ap_git.GitRepo): Ardupilot repository to be used for
  26. retrieving source for doing builds.
  27. Raises:
  28. RuntimeError: If BuildManager or APSourceMetadataFetcher is not
  29. initialised.
  30. """
  31. if bm.get_singleton() is None:
  32. raise RuntimeError(
  33. "BuildManager should be initialized first."
  34. )
  35. if apfetch.get_singleton() is None:
  36. raise RuntimeError(
  37. "APSourceMetadataFetcher should be initialised first."
  38. )
  39. self.__workdir_parent = workdir
  40. self.__master_repo = source_repo
  41. self.logger = logging.getLogger(__name__)
  42. def __log_build_info(self, build_id: str) -> None:
  43. """
  44. Logs the build information to the build log.
  45. Parameters:
  46. build_id (str): Unique identifier for the build.
  47. """
  48. build_info = bm.get_singleton().get_build_info(build_id)
  49. logpath = bm.get_singleton().get_build_log_path(build_id)
  50. with open(logpath, "a") as build_log:
  51. build_log.write(f"Vehicle: {build_info.vehicle}\n"
  52. f"Board: {build_info.board}\n"
  53. f"Remote URL: {build_info.remote_info.url}\n"
  54. f"git-sha: {build_info.git_hash}\n"
  55. "---\n"
  56. "Selected Features:\n")
  57. for d in build_info.selected_features:
  58. build_log.write(f"{d}\n")
  59. build_log.write("---\n")
  60. def __generate_extrahwdef(self, build_id: str) -> None:
  61. """
  62. Generates the extra hardware definition file (`extra_hwdef.dat`) for
  63. the build.
  64. Parameters:
  65. build_id (str): Unique identifier for the build.
  66. Raises:
  67. RuntimeError: If the parent directory for putting `extra_hwdef.dat`
  68. does not exist.
  69. """
  70. # Log to build log
  71. logpath = bm.get_singleton().get_build_log_path(build_id)
  72. with open(logpath, "a") as build_log:
  73. build_log.write("Generating extrahwdef file...\n")
  74. path = self.__get_path_to_extra_hwdef(build_id)
  75. self.logger.debug(
  76. f"Path to extra_hwdef for build id {build_id}: {path}"
  77. )
  78. if not os.path.exists(os.path.dirname(path)):
  79. raise RuntimeError(
  80. f"Create parent directory '{os.path.dirname(path)}' "
  81. "before writing extra_hwdef.dat"
  82. )
  83. build_info = bm.get_singleton().get_build_info(build_id)
  84. selected_features = build_info.selected_features
  85. self.logger.debug(
  86. f"Selected features for {build_id}: {selected_features}"
  87. )
  88. all_features = apfetch.get_singleton().get_build_options_at_commit(
  89. remote=build_info.remote_info.name,
  90. commit_ref=build_info.git_hash,
  91. )
  92. all_defines = {
  93. feature.define
  94. for feature in all_features
  95. }
  96. enabled_defines = selected_features.intersection(all_defines)
  97. disabled_defines = all_defines.difference(enabled_defines)
  98. self.logger.info(f"Enabled defines for {build_id}: {enabled_defines}")
  99. self.logger.info(f"Disabled defines for {build_id}: {enabled_defines}")
  100. with open(self.__get_path_to_extra_hwdef(build_id), "w") as f:
  101. # Undefine all defines at the beginning
  102. for define in all_defines:
  103. f.write(f"undef {define}\n")
  104. # Enable selected defines
  105. for define in enabled_defines:
  106. f.write(f"define {define} 1\n")
  107. # Disable the remaining defines
  108. for define in disabled_defines:
  109. f.write(f"define {define} 0\n")
  110. def __ensure_remote_added(self, remote_info: RemoteInfo) -> None:
  111. """
  112. Ensures that the remote repository is correctly added to the
  113. master repository.
  114. Parameters:
  115. remote_info (RemoteInfo): Information about the remote repository.
  116. """
  117. try:
  118. self.__master_repo.remote_add(
  119. remote=remote_info.name,
  120. url=remote_info.url,
  121. )
  122. self.logger.info(
  123. f"Added remote {remote_info.name} to master repo."
  124. )
  125. except ap_git.DuplicateRemoteError:
  126. self.logger.debug(
  127. f"Remote {remote_info.name} already exists."
  128. f"Setting URL to {remote_info.url}."
  129. )
  130. # Update the URL if the remote already exists
  131. self.__master_repo.remote_set_url(
  132. remote=remote_info.name,
  133. url=remote_info.url,
  134. )
  135. self.logger.info(
  136. f"Updated remote url to {remote_info.url}"
  137. f"for remote {remote_info.name}"
  138. )
  139. def __provision_build_source(self, build_id: str) -> None:
  140. """
  141. Provisions the source code for a specific build.
  142. Parameters:
  143. build_id (str): Unique identifier for the build.
  144. """
  145. # Log to build log
  146. logpath = bm.get_singleton().get_build_log_path(build_id)
  147. with open(logpath, "a") as build_log:
  148. build_log.write("Cloning build source...\n")
  149. build_info = bm.get_singleton().get_build_info(build_id)
  150. logging.info(
  151. f"Ensuring {build_info.remote_info.name} is added to master repo."
  152. )
  153. self.__ensure_remote_added(build_info.remote_info)
  154. logging.info(f"Cloning build source for {build_id} from master repo.")
  155. ap_git.GitRepo.shallow_clone_at_commit_from_local(
  156. source=self.__master_repo.get_local_path(),
  157. remote=build_info.remote_info.name,
  158. commit_ref=build_info.git_hash,
  159. dest=self.__get_path_to_build_src(build_id),
  160. )
  161. def __create_build_artifacts_dir(self, build_id: str) -> None:
  162. """
  163. Creates the output directory to store build artifacts.
  164. Parameters:
  165. build_id (str): Unique identifier for the build.
  166. """
  167. p = Path(bm.get_singleton().get_build_artifacts_dir_path(build_id))
  168. self.logger.info(f"Creating directory at {p}.")
  169. try:
  170. Path.mkdir(p, parents=True)
  171. except FileExistsError:
  172. shutil.rmtree(p)
  173. Path.mkdir(p)
  174. def __create_build_workdir(self, build_id: str) -> None:
  175. """
  176. Creates the working directory for the build.
  177. Parameters:
  178. build_id (str): Unique identifier for the build.
  179. """
  180. p = Path(self.__get_path_to_build_dir(build_id))
  181. self.logger.info(f"Creating directory at {p}.")
  182. try:
  183. Path.mkdir(p, parents=True)
  184. except FileExistsError:
  185. shutil.rmtree(p)
  186. Path.mkdir(p)
  187. def __generate_archive(self, build_id: str) -> None:
  188. """
  189. Placeholder for generating the zipped build artifact.
  190. Parameters:
  191. build_id (str): Unique identifier for the build.
  192. """
  193. build_info = bm.get_singleton().get_build_info(build_id)
  194. archive_path = bm.get_singleton().get_build_archive_path(build_id)
  195. files_to_include = []
  196. # include binaries
  197. bin_path = os.path.join(
  198. self.__get_path_to_build_dir(build_id),
  199. build_info.board,
  200. "bin"
  201. )
  202. bin_list = os.listdir(bin_path)
  203. self.logger.debug(f"bin_path: {bin_path}")
  204. self.logger.debug(f"bin_list: {bin_list}")
  205. for file in bin_list:
  206. file_path_abs = os.path.abspath(
  207. os.path.join(bin_path, file)
  208. )
  209. files_to_include.append(file_path_abs)
  210. # include log
  211. log_path_abs = os.path.abspath(
  212. bm.get_singleton().get_build_log_path(build_id)
  213. )
  214. files_to_include.append(log_path_abs)
  215. # include extra_hwdef.dat
  216. extra_hwdef_path_abs = os.path.abspath(
  217. self.__get_path_to_extra_hwdef(build_id)
  218. )
  219. files_to_include.append(extra_hwdef_path_abs)
  220. # create archive
  221. with tarfile.open(archive_path, "w:gz") as tar:
  222. for file in files_to_include:
  223. arcname = f"{build_id}/{os.path.basename(file)}"
  224. self.logger.debug(f"Added {file} as {arcname}")
  225. tar.add(file, arcname=arcname)
  226. self.logger.info(f"Generated {archive_path}.")
  227. def __clean_up_build_workdir(self, build_id: str) -> None:
  228. shutil.rmtree(self.__get_path_to_build_dir(build_id))
  229. def __process_build(self, build_id: str) -> None:
  230. """
  231. Processes a new build by preparing source code and extra_hwdef file
  232. and running the build finally.
  233. Parameters:
  234. build_id (str): Unique identifier for the build.
  235. """
  236. self.__create_build_workdir(build_id)
  237. self.__create_build_artifacts_dir(build_id)
  238. self.__log_build_info(build_id)
  239. self.__provision_build_source(build_id)
  240. self.__generate_extrahwdef(build_id)
  241. self.__build(build_id)
  242. self.__generate_archive(build_id)
  243. self.__clean_up_build_workdir(build_id)
  244. def __get_path_to_build_dir(self, build_id: str) -> str:
  245. """
  246. Returns the path to the temporary workspace for a build.
  247. This directory contains the source code and extra_hwdef.dat file.
  248. Parameters:
  249. build_id (str): Unique identifier for the build.
  250. Returns:
  251. str: Path to the build directory.
  252. """
  253. return os.path.join(self.__workdir_parent, build_id)
  254. def __get_path_to_extra_hwdef(self, build_id: str) -> str:
  255. """
  256. Returns the path to the extra_hwdef definition file for a build.
  257. Parameters:
  258. build_id (str): Unique identifier for the build.
  259. Returns:
  260. str: Path to the extra hardware definition file.
  261. """
  262. return os.path.join(
  263. self.__get_path_to_build_dir(build_id),
  264. "extra_hwdef.dat",
  265. )
  266. def __get_path_to_build_src(self, build_id: str) -> str:
  267. """
  268. Returns the path to the source code for a build.
  269. Parameters:
  270. build_id (str): Unique identifier for the build.
  271. Returns:
  272. str: Path to the build source directory.
  273. """
  274. return os.path.join(
  275. self.__get_path_to_build_dir(build_id),
  276. "build_src"
  277. )
  278. def __build(self, build_id: str) -> None:
  279. """
  280. Executes the actual build process for a build.
  281. This should be called after preparing build source code and
  282. extra_hwdef file.
  283. Parameters:
  284. build_id (str): Unique identifier for the build.
  285. Raises:
  286. RuntimeError: If source directory or extra hardware definition
  287. file does not exist.
  288. """
  289. if not os.path.exists(self.__get_path_to_build_dir(build_id)):
  290. raise RuntimeError("Creating build before building.")
  291. if not os.path.exists(self.__get_path_to_build_src(build_id)):
  292. raise RuntimeError("Cannot build without source code.")
  293. if not os.path.exists(self.__get_path_to_extra_hwdef(build_id)):
  294. raise RuntimeError("Cannot build without extra_hwdef.dat file.")
  295. build_info = bm.get_singleton().get_build_info(build_id)
  296. source_repo = ap_git.GitRepo(self.__get_path_to_build_src(build_id))
  297. # Checkout the specific commit and ensure submodules are updated
  298. source_repo.checkout_remote_commit_ref(
  299. remote=build_info.remote_info.name,
  300. commit_ref=build_info.git_hash,
  301. force=True,
  302. hard_reset=True,
  303. clean_working_tree=True,
  304. )
  305. source_repo.submodule_update(init=True, recursive=True, force=True)
  306. logpath = bm.get_singleton().get_build_log_path(build_id)
  307. with open(logpath, "a") as build_log:
  308. # Log initial configuration
  309. build_log.write(
  310. "Setting vehicle to: "
  311. f"{build_info.vehicle.capitalize()}\n"
  312. )
  313. build_log.flush()
  314. # Run the build steps
  315. self.logger.info("Running waf configure")
  316. build_log.write("Running waf configure\n")
  317. build_log.flush()
  318. subprocess.run(
  319. [
  320. "python3",
  321. "./waf",
  322. "configure",
  323. "--board",
  324. build_info.board,
  325. "--out",
  326. self.__get_path_to_build_dir(build_id),
  327. "--extra-hwdef",
  328. self.__get_path_to_extra_hwdef(build_id),
  329. ],
  330. cwd=self.__get_path_to_build_src(build_id),
  331. stdout=build_log,
  332. stderr=build_log,
  333. shell=False,
  334. )
  335. self.logger.info("Running clean")
  336. build_log.write("Running clean\n")
  337. build_log.flush()
  338. subprocess.run(
  339. ["python3", "./waf", "clean"],
  340. cwd=self.__get_path_to_build_src(build_id),
  341. stdout=build_log,
  342. stderr=build_log,
  343. shell=False,
  344. )
  345. self.logger.info("Running build")
  346. build_log.write("Running build\n")
  347. build_log.flush()
  348. subprocess.run(
  349. ["python3", "./waf", build_info.vehicle.lower()],
  350. cwd=self.__get_path_to_build_src(build_id),
  351. stdout=build_log,
  352. stderr=build_log,
  353. shell=False,
  354. )
  355. build_log.write("done build\n")
  356. build_log.flush()
  357. def run(self) -> None:
  358. """
  359. Continuously processes builds in the queue until termination.
  360. """
  361. while True:
  362. build_to_process = bm.get_singleton().get_next_build_id()
  363. self.__process_build(build_id=build_to_process)