blob: 52e511a022fa4ac6f771b1a3a5fb6d1f3e622e99 [file] [log] [blame]
# Copyright 2012 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilities for managing the toolchains in the chroot."""
import logging
import os
from pathlib import Path
import subprocess
from typing import List, Optional, TYPE_CHECKING, Union
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import gs
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import toolchain_list
from chromite.utils import key_value_store
if TYPE_CHECKING:
from chromite.lib.parser import package_info
if cros_build_lib.IsInsideChroot():
# Only import portage after we've checked that we're inside the chroot.
# Outside may not have portage, in which case the above may not happen.
# We'll check in main() if the operation needs portage.
# pylint: disable=import-error
import portage
class Error(Exception):
"""Base exception class for this module."""
class UnknownToolchainError(Error):
"""Missing a required package."""
class ToolchainInstallError(Error, cros_build_lib.RunCommandError):
"""An error when installing a toolchain."""
def __init__(
self,
msg: str,
result: cros_build_lib.CompletedProcess,
exception: Optional[Exception] = None,
tc_info: Optional[List["package_info.PackageInfo"]] = None,
):
"""ToolchainInstallError init.
Args:
msg: Error message.
result: The command result.
exception: The original exception.
tc_info: A list of the failed packages' package_info.PackageInfo.
"""
super().__init__(msg, result, exception)
self.failed_toolchain_info = tc_info
def GetHostTuple():
"""Returns compiler tuple for the host system."""
return portage.settings["CHOST"]
# Tree interface functions. They help with retrieving data about the current
# state of the tree:
def GetAllTargets():
"""Get the complete list of targets.
Returns:
The list of cross targets for the current tree
"""
targets = GetToolchainsForBoard("all")
# Remove the host target as that is not a cross-target. Replace with 'host'.
del targets[GetHostTuple()]
return targets
def GetToolchainsForBoard(board, buildroot=constants.SOURCE_ROOT):
"""Get a dictionary mapping toolchain targets to their options for a board.
Args:
board: board name in question (e.g. 'daisy').
buildroot: path to buildroot.
Returns:
The list of toolchain tuples for the given board
"""
overlays = portage_util.FindOverlays(
constants.BOTH_OVERLAYS,
None if board in ("all", "sdk") else board,
buildroot=buildroot,
)
toolchains = toolchain_list.ToolchainList(overlays=overlays)
targets = toolchains.GetMergedToolchainSettings()
if board == "sdk":
targets = FilterToolchains(targets, "sdk", True)
return targets
def GetToolchainTupleForBoard(board, buildroot=constants.SOURCE_ROOT):
"""Gets a tuple for the default and non-default toolchains for a board.
Args:
board: board name in question (e.g. 'daisy').
buildroot: path to buildroot.
Returns:
The tuples of toolchain targets ordered default, non-default for the
board.
"""
toolchains = GetToolchainsForBoard(board, buildroot)
return list(FilterToolchains(toolchains, "default", True)) + list(
FilterToolchains(toolchains, "default", False)
)
def FilterToolchains(targets, key, value):
"""Filter out targets based on their attributes.
Args:
targets: dict of toolchains
key: metadata to examine
value: expected value for metadata
Returns:
dict where all targets whose metadata |key| does not match |value|
have been deleted
"""
return dict((k, v) for k, v in targets.items() if v[key] == value)
def GetSdkURL(for_gsutil=False, suburl=""):
"""Construct a Google Storage URL for accessing SDK related archives
Args:
for_gsutil: Do you want a URL for passing to `gsutil`?
suburl: A url fragment to tack onto the end
Returns:
The fully constructed URL
"""
return gs.GetGsURL(
constants.SDK_GS_BUCKET, for_gsutil=for_gsutil, suburl=suburl
)
def GetArchForTarget(target):
"""Returns the arch used by the given toolchain.
Args:
target: a toolchain.
"""
ret = cros_build_lib.dbg_run(
["crossdev", "--show-target-cfg", target],
capture_output=True,
encoding="utf-8",
)
return key_value_store.LoadData(ret.stdout).get("arch")
def InstallToolchain(sysroot, toolchain=None, force=False, configure=True):
"""Simplified entry point for the toolchain installation process."""
if not cros_build_lib.IsInsideChroot():
# Build the command to run inside the chroot instead.
cmd = [
constants.CHROMITE_BIN_DIR / "install_toolchain",
"--sysroot",
sysroot.path,
]
if toolchain:
cmd.append("--toolchain")
cmd.append(toolchain)
if force:
cmd.append("--force")
if not configure:
cmd.append("--noconfigure")
cros_build_lib.run(cmd, enter_chroot=True)
else:
envvars = portage_util.PortageqEnvvars(["CHOST", "PKGDIR"])
installer = ToolchainInstaller(
force, configure, envvars["CHOST"], envvars["PKGDIR"]
)
installer.Install(sysroot, board_chost=toolchain)
class ToolchainInstaller:
"""Sysroot toolchain installer.
This class installs the toolchain into the given sysroots.
"""
def __init__(self, force: bool, configure: bool, cbuild: str, pkgdir: str):
"""ToolchainInstaller configuration.
|force| and |configure| alter the installer's behavior (details below).
|chost| and |pkgdir| are values fetched from the chroot that determine
which packages to install and how to fetch them.
See:
https://wiki.gentoo.org/wiki/CHOST
Args:
force: Whether to force the installation if already up to date.
configure: Whether to write out the config files in the sysroot
when complete.
cbuild: The CHOST value of the chroot SDK itself.
pkgdir: The PKGDIR value of the chroot; the path at which package
archives are stored.
"""
self.force = force
self.configure = configure
self.cbuild = cbuild
self.pkgdir = pkgdir
def Install(self, sysroot, board_chost=None):
"""Toolchain installation process.
Install most recent glibc version in the sysroot.
If the configure option passed to __init__ is True; the gcc and go
packages are listed in the sysroot's package.provided, and the glibc
version is stored in sysroot's LIBC_VERSION cached field. Otherwise, all
are left untouched.
Note: Must be run inside the chroot. Not currently asserted because
InstallToolchain is the preferred entry point.
Args:
sysroot: sysroot_lib.Sysroot - Must be a valid sysroot,
e.g. use setup_board to initialize one.
board_chost: str|None - The CHOST value to use for the board. If not
explicitly provided as an argument, must be set in the sysroot's
CHOST standard field.
"""
# Determine the toolchain we'll be installing; e.g. i686-pc-linux-gnu,
# armv7a-softfloat-linux-gnueabi.
# Run `qlist -Iv cross-` inside the chroot for more examples.
chost = board_chost or sysroot.GetStandardField("CHOST")
if not chost:
raise ValueError(
"Toolchain not provided or specified in the sysroot."
)
tc_info = ToolchainInfo(chost, self.cbuild)
if not self._NeedsInstalled(sysroot, tc_info):
# Up to date and not forced; nothing to do.
logging.info("Cross-compiler already up to date. Nothing to do.")
return
# Verify we can install the required packages.
if not tc_info.gcc_version or not tc_info.libc_version:
raise UnknownToolchainError(
"Cannot find toolchain to install into board " "root."
)
logging.info("Installing toolchain to the board root: %s", sysroot.path)
self._InstallLibc(sysroot, tc_info)
self._WriteConfigs(sysroot, tc_info)
self._UpdateProvided(sysroot, tc_info)
def _InstallLibc(
self, sysroot: "sysroot_lib.Sysroot", tc_info: "ToolchainInfo"
):
"""Install the libc package to the sysroot.
Args:
sysroot: The sysroot where it's being installed.
tc_info: The toolchain being installed.
"""
if not tc_info.IsCrossCompiler():
# Host and target toolchains match, install standard packages.
cmd = [
"emerge",
"--oneshot",
"--nodeps",
"-k",
"--root",
sysroot.path,
"=%s" % tc_info.libc_cpf,
]
try:
cros_build_lib.sudo_run(cmd)
except cros_build_lib.RunCommandError as e:
raise ToolchainInstallError(
str(e),
e.result,
exception=e,
tc_info=[tc_info.libc_pkg_info],
)
else:
# They do not match, install appropriate cross-toolchain variant
# package. See ToolchainInfo for alternate package name build outs.
libc_path = os.path.join(self.pkgdir, "%s.tbz2" % tc_info.libc_cpf)
if not os.path.exists(libc_path):
# Install libc in chroot if it hasn't already been installed.
cmd = ["emerge", "--nodeps", "-gf", "=%s" % tc_info.libc_cpf]
cros_build_lib.sudo_run(cmd)
try:
self._ExtractLibc(sysroot, tc_info.target, libc_path)
except ToolchainInstallError as e:
e.failed_toolchain_info = [tc_info.libc_pkg_info]
raise e
def _ExtractLibc(
self, sysroot: "sysroot_lib.Sysroot", board_chost: str, libc_path: str
):
"""Extract the libc archive to the sysroot.
Args:
sysroot: The sysroot where it's being installed.
board_chost: The board's CHOST value.
libc_path: The location of the libc archive.
"""
compression = cros_build_lib.CompressionDetectType(libc_path)
compressor = cros_build_lib.FindCompressor(compression)
compressor_algo = Path(compressor).name
if compressor_algo == "pbzip2":
compressor = "%s --ignore-trailing-garbage=1" % compressor
elif compressor_algo.startswith("zstd"):
compressor += " -f"
with osutils.TempDir(sudo_rm=True) as tempdir:
# Extract to the temporary directory.
cmd = ["tar", "-I", compressor, "-xpf", libc_path, "-C", tempdir]
result = cros_build_lib.sudo_run(
cmd,
check=False,
stdout=True,
stderr=subprocess.STDOUT,
)
if result.returncode:
logging.error("failed to extract libc:\n%s", result.stdout)
raise ToolchainInstallError(
f"Error extracting libc: {result.stdout}", result
)
# Sync the files to the sysroot to install. Trailing / on source to
# sync contents instead of the directory itself.
source = os.path.join(tempdir, "usr", board_chost)
cmd = [
"rsync",
"--archive",
"--hard-links",
f"{source}/",
f"{sysroot.path}/",
]
result = cros_build_lib.sudo_run(
cmd,
check=False,
stdout=True,
stderr=subprocess.STDOUT,
)
if result.returncode:
logging.error("failed to install libc:\n%s", result.stdout)
raise ToolchainInstallError(
f"Error installing libc: {result.stdout}", result
)
# Make the debug directory.
debug_dir = os.path.join(sysroot.path, "usr/lib/debug")
osutils.SafeMakedirs(debug_dir, sudo=True)
# Sync the debug files to the debug directory.
source = os.path.join(tempdir, "usr/lib/debug/usr", board_chost)
cmd = [
"rsync",
"--archive",
"--hard-links",
f"{source}/",
f"{debug_dir}/",
]
result = cros_build_lib.sudo_run(
cmd,
check=False,
stdout=True,
stderr=subprocess.STDOUT,
)
if result.returncode:
logging.error(
"failed to copy lib debug info:\n%s", result.stdout
)
raise ToolchainInstallError(
f"Error copying libc debug info: {result.stdout}", result
)
def _NeedsInstalled(self, sysroot, tc_info):
"""Check if the toolchain installation needs to be run."""
return self.force or not tc_info.LibcVersionsMatch(sysroot)
def _WriteConfigs(self, sysroot, tc_info):
"""Write out config updates."""
if self.configure:
sysroot.SetCachedField("LIBC_VERSION", tc_info.libc_version)
sysroot.SetCachedField("LIBCXX_VERSION", tc_info.libcxx_version)
sysroot.SetCachedField("LIBGCC_VERSION", tc_info.libgcc_version)
def _UpdateProvided(self, sysroot, tc_info):
"""Write the package.provided file."""
if self.configure:
content = "\n".join(tc_info.sdk_cpfs)
pkg_provided = os.path.join(
sysroot.path, "etc/portage/profile/package.provided"
)
osutils.WriteFile(pkg_provided, content, makedirs=True, sudo=True)
class ToolchainInfo:
"""Class to manage some of the toolchain related information."""
# Package reference names.
_PKG_GCC = "gcc"
_PKG_LIBC = "glibc"
_PKG_LIBCXX = "libcxx"
_PKG_LIBGCC = "llvm-libunwind"
_PKG_LIBXCRYPT = "libxcrypt"
_PKG_GO = "go"
_PKG_RPCSVC = "rpcsvc"
# Standard group/package names.
_PACKAGES = {
_PKG_GCC: "sys-devel/gcc",
_PKG_LIBC: "sys-libs/glibc",
_PKG_LIBCXX: "sys-libs/libcxx",
_PKG_LIBGCC: "sys-libs/llvm-libunwind",
_PKG_LIBXCRYPT: "sys-libs/libxcrypt",
_PKG_GO: "dev-lang/go",
_PKG_RPCSVC: "net-libs/rpcsvc-proto",
}
def __init__(self, target: str, cbuild: str):
"""ToolchainInfo init.
Args:
target: The target cross-compiler tuple, i.e. the board's CHOST.
cbuild: The CHOST value of the chroot SDK itself.
"""
self.target = target
self.cbuild = cbuild
self._pkgs = {}
@property
def libc_version(self) -> str:
return self._GetVersion(self._PKG_LIBC)
@property
def libc_cpf(self) -> str:
return self._GetCPF(self._PKG_LIBC)
@property
def libc_pkg_info(self) -> "package_info.PackageInfo":
return self._get_pkg(self._PKG_LIBC)
@property
def libcxx_version(self) -> str:
return self._GetVersion(self._PKG_LIBCXX)
@property
def libcxx_cpf(self) -> str:
return self._GetCPF(self._PKG_LIBCXX)
@property
def libcxx_pkg_info(self) -> "package_info.PackageInfo":
return self._get_pkg(self._PKG_LIBCXX)
@property
def libgcc_version(self) -> str:
return self._GetVersion(self._PKG_LIBGCC)
@property
def libgcc_cpf(self) -> str:
return self._GetCPF(self._PKG_LIBGCC)
@property
def libgcc_pkg_info(self) -> "package_info.PackageInfo":
return self._get_pkg(self._PKG_LIBGCC)
@property
def gcc_version(self) -> str:
return self._GetVersion(self._PKG_GCC)
@property
def gcc_cpf(self) -> str:
return self._GetCPF(self._PKG_GCC)
@property
def go_version(self):
return self._GetVersion(self._PKG_GO)
@property
def go_cpf(self):
return self._GetCPF(self._PKG_GO)
@property
def rpcsvc_proto_version(self):
return self._GetVersion(self._PKG_RPCSVC)
@property
def rpcsvc_proto_cpf(self):
return self._GetCPF(self._PKG_RPCSVC)
@property
def sdk_cpfs(self):
"""Get the standard CPFs, i.e. the host, not necessarily the target."""
cpfs = [
"%s-%s" % (self._PACKAGES[self._PKG_GCC], self.gcc_version),
"%s-%s" % (self._PACKAGES[self._PKG_LIBC], self.libc_version),
]
if self.go_version:
cpfs.append(
"%s-%s" % (self._PACKAGES[self._PKG_GO], self.go_version)
)
if self.rpcsvc_proto_version:
cpfs.append(
"%s-%s"
% (self._PACKAGES[self._PKG_RPCSVC], self.rpcsvc_proto_version)
)
return cpfs
def _get_pkg(self, pkg: str) -> Union["package_info.PackageInfo", None]:
"""Return PackageInfo object for the package."""
if pkg not in self._pkgs:
self._pkgs[pkg] = portage_util.PortageqMatch(self._GetCP(pkg))
return self._pkgs[pkg]
def _GetVersion(self, pkg):
"""Get version for the package."""
return getattr(self._get_pkg(pkg), "vr", None)
def _GetCP(self, pkg):
"""Returns the appropriate 'category/package' for the toolchain."""
return (
self._PACKAGES[pkg]
if not self.IsCrossCompiler()
else "cross-%(t)s/%(pkg)s" % {"t": self.target, "pkg": pkg}
)
def _GetCPF(self, pkg):
"""Get the full 'category/package-version-revision'."""
return getattr(self._get_pkg(pkg), "cpvr", None)
def LibcVersionsMatch(self, sysroot: "sysroot_lib.Sysroot"):
"""Check if the two libc package versions match.
Args:
sysroot: The sysroot whose libc version is checked.
Returns:
bool - True iff same version installed in sdk and sysroot.
"""
board_version = sysroot.GetCachedField("LIBC_VERSION")
toolchain_version = self.libc_version
return board_version == toolchain_version and (
board_version or toolchain_version
)
def IsCrossCompiler(self):
"""Whether the sdk and board chost values match."""
return self.cbuild != self.target