blob: b60b45f9b2f03043a0700cc6a19bfdf13491b0aa [file] [log] [blame]
# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Choose the profile for a board that has been or is being setup."""
import functools
import logging
import os
from typing import Optional
from chromite.lib import build_target_lib
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import sysroot_lib
# Default value constants.
_DEFAULT_PROFILE = "base"
def PathPrefixDecorator(f):
"""Add a prefix to the path or paths returned by the decorated function.
Will not prepend the prefix if the path already starts with the prefix, so
the decorator may be applied to functions that have mixed sources that may
or may not already have applied them. This is especially useful for allowing
tests and CLI args a little more leniency in how paths are provided.
"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
result = f(*args, **kwargs)
prefix = PathPrefixDecorator.prefix
if not prefix or not result:
# Nothing to do.
return result
# Convert Path objects to str.
if isinstance(prefix, os.PathLike):
prefix = str(prefix)
if not isinstance(result, str):
# Transform each path in the collection.
new_result = []
for path in result:
prefixed_path = os.path.join(prefix, path.lstrip(os.sep))
new_result.append(
path if path.startswith(prefix) else prefixed_path
)
return new_result
elif not result.startswith(prefix):
# Add the prefix.
return os.path.join(prefix, result.lstrip(os.sep))
# An already prefixed path.
return result
return wrapper
PathPrefixDecorator.prefix = None
class Error(Exception):
"""Base error for custom exceptions in this script."""
class InvalidArgumentsError(Error):
"""Invalid arguments."""
class MakeProfileIsNotLinkError(Error):
"""The make profile exists but is not a link."""
class ProfileDirectoryNotFoundError(Error):
"""Unable to find the profile directory."""
def ChooseProfile(board, profile) -> None:
"""Make the link to choose the profile, print relevant warnings.
Args:
board: Board - the board being used.
profile: Profile - the profile being used.
Raises:
OSError when the board's make_profile path exists and is not a link.
"""
if not os.path.isfile(os.path.join(profile.directory, "parent")):
logging.warning(
"Portage profile directory %s has no 'parent' file. "
"This likely means your profile directory is invalid and "
"`cros build-packages` will fail.",
profile.directory,
)
current_profile = None
if os.path.exists(board.make_profile):
# Only try to read if it exists; we only want it to raise an error when
# the path exists and is not a link.
try:
current_profile = os.readlink(board.make_profile)
except OSError:
raise MakeProfileIsNotLinkError(
"%s is not a link." % board.make_profile
)
if current_profile == profile.directory:
# The existing link is what we were going to make, so nothing to do.
return
elif current_profile is not None:
# It exists and is changing, emit warning.
fmt = {"board": board.board_variant, "profile": profile.name}
msg = (
"You are switching profiles for a board that is already setup. "
"This can cause trouble for Portage. If you experience problems "
"with `cros build-packages` you may need to run:\n"
"\t'setup_board --board %(board)s --force --profile %(profile)s'\n"
"\nAlternatively, you can correct the dependency graph by using "
"'emerge-%(board)s -c' or 'emerge-%(board)s -C <ebuild>'."
)
logging.warning(msg, fmt)
# Make the symlink, overwrites existing link if one already exists.
osutils.SafeSymlink(profile.directory, board.make_profile, sudo=True)
# Update the profile override value.
if profile.override:
board.profile_override = profile.override
class Profile:
"""Simple data container class for the profile data."""
def __init__(self, name, directory, override) -> None:
self.name = name
self._directory = directory
self.override = override
@property
@PathPrefixDecorator
def directory(self):
return self._directory
def _GetProfile(opts, board):
"""Get the profile list."""
# Determine the override value - which profile is being selected.
override = opts.profile if opts.profile else board.profile_override
profile = _DEFAULT_PROFILE
profile_directory = None
if override and os.path.exists(override):
profile_directory = os.path.abspath(override)
profile = os.path.basename(profile_directory)
elif override:
profile = override
if profile_directory is None:
# Build profile directories in reverse order, so we can search from most
# to least specific.
profile_dirs = [
"%s/profiles/%s" % (overlay, profile)
for overlay in reversed(board.overlays)
]
for profile_dir in profile_dirs:
if os.path.isdir(profile_dir):
profile_directory = profile_dir
break
else:
searched = ", ".join(profile_dirs)
raise ProfileDirectoryNotFoundError(
"Profile directory not found, searched in (%s)." % searched
)
return Profile(profile, profile_directory, override)
class Board:
"""Manage the board arguments and configs."""
# Files located on the board.
MAKE_PROFILE = "%(board_root)s/etc/portage/make.profile"
def __init__(
self,
board: Optional[str] = None,
variant: Optional[str] = None,
board_root: Optional[str] = None,
) -> None:
"""Board constructor.
board [+ variant] is given preference when both board and board_root are
provided.
Preconditions:
Either board and build_root are not None, or board_root is not None.
With board + build_root we can construct the board root.
With the board root we can have the board directory.
Args:
board: The board name.
variant: The variant name. TODO: Deprecate?
board_root: The boards fully qualified build directory path.
"""
if not board and not board_root:
# Enforce preconditions.
raise InvalidArgumentsError(
"Either board or board_root must be provided."
)
elif board:
# The board and variant can be specified separately, or can both be
# contained in the board name, separated by an underscore.
board_split = board.split("_")
variant_default = variant
self._board_root = None
else:
self._board_root = os.path.normpath(board_root)
board_split = os.path.basename(self._board_root).split("_")
variant_default = None
self.board = board_split.pop(0)
self.variant = board_split.pop(0) if board_split else variant_default
if self.variant:
self.board_variant = "%s_%s" % (self.board, self.variant)
else:
self.board_variant = self.board
self.make_profile = self.MAKE_PROFILE % {"board_root": self.root}
# This must come after the arguments required to build each variant of
# the build root have been processed.
self._sysroot_config = sysroot_lib.Sysroot(self.root)
@property
@PathPrefixDecorator
def root(self):
if self._board_root:
return self._board_root
return build_target_lib.get_default_sysroot_path(self.board_variant)
@property
@PathPrefixDecorator
def overlays(self):
return self._sysroot_config.GetStandardField(
sysroot_lib.STANDARD_FIELD_BOARD_OVERLAY
).split()
@property
def profile_override(self):
return self._sysroot_config.GetCachedField("PROFILE_OVERRIDE")
@profile_override.setter
def profile_override(self, value) -> None:
self._sysroot_config.SetCachedField("PROFILE_OVERRIDE", value)
def _GetBoard(opts):
"""Factory method to build a Board from the parsed CLI arguments."""
return Board(
board=opts.board, variant=opts.variant, board_root=opts.board_root
)
def GetParser():
"""ArgumentParser builder and argument definitions."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument(
"-b",
"--board",
default=os.environ.get("DEFAULT_BOARD"),
help="The name of the board to set up.",
)
parser.add_argument(
"-r",
"--board-root",
type="str_path",
help="Board root where the profile should be created.",
)
parser.add_argument(
"-p", "--profile", help="The portage configuration profile to use."
)
parser.add_argument("--variant", help="Board variant.")
group = parser.add_argument_group("Advanced options")
group.add_argument(
"--filesystem-prefix",
type="str_path",
help="Force filesystem accesses to be prefixed by the given path.",
)
return parser
def ParseArgs(argv):
"""Parse and validate the arguments."""
parser = GetParser()
opts = parser.parse_args(argv)
# See Board.__init__ Preconditions.
board_valid = opts.board is not None
board_root_valid = opts.board_root and os.path.exists(opts.board_root)
if not board_valid and not board_root_valid:
parser.error("Either board or board_root must be provided.")
PathPrefixDecorator.prefix = opts.filesystem_prefix
del opts.filesystem_prefix
opts.Freeze()
return opts
def main(argv) -> None:
# Parse arguments.
opts = ParseArgs(argv)
# Build and validate the board and profile.
board = _GetBoard(opts)
if not os.path.exists(board.root):
cros_build_lib.Die(
"The board has not been setup, please run setup_board first."
)
try:
profile = _GetProfile(opts, board)
except ProfileDirectoryNotFoundError as e:
cros_build_lib.Die(e)
# Change the profile to the selected.
logging.info("Selecting profile: %s for %s", profile.directory, board.root)
try:
ChooseProfile(board, profile)
except MakeProfileIsNotLinkError as e:
cros_build_lib.Die(e)