builder.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import os
  2. import shutil
  3. import subprocess
  4. import logging
  5. import time
  6. from build_manager import BuildManager, BuildState
  7. class Builder:
  8. def __init__(self, build_id: str = None, workdir: str = None, **kwargs):
  9. self.logger = logging.getLogger(__name__)
  10. self.bm = BuildManager.get_singleton()
  11. # --- MODIFICATION START ---
  12. # Reason: Upstream (web app) and worker container need a consistent
  13. # base directory. We default to /base where the ArduPilot source lives.
  14. self.workdir = workdir or os.environ.get("CBS_BASEDIR", "/base")
  15. # --- MODIFICATION END ---
  16. self.build_id = build_id
  17. if self.build_id:
  18. self._setup_paths()
  19. def _setup_paths(self):
  20. self.artifacts_dir = self.bm.get_build_artifacts_dir_path(self.build_id)
  21. self.log_file = self.bm.get_build_log_path(self.build_id)
  22. self.info = self.bm.get_build_info(self.build_id)
  23. def __run_cmd(self, cmd, cwd, log_handle):
  24. # --- MODIFICATION START ---
  25. # Reason: Using python3 explicitly to run 'waf' is more robust than
  26. # relying on the execution bit (+x) inside a Docker volume.
  27. process = subprocess.Popen(
  28. cmd, cwd=cwd, stdout=log_handle, stderr=subprocess.STDOUT, text=True
  29. )
  30. process.wait()
  31. if process.returncode != 0:
  32. raise subprocess.CalledProcessError(process.returncode, cmd)
  33. # --- MODIFICATION END ---
  34. def build(self, build_id: str = None):
  35. if build_id:
  36. self.build_id = build_id
  37. self._setup_paths()
  38. if not self.build_id:
  39. raise ValueError("No build_id provided to Builder.")
  40. try:
  41. self.logger.info(f"[{self.build_id}] Starting build process...")
  42. os.makedirs(self.artifacts_dir, exist_ok=True)
  43. # --- MODIFICATION START ---
  44. # Reason: We must ensure we are in the directory containing 'waf'.
  45. # If the path is wrong, the build fails immediately.
  46. repo = self.workdir
  47. if not os.path.exists(os.path.join(repo, "waf")):
  48. self.logger.error(f"[{self.build_id}] waf not found in {repo}")
  49. raise FileNotFoundError(f"Cannot find waf in {repo}")
  50. with open(self.log_file, "a") as log:
  51. log.write(f"Starting build: {self.info.vehicle_id} on {self.info.board}\n")
  52. # --- EQUALMASS OVERLAY INJECTION ---
  53. # Reason: This is where your custom files from the sidecar manager
  54. # are merged into the ArduPilot source before the compiler starts.
  55. overlay_dir = "/app/overlay"
  56. if os.path.exists(overlay_dir) and os.listdir(overlay_dir):
  57. self.logger.info(f"[{self.build_id}] Custom overlay found. Injecting...")
  58. shutil.copytree(overlay_dir, repo, dirs_exist_ok=True)
  59. else:
  60. self.logger.info(f"[{self.build_id}] No overlay files. Building vanilla.")
  61. # Running waf via python3 for better Docker compatibility
  62. self.logger.info(f"[{self.build_id}] Running waf configure...")
  63. self.__run_cmd(["python3", "waf", "configure", "--board", self.info.board], repo, log)
  64. self.logger.info(f"[{self.build_id}] Running waf build...")
  65. self.__run_cmd(["python3", "waf", self.info.vehicle_id], repo, log)
  66. # --- MODIFICATION END ---
  67. self.bm.update_build_progress_state(self.build_id, BuildState.SUCCESS)
  68. except Exception as e:
  69. self.logger.error(f"[{self.build_id}] Build failed: {e}")
  70. self.bm.update_build_progress_state(self.build_id, BuildState.FAILURE)
  71. # --- MODIFICATION START ---
  72. # Reason: The builder container needs this loop to stay alive and poll
  73. # Redis for jobs. The web app also checks for this method during startup.
  74. def run(self):
  75. """Main worker heartbeat loop."""
  76. self.logger.info("Worker online. Waiting for builds from Redis...")
  77. while True:
  78. try:
  79. build_id = self.bm.get_next_build_id()
  80. if build_id:
  81. self.logger.info(f"Job received: {build_id}")
  82. self.build(build_id=build_id)
  83. time.sleep(2) # Prevent CPU spiking
  84. except Exception as e:
  85. self.logger.error(f"Error in worker loop: {e}")
  86. time.sleep(5)
  87. # --- MODIFICATION END ---