blob: a95cd3ca4317afdddc505a664f770ce47e0d1fc9 [file] [log] [blame]
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Build target class and related functionality."""
from __future__ import annotations
import functools
import json
import logging
import os
from pathlib import Path
import re
from typing import Any, Iterator, Optional
from chromite.api.gen.chromiumos import common_pb2
from chromite.lib import build_query
from chromite.lib import constants
from chromite.lib import portage_util
class Error(Exception):
"""Base module error class."""
class InvalidSerializedBuildTarget(Error):
"""Given an invalid serialization of a build target."""
class BuildTarget:
"""Class to handle the build target information."""
def __init__(
self,
name: Optional[str],
profile: str = "base",
build_root: Optional[str] = None,
public: Optional[bool] = None,
) -> None:
"""Build Target init.
Args:
name: The full name of the target.
profile: The profile name.
build_root: The path to the buildroot.
public: If true, simulate a public checkout. By default, enable
for boards without a private overlay.
"""
self._name = name or constants.CHROOT_BUILDER_BOARD
self.profile = profile
self._public = public
if build_root:
self.root = os.path.normpath(build_root)
else:
self.root = get_default_sysroot_path(self.name)
self.broot = Path(self.root) / "build" / "broot"
def __eq__(self, other: Any) -> bool:
if self.__class__ is other.__class__:
return (
self.name == other.name
and self.profile == other.profile
and self.root == other.root
and self._public == other._public
)
return NotImplemented
def __hash__(self) -> int:
return hash((self.name, self.profile, self._public, self.root))
def __str__(self) -> str:
return f"{self.name or 'amd64-host'}:{self.profile}"
@property
def name(self) -> Optional[str]:
"""Build target name, a.k.a. board."""
return self._name
@functools.cached_property
def board(self) -> build_query.Board:
"""The build_query.Board corresponding to this target."""
return build_query.Board.get(self.name, profile=self.profile)
@property
def public(self) -> bool:
"""True if this build should be done from public sources only."""
if self._public is not None:
return self._public
return not self.board.private_overlay
def full_path(self, *args):
"""Turn a sysroot-relative path into an absolute path."""
return os.path.join(self.root, *[part.lstrip(os.sep) for part in args])
def get_command(self, base_command: str) -> str:
"""Get the build target's variant of the given base command.
We create wrappers for many scripts that handle the build target's
arguments. Build the target-specific variant for such a command.
e.g. emerge -> emerge-eve.
TODO: Add optional validation the command exists.
Args:
base_command: The wrapped command.
Returns:
The build target's command wrapper.
"""
if self.is_host():
return base_command
return "%s-%s" % (base_command, self.name)
def find_overlays(
self, source_root: Path = constants.SOURCE_ROOT
) -> Iterator[Path]:
"""Find the overlays for this build target.
Args:
source_root: If provided, use an alternative SOURCE_ROOT (useful for
testing).
Yields:
Paths to the overlays.
"""
overlay_type = (
constants.PUBLIC_OVERLAYS
if self.public
else constants.BOTH_OVERLAYS
)
for overlay in portage_util.FindOverlays(
overlay_type, self.name, buildroot=source_root
):
yield Path(overlay)
def is_host(self) -> bool:
"""Check if the build target refers to the host."""
return self.name.endswith("-host")
def to_proto(self) -> common_pb2.BuildTarget:
"""Convert to a common_pb2.BuildTarget."""
return common_pb2.BuildTarget(
name=self.name,
profile=common_pb2.Profile(name=self.profile),
)
def to_json(self) -> str:
"""Convert to a json dict."""
return json.dumps(
{
"name": self.name,
"profile": self.profile,
"root": str(self.root),
"public": self._public,
}
)
@classmethod
def from_json(cls, serialized: str) -> BuildTarget:
"""Create an instance from a json string."""
try:
data = json.loads(serialized)
except json.JSONDecodeError as e:
logging.exception("Unable to parse the build target.")
raise InvalidSerializedBuildTarget(
"Unable to parse the build target."
) from e
try:
return cls(
name=data["name"],
profile=data.get("profile", "base"),
build_root=data.get("root"),
public=data.get("public"),
)
except TypeError as e:
msg = (
"Unable to create a build target from the given "
f"serialization: {serialized}"
)
logging.exception(msg)
raise InvalidSerializedBuildTarget(msg) from e
def get_default_sysroot_path(build_target_name: Optional[str] = None) -> str:
"""Get the default sysroot location or / if |build_target_name| is None."""
if build_target_name is None:
return "/"
return os.path.join("/build", build_target_name)
def get_sdk_sysroot_path() -> str:
"""Get the SDK's sysroot path.
Convenience/clarification wrapper for get_default_sysroot_path for use when
explicitly fetching the SDK's sysroot path.
"""
return get_default_sysroot_path()
def is_valid_name(build_target_name):
"""Validate |build_target_name| is a valid name."""
return bool(re.match(r"^[a-zA-Z0-9-_]+$", build_target_name))