blob: 1b74b7b53e8a42292c0b1d983dcf373edd623c5e [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.
"""Sysroot controller."""
import logging
import os
from pathlib import Path
import traceback
from chromite.api import controller
from chromite.api import faux
from chromite.api import validate
from chromite.api.controller import controller_util
from chromite.api.gen.chromiumos import common_pb2
from chromite.api.metrics import deserialize_metrics_log
from chromite.lib import build_target_lib
from chromite.lib import chroot_lib
from chromite.lib import cros_build_lib
from chromite.lib import goma_lib
from chromite.lib import metrics_lib
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import remoteexec_lib
from chromite.lib import sysroot_lib
from chromite.service import sysroot
_ACCEPTED_LICENSES = "@CHROMEOS"
DEFAULT_BACKTRACK = 30
def _GetGomaLogDirectory():
"""Get goma's log directory based on the env variables.
Returns:
a string of a directory name where goma's log may exist, or None if no
potential directories exist.
"""
# TODO(crbug.com/1045001): Replace environment variable with query to
# goma object after goma refactoring allows this.
candidates = [
"GLOG_log_dir",
"GOOGLE_LOG_DIR",
"TEST_TMPDIR",
"TMPDIR",
"TMP",
]
for candidate in candidates:
value = os.environ.get(candidate)
if value and os.path.isdir(value):
return value
# "/tmp" will always exist.
return "/tmp"
def ExampleGetResponse():
"""Give an example response to assemble upstream in caller artifacts."""
uabs = common_pb2.UploadedArtifactsByService
cabs = common_pb2.ArtifactsByService
return uabs.Sysroot(
artifacts=[
uabs.Sysroot.ArtifactPaths(
artifact_type=cabs.Sysroot.ArtifactType.SIMPLE_CHROME_SYSROOT,
paths=[
common_pb2.Path(
path=(
"/tmp/sysroot_chromeos-base_chromeos-chrome.tar.xz"
),
location=common_pb2.Path.OUTSIDE,
)
],
),
uabs.Sysroot.ArtifactPaths(
artifact_type=cabs.Sysroot.ArtifactType.DEBUG_SYMBOLS,
paths=[
common_pb2.Path(
path="/tmp/debug.tgz", location=common_pb2.Path.OUTSIDE
)
],
),
uabs.Sysroot.ArtifactPaths(
artifact_type=cabs.Sysroot.ArtifactType.BREAKPAD_DEBUG_SYMBOLS,
paths=[
common_pb2.Path(
path="/tmp/debug_breakpad.tar.xz",
location=common_pb2.Path.OUTSIDE,
)
],
),
]
)
def GetArtifacts(
in_proto: common_pb2.ArtifactsByService.Sysroot,
chroot: chroot_lib.Chroot,
sysroot_class: sysroot_lib.Sysroot,
build_target: build_target_lib.BuildTarget,
output_dir: str,
) -> list:
"""Builds and copies sysroot artifacts to specified output_dir.
Copies sysroot artifacts to output_dir, returning a list of
(output_dir: str) paths to the desired files.
Args:
in_proto: Proto request defining reqs.
chroot: The chroot class used for these artifacts.
sysroot_class: The sysroot class used for these artifacts.
build_target: The build target used for these artifacts.
output_dir: The path to write artifacts to.
Returns:
A list of dictionary mappings of ArtifactType to list of paths.
"""
def _BundleBreakpadSymbols(chroot, sysroot_class, build_target, output_dir):
# pylint: disable=line-too-long
ignore_breakpad_symbol_generation_expected_files = [
common_pb2.ArtifactsByService.Sysroot.BreakpadSymbolGenerationExpectedFile.Name(
x
)
for x in in_proto.ignore_breakpad_symbol_generation_expected_files
if x
!= common_pb2.ArtifactsByService.Sysroot.BreakpadSymbolGenerationExpectedFile.EXPECTED_FILE_UNSET
and x
in common_pb2.ArtifactsByService.Sysroot.BreakpadSymbolGenerationExpectedFile.values()
]
# pylint: enable=line-too-long
ignore_breakpad_symbol_generation_expected_files = [
x[len("EXPECTED_FILE_") :]
for x in ignore_breakpad_symbol_generation_expected_files
]
return sysroot.BundleBreakpadSymbols(
chroot,
sysroot_class,
build_target,
output_dir,
in_proto.ignore_breakpad_symbol_generation_errors,
ignore_breakpad_symbol_generation_expected_files,
)
generated = []
# pylint: disable=line-too-long
artifact_types = {
in_proto.ArtifactType.SIMPLE_CHROME_SYSROOT: sysroot.CreateSimpleChromeSysroot,
in_proto.ArtifactType.CHROME_EBUILD_ENV: sysroot.CreateChromeEbuildEnv,
in_proto.ArtifactType.BREAKPAD_DEBUG_SYMBOLS: _BundleBreakpadSymbols,
in_proto.ArtifactType.DEBUG_SYMBOLS: sysroot.BundleDebugSymbols,
in_proto.ArtifactType.FUZZER_SYSROOT: sysroot.CreateFuzzerSysroot,
in_proto.ArtifactType.SYSROOT_ARCHIVE: sysroot.ArchiveSysroot,
}
# pylint: enable=line-too-long
for output_artifact in in_proto.output_artifacts:
for artifact_type, func in artifact_types.items():
if artifact_type in output_artifact.artifact_types:
try:
result = func(
chroot, sysroot_class, build_target, output_dir
)
except Exception as e:
generated.append(
{
"type": artifact_type,
"failed": True,
"failure_reason": str(e),
}
)
artifact_name = (
common_pb2.ArtifactsByService.Sysroot.ArtifactType.Name(
artifact_type
)
)
logging.warning(
"%s artifact generation failed with exception %s",
artifact_name,
e,
)
logging.warning("traceback:\n%s", traceback.format_exc())
continue
if result:
generated.append(
{
"paths": [str(result)]
if isinstance(result, (os.PathLike, str))
else result,
"type": artifact_type,
}
)
return generated
@faux.all_empty
@validate.require("build_target.name")
@validate.validation_complete
def Create(input_proto, output_proto, _config):
"""Create or replace a sysroot."""
update_chroot = not input_proto.flags.chroot_current
replace_sysroot = input_proto.flags.replace
use_cq_prebuilts = input_proto.flags.use_cq_prebuilts
build_target = controller_util.ParseBuildTarget(
input_proto.build_target, input_proto.profile
)
package_indexes = [
controller_util.deserialize_package_index_info(x)
for x in input_proto.package_indexes
]
run_configs = sysroot.SetupBoardRunConfig(
force=replace_sysroot,
upgrade_chroot=update_chroot,
package_indexes=package_indexes,
use_cq_prebuilts=use_cq_prebuilts,
backtrack=DEFAULT_BACKTRACK,
)
try:
created = sysroot.Create(
build_target, run_configs, accept_licenses=_ACCEPTED_LICENSES
)
except sysroot.Error as e:
cros_build_lib.Die(e)
output_proto.sysroot.path = created.path
output_proto.sysroot.build_target.name = build_target.name
return controller.RETURN_CODE_SUCCESS
@faux.all_empty
@validate.require("build_target.name", "packages")
@validate.require_each("packages", ["category", "package_name"])
@validate.validation_complete
def GenerateArchive(input_proto, output_proto, _config):
"""Generate a sysroot. Typically used by informational builders."""
build_target_name = input_proto.build_target.name
pkg_list = []
for package in input_proto.packages:
pkg_list.append("%s/%s" % (package.category, package.package_name))
with osutils.TempDir(delete=False) as temp_output_dir:
sysroot_tar_path = sysroot.GenerateArchive(
temp_output_dir, build_target_name, pkg_list
)
# By assigning this Path variable to the tar path, the tar file will be
# copied out to the input_proto's ResultPath location.
output_proto.sysroot_archive.path = sysroot_tar_path
output_proto.sysroot_archive.location = common_pb2.Path.INSIDE
def _MockFailedPackagesResponse(_input_proto, output_proto, _config):
"""Mock error response that populates failed packages."""
fail = output_proto.failed_package_data.add()
fail.name.package_name = "package"
fail.name.category = "category"
fail.name.version = "1.0.0_rc-r1"
fail.log_path.path = (
"/path/to/package:category-1.0.0_rc-r1:20210609-1337.log"
)
fail.log_path.location = common_pb2.Path.INSIDE
fail2 = output_proto.failed_package_data.add()
fail2.name.package_name = "bar"
fail2.name.category = "foo"
fail2.name.version = "3.7-r99"
fail2.log_path.path = "/path/to/foo:bar-3.7-r99:20210609-1620.log"
fail2.log_path.location = common_pb2.Path.INSIDE
@faux.empty_success
@faux.error(_MockFailedPackagesResponse)
@validate.require("sysroot.path", "sysroot.build_target.name")
@validate.exists("sysroot.path")
@validate.validation_complete
def InstallToolchain(input_proto, output_proto, _config):
"""Install the toolchain into a sysroot."""
compile_source = (
input_proto.flags.compile_source or input_proto.flags.toolchain_changed
)
sysroot_path = input_proto.sysroot.path
build_target = controller_util.ParseBuildTarget(
input_proto.sysroot.build_target
)
target_sysroot = sysroot_lib.Sysroot(sysroot_path)
run_configs = sysroot.SetupBoardRunConfig(usepkg=not compile_source)
_LogBinhost(build_target.name)
try:
sysroot.InstallToolchain(build_target, target_sysroot, run_configs)
except sysroot_lib.ToolchainInstallError as e:
controller_util.retrieve_package_log_paths(
e.failed_toolchain_info, output_proto, target_sysroot
)
return controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE
return controller.RETURN_CODE_SUCCESS
@faux.empty_success
@faux.error(_MockFailedPackagesResponse)
@validate.require("sysroot.build_target.name")
@validate.exists("sysroot.path")
@validate.require_each("packages", ["category", "package_name"])
@validate.require_each("use_flags", ["flag"])
@validate.validation_complete
@metrics_lib.collect_metrics
def InstallPackages(input_proto, output_proto, _config):
"""Install packages into a sysroot, building as necessary and permitted."""
compile_source = (
input_proto.flags.compile_source or input_proto.flags.toolchain_changed
)
use_remoteexec = bool(
input_proto.remoteexec_config.reproxy_cfg_file
and input_proto.remoteexec_config.reclient_dir
)
# Testing if Goma will support unknown compilers now.
use_goma = input_proto.flags.use_goma and not use_remoteexec
target_sysroot = sysroot_lib.Sysroot(input_proto.sysroot.path)
build_target = controller_util.ParseBuildTarget(
input_proto.sysroot.build_target
)
# Get the package atom for each specified package. The field is optional, so
# error only when we cannot parse an atom for each of the given packages.
packages = [
controller_util.deserialize_package_info(x).atom
for x in input_proto.packages
]
package_indexes = [
controller_util.deserialize_package_index_info(x)
for x in input_proto.package_indexes
]
# Calculate which packages would have been merged, but don't install
# anything.
dryrun = input_proto.flags.dryrun
# Allow cros workon packages to build from the unstable ebuilds.
workon = input_proto.flags.workon
# Use Bazel to build packages.
bazel = input_proto.flags.bazel
if not target_sysroot.IsToolchainInstalled():
cros_build_lib.Die("Toolchain must first be installed.")
_LogBinhost(build_target.name)
use_flags = [u.flag for u in input_proto.use_flags]
build_packages_config = sysroot.BuildPackagesRunConfig(
use_any_chrome=False,
usepkg=not compile_source,
install_debug_symbols=True,
packages=packages,
package_indexes=package_indexes,
use_flags=use_flags,
use_goma=use_goma,
use_remoteexec=use_remoteexec,
incremental_build=False,
dryrun=dryrun,
backtrack=DEFAULT_BACKTRACK,
workon=workon,
bazel=bazel,
)
try:
sysroot.BuildPackages(
build_target, target_sysroot, build_packages_config
)
except sysroot_lib.PackageInstallError as e:
if not e.failed_packages:
# No packages to report, so just exit with an error code.
return controller.RETURN_CODE_COMPLETED_UNSUCCESSFULLY
controller_util.retrieve_package_log_paths(
e.failed_packages, output_proto, target_sysroot
)
return controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE
finally:
# Copy goma logs to specified directory if there is a goma_config and
# it contains a log_dir to store artifacts.
if input_proto.goma_config.log_dir.dir:
log_source_dir = _GetGomaLogDirectory()
archiver = goma_lib.LogsArchiver(
log_source_dir,
dest_dir=input_proto.goma_config.log_dir.dir,
stats_file=input_proto.goma_config.stats_file,
counterz_file=input_proto.goma_config.counterz_file,
)
archiver_tuple = archiver.Archive()
if archiver_tuple.stats_file:
output_proto.goma_artifacts.stats_file = (
archiver_tuple.stats_file
)
if archiver_tuple.counterz_file:
output_proto.goma_artifacts.counterz_file = (
archiver_tuple.counterz_file
)
output_proto.goma_artifacts.log_files[:] = archiver_tuple.log_files
if input_proto.remoteexec_config.log_dir.dir:
archiver = remoteexec_lib.LogsArchiver(
dest_dir=Path(input_proto.remoteexec_config.log_dir.dir),
)
archived_logs = archiver.archive()
output_proto.remoteexec_artifacts.log_files[:] = [
str(x) for x in archived_logs
]
# Return without populating the response if it is a dryrun.
if dryrun:
return controller.RETURN_CODE_SUCCESS
# Read metric events log and pipe them into output_proto.events.
deserialize_metrics_log(output_proto.events, prefix=build_target.name)
def _LogBinhost(board):
"""Log the portage binhost for the given board."""
binhost = portage_util.PortageqEnvvar(
"PORTAGE_BINHOST", board=board, allow_undefined=True
)
if not binhost:
logging.warning("Portage Binhost not found.")
else:
logging.info("Portage Binhost: %s", binhost)