| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- """
- Builds service for handling build-related business logic.
- """
- import logging
- import os
- from fastapi import Request
- from typing import List, Optional
- from schemas import (
- BuildRequest,
- BuildSubmitResponse,
- BuildOut,
- BuildProgress,
- RemoteInfo,
- )
- from schemas.vehicles import VehicleBase, BoardBase
- # Import external modules
- # pylint: disable=wrong-import-position
- import build_manager # noqa: E402
- logger = logging.getLogger(__name__)
- class BuildsService:
- """Service for managing firmware builds."""
- def __init__(
- self,
- build_manager=None,
- versions_fetcher=None,
- ap_src_metadata_fetcher=None,
- repo=None,
- vehicles_manager=None
- ):
- self.manager = build_manager
- self.versions_fetcher = versions_fetcher
- self.ap_src_metadata_fetcher = ap_src_metadata_fetcher
- self.repo = repo
- self.vehicles_manager = vehicles_manager
- def create_build(
- self,
- build_request: BuildRequest,
- client_ip: str
- ) -> BuildSubmitResponse:
- """
- Create a new build request.
- Args:
- build_request: Build configuration
- client_ip: Client IP address for rate limiting
- Returns:
- Simple response with build_id and URL
- Raises:
- ValueError: If validation fails
- """
- # Validate version_id
- if not build_request.version_id:
- raise ValueError("version_id is required")
- # Validate vehicle
- vehicle_id = build_request.vehicle_id
- if not vehicle_id:
- raise ValueError("vehicle_id is required")
- # Get version info using version_id
- version_info = self.versions_fetcher.get_version_info(
- vehicle_id=vehicle_id,
- version_id=build_request.version_id
- )
- if version_info is None:
- raise ValueError("Invalid version_id for vehicle")
- remote_name = version_info.remote_info.name
- commit_ref = version_info.commit_ref
- # Validate remote
- remote_info = self.versions_fetcher.get_remote_info(remote_name)
- if remote_info is None:
- raise ValueError(f"Remote {remote_name} is not whitelisted")
- # Validate board
- board_name = build_request.board_id
- if not board_name:
- raise ValueError("board_id is required")
- # Check board exists at this version
- with self.repo.get_checkout_lock():
- boards_at_commit = self.ap_src_metadata_fetcher.get_boards(
- remote=remote_name,
- commit_ref=commit_ref,
- vehicle_id=vehicle_id,
- )
- if board_name not in boards_at_commit:
- raise ValueError("Invalid board for this version")
- # Get git hash
- git_hash = self.repo.commit_id_for_remote_ref(
- remote=remote_name,
- commit_ref=commit_ref
- )
- # Map feature labels (IDs from API) to defines
- # (required by build manager)
- selected_feature_defines = set()
- if build_request.selected_features:
- # Get build options to map labels to defines
- with self.repo.get_checkout_lock():
- options = (
- self.ap_src_metadata_fetcher
- .get_build_options_at_commit(
- remote=remote_name,
- commit_ref=commit_ref
- )
- )
- # Create label to define mapping
- label_to_define = {
- option.label: option.define for option in options
- }
- # Map each selected feature label to its define
- for feature_label in build_request.selected_features:
- if feature_label in label_to_define:
- selected_feature_defines.add(
- label_to_define[feature_label]
- )
- else:
- logger.warning(
- f"Feature label '{feature_label}' not found in "
- f"build options for {vehicle_id} {remote_name} "
- f"{commit_ref}"
- )
- # Create build info
- build_info = build_manager.BuildInfo(
- vehicle_id=vehicle_id,
- remote_info=remote_info,
- git_hash=git_hash,
- board=board_name,
- selected_features=selected_feature_defines
- )
- # Submit build
- build_id = self.manager.submit_build(
- build_info=build_info,
- client_ip=client_ip,
- )
- # Return simple submission response
- return BuildSubmitResponse(
- build_id=build_id,
- url=f"/api/v1/builds/{build_id}",
- status="submitted"
- )
- def list_builds(
- self,
- vehicle_id: Optional[str] = None,
- board_id: Optional[str] = None,
- state: Optional[str] = None,
- limit: int = 20,
- offset: int = 0
- ) -> List[BuildOut]:
- """
- Get list of builds with optional filters.
- Args:
- vehicle_id: Filter by vehicle
- board_id: Filter by board
- state: Filter by build state
- limit: Maximum results
- offset: Results to skip
- Returns:
- List of builds
- """
- all_build_ids = self.manager.get_all_build_ids()
- all_builds = []
- for build_id in all_build_ids:
- build_info = self.manager.get_build_info(build_id)
- if build_info is None:
- continue
- # Apply filters
- if (vehicle_id and
- build_info.vehicle_id.lower() != vehicle_id.lower()):
- continue
- if board_id and build_info.board != board_id:
- continue
- if state and build_info.progress.state.name != state:
- continue
- all_builds.append(
- self._build_info_to_output(build_id, build_info)
- )
- # Sort by creation time (newest first)
- all_builds.sort(key=lambda x: x.time_created, reverse=True)
- # Apply pagination
- return all_builds[offset:offset + limit]
- def get_build(self, build_id: str) -> Optional[BuildOut]:
- """
- Get details of a specific build.
- Args:
- build_id: The unique build identifier
- Returns:
- Build details or None if not found
- """
- if not self.manager.build_exists(build_id):
- return None
- build_info = self.manager.get_build_info(build_id)
- if build_info is None:
- return None
- return self._build_info_to_output(build_id, build_info)
- def get_build_logs(
- self,
- build_id: str,
- tail: Optional[int] = None
- ) -> Optional[str]:
- """
- Get build logs for a specific build.
- Args:
- build_id: The unique build identifier
- tail: Optional number of last lines to return
- Returns:
- Build logs as text or None if not found/available
- """
- if not self.manager.build_exists(build_id):
- return None
- log_path = self.manager.get_build_log_path(build_id)
- if not os.path.exists(log_path):
- return None
- try:
- with open(log_path, 'r') as f:
- if tail:
- # Read last N lines
- lines = f.readlines()
- return ''.join(lines[-tail:])
- else:
- return f.read()
- except Exception as e:
- logger.error(f"Error reading log file for build {build_id}: {e}")
- return None
- def get_artifact_path(self, build_id: str) -> Optional[str]:
- """
- Get the path to the build artifact.
- Args:
- build_id: The unique build identifier
- Returns:
- Path to artifact or None if not available
- """
- if not self.manager.build_exists(build_id):
- return None
- build_info = self.manager.get_build_info(build_id)
- if build_info is None:
- return None
- # Only return artifact if build was successful
- if build_info.progress.state.name != "SUCCESS":
- return None
- artifact_path = self.manager.get_build_archive_path(build_id)
- if os.path.exists(artifact_path):
- return artifact_path
- return None
- def _build_info_to_output(
- self,
- build_id: str,
- build_info
- ) -> BuildOut:
- """
- Convert BuildInfo object to BuildOut schema.
- Args:
- build_id: The build identifier
- build_info: BuildInfo object from build_manager
- Returns:
- BuildOut schema object
- """
- # Convert build_manager.BuildProgress to schema BuildProgress
- progress = BuildProgress(
- percent=build_info.progress.percent,
- state=build_info.progress.state.name
- )
- # Convert RemoteInfo
- remote_info = RemoteInfo(
- name=build_info.remote_info.name,
- url=build_info.remote_info.url
- )
- # Map feature defines back to labels for API response
- selected_feature_labels = []
- if build_info.selected_features:
- try:
- # Get build options to map defines back to labels
- with self.repo.get_checkout_lock():
- options = (
- self.ap_src_metadata_fetcher
- .get_build_options_at_commit(
- remote=build_info.remote_info.name,
- commit_ref=build_info.git_hash
- )
- )
- # Create define to label mapping
- define_to_label = {
- option.define: option.label for option in options
- }
- # Map each selected feature define to its label
- for feature_define in build_info.selected_features:
- if feature_define in define_to_label:
- selected_feature_labels.append(
- define_to_label[feature_define]
- )
- else:
- # Fallback: use define if label not found
- logger.warning(
- f"Feature define '{feature_define}' not "
- f"found in build options for build "
- f"{build_id}"
- )
- selected_feature_labels.append(feature_define)
- except Exception as e:
- logger.error(
- f"Error mapping feature defines to labels for "
- f"build {build_id}: {e}"
- )
- # Fallback: use defines as-is
- selected_feature_labels = list(
- build_info.selected_features
- )
- vehicle = self.vehicles_manager.get_vehicle_by_id(
- build_info.vehicle_id
- )
- return BuildOut(
- build_id=build_id,
- vehicle=VehicleBase(
- id=build_info.vehicle_id,
- name=vehicle.name
- ),
- board=BoardBase(
- id=build_info.board,
- name=build_info.board # Board name is same as board ID for now
- ),
- git_hash=build_info.git_hash,
- remote_info=remote_info,
- selected_features=selected_feature_labels,
- progress=progress,
- time_created=build_info.time_created,
- )
- def get_builds_service(request: Request) -> BuildsService:
- """
- Get BuildsService instance with dependencies from app state.
- Args:
- request: FastAPI Request object
- Returns:
- BuildsService instance initialized with app state dependencies
- """
- return BuildsService(
- build_manager=request.app.state.build_manager,
- versions_fetcher=request.app.state.versions_fetcher,
- ap_src_metadata_fetcher=request.app.state.ap_src_metadata_fetcher,
- repo=request.app.state.repo,
- vehicles_manager=request.app.state.vehicles_manager,
- )
|