blob: f05b186c280fe10b91762393c4941641592891b9 [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Wrapper for board specific portage commands.
This script is meant to be used in generated wrapper scripts, not used directly.
"""
import logging
import os
from pathlib import Path
from typing import Iterable, List, Optional
from chromite.third_party.opentelemetry import trace
from chromite.lib import build_query
from chromite.lib import chromite_config
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import sysroot_lib
from chromite.lib.parser import package_info
from chromite.utils import telemetry
tracer = trace.get_tracer(__name__)
def get_parser() -> commandline.ArgumentParser:
"""Build the argument parser."""
parser = commandline.ArgumentParser(description=__doc__)
parser.add_argument(
"--build-target",
required=True,
help="The build target name.",
)
parser.add_argument(
"--sysroot",
type="path",
required=True,
help="The path to the sysroot for which the command will be created.",
)
parser.add_argument(
"--chost",
required=True,
help="The CHOST value for the sysroot.",
)
parser.add_argument(
"command",
nargs="+",
help="The command to run.",
)
return parser
def parse_arguments(argv: List[str]) -> commandline.ArgumentNamespace:
"""Parse and validate arguments."""
parser = get_parser()
opts = parser.parse_args(argv)
opts.Freeze()
return opts
@tracer.start_as_current_span("portage_cmd_wrapper.parse_pkgs")
def parse_pkgs(command: List[str], build_target_name: str) -> Iterable[str]:
"""Parse packages from a command."""
span = trace.get_current_span()
span.update_name(f"portage_cmd_wrapper.{command[0]}.parse_pkgs")
pkg_fragments = set()
for arg in command[1:]:
if arg.startswith("-"):
# Skip --arguments.
continue
try:
pkg = package_info.parse(arg)
except ValueError:
# e.g. /some/path.
continue
if pkg.cpvr or pkg.atom:
# We have at least an atom, that's good enough.
yield pkg.cpvr or pkg.atom
else:
# The fragment gets parsed as the package name.
pkg_fragments.add(pkg.package)
if pkg_fragments:
ebuilds = build_query.Query(build_query.Ebuild, board=build_target_name)
for ebuild in ebuilds:
if ebuild.package_info.package in pkg_fragments:
yield ebuild.package_info.cpvr
# TODO: Find a better name and a reusable location for this.
@tracer.start_as_current_span("portage_cmd_wrapper.sudo_run_cmd")
def sudo_run_cmd_with_failed_pkg_parsing(command, extra_env):
"""Wrapper for sudo_run that adds CROS_METRICS_DIR usage."""
span = trace.get_current_span()
span.update_name(f"portage_cmd_wrapper.{command[0]}.sudo_run_cmd")
extra_env = extra_env.copy()
with osutils.TempDir() as tempdir:
extra_env[constants.CROS_METRICS_DIR_ENVVAR] = tempdir
try:
return cros_build_lib.sudo_run(
command,
preserve_env=True,
extra_env=extra_env,
)
except cros_build_lib.RunCommandError as e:
raise sysroot_lib.PackageInstallError(
"Merging board packages failed",
e.result,
exception=e,
packages=portage_util.ParseDieHookStatusFile(tempdir),
)
@tracer.start_as_current_span("portage_cmd_wrapper.execute_cmd")
def execute_cmd(opts: commandline.ArgumentNamespace) -> int:
extra_env = {
"CHOST": opts.chost,
"PORTAGE_CONFIGROOT": opts.sysroot,
"SYSROOT": opts.sysroot,
"ROOT": opts.sysroot,
"PORTAGE_USERNAME": (
os.environ.get("PORTAGE_USERNAME") or Path("~").expanduser().name
),
}
# If we try to use sudo when the sandbox is active, we get ugly warnings
# that just confuse developers.
if os.environ.get("SANDBOX_ON") == "1":
os.environ["SANDBOX_ON"] = "0"
os.environ.pop("LD_PRELOAD", None)
pkgs = []
if opts.command[0] == "emerge":
pkgs = list(parse_pkgs(opts.command, opts.build_target))
span = trace.get_current_span()
span.update_name(f"portage_cmd_wrapper.{opts.command[0]}.execute_cmd")
span.set_attributes(
{
"executable": opts.command[0],
"command": opts.command,
"build_target": opts.build_target,
"extra_env": [f"{k}={v}" for k, v in extra_env.items()],
"packages": pkgs,
}
)
return sudo_run_cmd_with_failed_pkg_parsing(
opts.command, extra_env
).returncode
@tracer.start_as_current_span("portage_cmd_wrapper.main")
def main(argv: Optional[List[str]]) -> Optional[int]:
"""Main."""
commandline.RunInsideChroot()
opts = parse_arguments(argv)
if opts.command[0] != "equery":
# There's a *lot* more equery calls than any other command. Specifically
# lots of parallel executions in cros clean-outdated-packages.
# Nothing wrong with those usages, but it's pretty noisy for our data
# given it's not one we're currently concerned about. So for now, just
# skip all equery invocations.
chromite_config.initialize()
telemetry.initialize(chromite_config.TELEMETRY_CONFIG, debug=opts.debug)
span = trace.get_current_span()
span.update_name(f"portage_cmd_wrapper.{opts.command[0]}.main")
try:
return execute_cmd(opts)
except cros_build_lib.RunCommandError as e:
logging.error(e)
logging.error("Error running %s.", opts.command[0])
logging.error(
"Full command: %s", cros_build_lib.CmdToStr(e.result.args)
)
# sysroot_lib.PackageInstallError is a subclass of RunCommandError.
if hasattr(e, "failed_packages"):
logging.error("Failed Packages: %s", " ".join(e.failed_packages))
return e.result.returncode