blob: f67f550c511f3f441a1f13b517c0cccde102d3ea [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.
"""The cros chrome-sdk command for the simple chrome workflow."""
import argparse
import collections
import contextlib
import datetime
import glob
import json
import logging
import os
from pathlib import Path
import re
import stat
import textwrap
from chromite.third_party.gn_helpers import gn_helpers
from chromite.cli import command
from chromite.lib import cache
from chromite.lib import chrome_lkgm
from chromite.lib import chromite_config
from chromite.lib import cipd
from chromite.lib import config_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_sdk_lib
from chromite.lib import gclient
from chromite.lib import gs
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import path_util
from chromite.lib import portage_util
from chromite.lib import qemu
from chromite.utils import memoize
from chromite.utils import pformat
COMMAND_NAME = "chrome-sdk"
CUSTOM_VERSION = "custom"
def Log(*args, **kwargs) -> None:
"""Conditional logging.
Args:
silent: If set to True, then logs with level DEBUG. logs with level INFO
otherwise. Defaults to False.
"""
silent = kwargs.pop("silent", False)
level = logging.DEBUG if silent else logging.INFO
logging.log(level, *args, **kwargs)
class MissingSDK(Exception):
"""Error thrown when we cannot find an SDK."""
def _ConstructDashboardURL(self, config, board):
"""Returns link to the given board's dashboard."""
if config.endswith(f"-{config_lib.CONFIG_TYPE_RELEASE}"):
return "http://go/rubik-release-?f=build_target:in:%s" % board
elif config.endswith(f"-{config_lib.CONFIG_TYPE_PUBLIC}"):
return (
"http://go/cros-ci-builds-/public/?f=build_target:in:%s" % board
)
else:
return ""
def __init__(self, config, board, version=None) -> None:
msg = "Cannot find SDK for %s" % config
if version is not None:
msg += " with version %s" % version
msg += " from its builder"
dashboard_url = self._ConstructDashboardURL(config, board)
if dashboard_url != "":
msg += f": {dashboard_url}"
Exception.__init__(self, msg)
class SDKFetcher:
"""Functionality for fetching an SDK environment.
For the version of ChromeOS specified, the class downloads and caches
SDK components.
"""
SDK_BOARD_ENV = "%SDK_BOARD"
SDK_PATH_ENV = "%SDK_PATH"
SDK_VERSION_ENV = "%SDK_VERSION"
SDKContext = collections.namedtuple(
"SDKContext", ["version", "target_tc", "key_map"]
)
CIPD_CACHE = "cipd"
TARBALL_CACHE = "tarballs"
MISC_CACHE = "misc"
SYMLINK_CACHE = "symlinks"
ARM32_TUPLE = "armv7a-cros-linux-gnueabihf"
ARM64_TUPLE = "aarch64-cros-linux-gnu"
TARGET_TOOLCHAIN_KEY = "target_toolchain"
NACL_ARM32_TOOLCHAIN_KEY = "arm32_toolchain_for_nacl_helper"
SQUASHFS_CIPD_PATH = "infra/3pp/tools/squashfs/linux-amd64"
SQUASHFS_CIPD_VER = "97pLXFMaDo0YFKrWyL_wfrZHyTNXM9iO6T_uRHkMkrQC"
ZSTD_CIPD_PATH = "infra/3pp/static_libs/libzstd/linux-amd64"
ZSTD_CIPD_VER = "znTYHKuCvEQXnJ16VeZl1-TvGRwrCUBoFDuKLIB_5IIC"
CANARIES_PER_DAY = 3
DAYS_TO_CONSIDER = 14
VERSIONS_TO_CONSIDER = DAYS_TO_CONSIDER * CANARIES_PER_DAY
def __init__(
self,
cache_dir,
board,
clear_cache=False,
chrome_src=None,
sdk_path=None,
toolchain_path=None,
silent=False,
use_external_config=None,
fallback_versions=VERSIONS_TO_CONSIDER,
is_lacros=False,
) -> None:
"""Initialize the class.
Args:
cache_dir: The toplevel cache dir to use.
board: The board to manage the SDK for.
clear_cache: Clears the sdk cache during __init__.
chrome_src: The location of the chrome checkout. If unspecified, the
cwd is presumed to be within a chrome checkout.
sdk_path: The path (whether a local directory or a gs:// path) to
fetch SDK components from.
toolchain_path: The path (whether a local directory or a gs:// path)
to fetch toolchain components from.
silent: If set, the fetcher prints less output.
use_external_config: When identifying the configuration for a board,
force usage of the external configuration if both external and
internal are available.
fallback_versions: The number of versions to consider.
is_lacros: whether it's Lacros-Chrome build or not.
"""
self.cache_base = os.path.join(cache_dir, COMMAND_NAME)
if clear_cache:
logging.warning("Clearing the SDK cache.")
osutils.RmDir(self.cache_base, ignore_missing=True)
self.tarball_cache = cache.TarballCache(
os.path.join(self.cache_base, self.TARBALL_CACHE)
)
self.misc_cache = cache.DiskCache(
os.path.join(self.cache_base, self.MISC_CACHE)
)
self.symlink_cache = cache.DiskCache(
os.path.join(self.cache_base, self.SYMLINK_CACHE)
)
self.cipd_cache = cache.DiskCache(
os.path.join(self.cache_base, self.CIPD_CACHE)
)
self.board = board
self.clear_cache = clear_cache
self.chrome_src = chrome_src
self.sdk_path = sdk_path
self.toolchain_path = toolchain_path
self.fallback_versions = fallback_versions
self.silent = silent
self.is_lacros = is_lacros
self.gs_ctx = gs.GSContext(cache_dir=cache_dir, init_boto=False)
if self.sdk_path is None:
self.sdk_path = os.environ.get(self.SDK_PATH_ENV)
if self.toolchain_path is None:
self.toolchain_path = "gs://%s" % constants.SDK_GS_BUCKET
if use_external_config or not self._HasInternalConfig():
self.config_name = f"{board}-{config_lib.CONFIG_TYPE_PUBLIC}"
self.gs_base = f"gs://chromiumos-image-archive/{self.config_name}"
else:
self.config_name = f"{board}-{config_lib.CONFIG_TYPE_RELEASE}"
self.gs_base = f"gs://chromeos-image-archive/{self.config_name}"
self.version_finder = chrome_lkgm.ChromeOSVersionFinder(
cache_dir,
self.board,
self.fallback_versions,
self.chrome_src,
use_external_config,
)
def _HasInternalConfig(self):
"""Determines if the SDK we need is provided by an internal builder.
A given board can have a public and/or an internal builder that
publishes its Simple Chrome SDK. e.g. "amd64-generic" only has a public
builder, "scarlet" only has an internal builder, "octopus" has both. So
if we haven't explicitly passed "--use-external-config", we need to
figure out if we want to use a public or internal builder.
The configs inside gs://chromeos-build-release-console are the proper
source of truth for what boards have public or internal builders.
However, the ACLs on that bucket make it difficult for some folk to
inspect it. So we instead simply assume that everything but the
"*-generic" boards have internal configs.
TODO(b/241964080): Inspect gs://chromeos-build-release-console here
instead if/when ACLs on that bucket are opened up.
Returns:
True if there's an internal builder available that publishes SDKs
for the board.
"""
if "generic" in self.board:
return False
return True
def _InstallFromCipd(
self, cipd_path: str, version: str, subdir: str = "bin"
) -> str:
"""Install a package from cipd."""
key = (cipd_path.replace("/", "-"), version)
with self.cipd_cache.Lookup(key) as ref:
if not ref.Exists(lock=True):
Log("SDK: Getting %s", cipd_path)
path = cipd.InstallPackage(
cipd.GetCIPDFromCache(),
cipd_path,
version,
self.cipd_cache.staging_dir,
)
ref.SetDefault(os.path.join(path, subdir))
return ref.path
def _InstallZstdFromCipd(self) -> None:
"""Install zstd from cipd if the system doesn't have it."""
if osutils.Which("zstd"):
return
path = self._InstallFromCipd(self.ZSTD_CIPD_PATH, self.ZSTD_CIPD_VER)
os.environ["PATH"] += f":{path}"
def _InstallSquashfsFromCipd(self) -> None:
"""Install mksquahsfs from cipd if the system doesn't have it."""
if osutils.Which("mksquashfs"):
return
path = self._InstallFromCipd(
self.SQUASHFS_CIPD_PATH,
self.SQUASHFS_CIPD_VER,
subdir="squashfs-tools",
)
os.environ["PATH"] += f":{path}"
def _UpdateTarball(self, key, url, ref) -> None:
"""Worker function to fetch a tarball.
Args:
key: Key for the tarball's entry in the cache.
url: GS URL to fetch the tarball from.
ref: cache.CacheReference of the tarball's entry in the cache.
"""
with osutils.TempDir(
base_dir=self.tarball_cache.staging_dir
) as tempdir:
local_path = os.path.join(tempdir, os.path.basename(url))
Log("SDK: Fetching %s", url, silent=self.silent)
try:
self.gs_ctx.Copy(url, tempdir, debug_level=logging.DEBUG)
ref.SetDefault(local_path, lock=True)
except gs.GSNoSuchKey:
if key == constants.TEST_IMAGE_TAR:
logging.warning(
"No VM available for board %s. Please try a different "
"board, e.g. amd64-generic.",
self.board,
)
else:
raise
def _UpdateCacheSymlink(self, ref, source_path) -> None:
"""Adds a symlink to the cache pointing at the given source.
Args:
ref: cache.CacheReference of the link to be created.
source_path: Absolute path that the symlink will point to.
"""
with osutils.TempDir(
base_dir=self.symlink_cache.staging_dir
) as tempdir:
# Make the symlink relative so the cache can be moved to different
# locations/machines without breaking the link.
rel_source_path = os.path.relpath(
source_path, start=os.path.dirname(ref.path)
)
link_name_path = os.path.join(tempdir, "tmp-link")
osutils.SafeSymlink(rel_source_path, link_name_path)
ref.SetDefault(link_name_path, lock=True)
def _GetBuildReport(self, version):
"""Return build_report.json (as a dict) for a given version."""
raw_json = None
version_base = self._GetVersionGSBase(version)
report_path = os.path.join(version_base, constants.BUILD_REPORT_JSON)
with self.misc_cache.Lookup(
self._GetTarballCacheKey(constants.BUILD_REPORT_JSON, report_path)
) as ref:
if ref.Exists(lock=True):
raw_json = osutils.ReadFile(ref.path)
else:
try:
raw_json = self.gs_ctx.Cat(
report_path,
retries=0,
debug_level=logging.DEBUG,
encoding="utf-8",
)
except (gs.GSNoSuchKey, gs.GSCommandError):
# Make this fatal once we stop using metadata.json from old
# cbuildbot builders. (GSCommandError gets thrown instead of
# GSNoSuchKey for anonymous users, e.g. Chrome's public
# bots.)
return
ref.AssignText(raw_json)
return json.loads(raw_json)
def _GetMetadata(self, version):
"""Return metadata (in the form of a dict) for a given version."""
raw_json = None
version_base = self._GetVersionGSBase(version)
metadata_path = os.path.join(version_base, constants.METADATA_JSON)
partial_metadata_path = os.path.join(
version_base, constants.PARTIAL_METADATA_JSON
)
with self.misc_cache.Lookup(
self._GetTarballCacheKey(
constants.PARTIAL_METADATA_JSON, partial_metadata_path
)
) as ref:
if ref.Exists(lock=True):
raw_json = osutils.ReadFile(ref.path)
else:
try:
raw_json = self.gs_ctx.Cat(
metadata_path,
debug_level=logging.DEBUG,
encoding="utf-8",
)
except gs.GSNoSuchKey:
logging.info(
"Could not read %s, falling back to %s",
metadata_path,
partial_metadata_path,
)
raw_json = self.gs_ctx.Cat(
partial_metadata_path,
debug_level=logging.DEBUG,
encoding="utf-8",
)
ref.AssignText(raw_json)
return json.loads(raw_json)
@classmethod
def _LookupMiscCache(cls, cache_dir, key):
"""Looks up an item in the misc cache.
This should be used when inspecting an SDK that's already been
initialized elsewhere.
Args:
cache_dir: The toplevel cache dir to search in.
key: Key of item in the cache.
Returns:
Value of the item, or None if the item is missing.
"""
misc_cache_path = os.path.join(cache_dir, COMMAND_NAME, cls.MISC_CACHE)
misc_cache = cache.DiskCache(misc_cache_path)
with misc_cache.Lookup(key) as ref:
if ref.Exists(lock=True):
return osutils.ReadFile(ref.path).strip()
return None
@classmethod
def GetSDKVersion(cls, cache_dir, board):
"""Looks up the SDK version.
Look at the environment variable, and then the misc cache.
Args:
cache_dir: The toplevel cache dir to search in.
board: The board to search for.
Returns:
SDK version string, if found.
"""
sdk_version = os.environ.get(cls.SDK_VERSION_ENV)
if sdk_version:
return sdk_version
assert board
return cls._LookupMiscCache(cache_dir, (board, "latest"))
@classmethod
def GetCachedFullVersion(cls, cache_dir, board):
"""Get full version from the misc cache.
Args:
cache_dir: The toplevel cache dir to search in.
board: The board to search for.
Returns:
Full version from the misc cache, if found.
"""
assert board
sdk_version = cls.GetSDKVersion(cache_dir, board)
if not sdk_version:
return None
return cls._LookupMiscCache(
cache_dir, ("full-version", board, sdk_version)
)
@classmethod
def GetCachePath(cls, key, cache_dir, board):
"""Gets the path to an item in the cache.
This should be used when inspecting an SDK that's already been
initialized elsewhere.
Args:
key: Key of item in the cache.
cache_dir: The toplevel cache dir to search in.
board: The board to search for.
Returns:
Path to the item, or None if the item is missing.
"""
# The board is always known in the simple chrome SDK shell.
if board is None:
return None
sdk_version = cls.GetSDKVersion(cache_dir, board)
if not sdk_version:
return None
# Look up the cache entry in the symlink cache.
symlink_cache_path = os.path.join(
cache_dir, COMMAND_NAME, cls.SYMLINK_CACHE
)
symlink_cache = cache.DiskCache(symlink_cache_path)
cache_key = (board, sdk_version, key)
with symlink_cache.Lookup(cache_key) as ref:
if ref.Exists():
return ref.path
return None
@classmethod
def FixCachePermissions(cls, cache_dir) -> None:
"""Fixes directories in the cache that are read-only.
crrev.com/c/3905759 added read-only directories into the sysroot that
Simple Chrome downloads. This leads to errors when the cache gets
automatically cleaned up. So we forcibly make every dir in the cache
writable here to avoid that.
Args:
cache_dir: Location of the cache to be cleaned up.
"""
for root, dirs, _ in os.walk(cache_dir):
for dir_name in dirs:
dir_path = os.path.join(root, dir_name)
if not os.access(dir_path, os.W_OK):
os.chmod(dir_path, os.stat(dir_path).st_mode | stat.S_IWUSR)
@classmethod
def ClearOldItems(cls, cache_dir, max_age_days=14) -> None:
"""Removes old items from the tarball cache older than max_age_days.
Inspects the entire cache, not just a single board's items.
Args:
cache_dir: Location of the cache to be cleaned up.
max_age_days: Any item in the cache not created/modified within this
amount of time will be removed.
"""
tarball_cache_path = os.path.join(
cache_dir, COMMAND_NAME, cls.TARBALL_CACHE
)
tarball_cache = cache.TarballCache(tarball_cache_path)
tarball_cache.DeleteStale(datetime.timedelta(days=max_age_days))
# Now clean up any links in the symlink cache that are dangling due to
# the removal of items above.
symlink_cache_path = os.path.join(
cache_dir, COMMAND_NAME, cls.SYMLINK_CACHE
)
symlink_cache = cache.DiskCache(symlink_cache_path)
removed_keys = set()
for key in symlink_cache.ListKeys():
link_path = symlink_cache.GetKeyPath(key)
if not os.path.exists(os.path.realpath(link_path)):
symlink_cache.Lookup(key).Remove()
removed_keys.add((key[0], key[1]))
for board, version in removed_keys:
logging.debug(
"Evicted SDK for %s-%s from the cache.", board, version
)
@memoize.Memoize
def _GetSDKVersion(self, version):
"""Get SDK version from metadata.
Args:
version: LKGM version, e.g. 12345.0.0
Returns:
sdk_version, e.g. 2018.06.04.200410
"""
metadata = self._GetMetadata(version)
build_report = self._GetBuildReport(version)
return metadata.get("sdk-version") or build_report.get("sdkVersion")
def _GetManifest(self, version):
"""Get the build manifest from the cache, downloading it if necessary.
Args:
version: LKGM version, e.g. 12345.0.0
Returns:
build manifest as a python dictionary. The build manifest contains
build versions for packages built by the SDK builder.
"""
with self.misc_cache.Lookup(("manifest", self.board, version)) as ref:
if ref.Exists(lock=True):
manifest = osutils.ReadFile(ref.path)
else:
manifest_path = cros_sdk_lib.get_sdk_manifest_url(
self._GetSDKVersion(version),
for_gsutil=True,
)
manifest = self.gs_ctx.Cat(manifest_path, encoding="utf-8")
ref.AssignText(manifest)
return json.loads(manifest)
def GetDefaultVersion(self):
"""Get the default SDK version to use.
If we are in an existing SDK shell, the default version will just be
the current version. Otherwise, we will try to calculate the
appropriate version to use based on the checkout.
"""
if os.environ.get(self.SDK_BOARD_ENV) == self.board:
sdk_version = os.environ.get(self.SDK_VERSION_ENV)
if sdk_version is not None:
return sdk_version
with self.misc_cache.Lookup((self.board, "latest")) as ref:
if ref.Exists(lock=True):
version = osutils.ReadFile(ref.path).strip()
# Deal with the old version format.
if version.startswith("R"):
version = version.split("-")[1]
return version
else:
return None
def _SetDefaultVersion(self, version) -> None:
"""Set the new default version."""
with self.misc_cache.Lookup((self.board, "latest")) as ref:
ref.AssignText(version)
def UpdateDefaultVersion(self):
"""Update the version that we default to using.
Returns:
A tuple of the form (version, updated), where |version| is the
version number in the format '3929.0.0', and |updated| indicates
whether the version was indeed updated.
"""
checkout_dir = self.chrome_src if self.chrome_src else os.getcwd()
checkout = path_util.DetermineCheckout(checkout_dir)
current = self.GetDefaultVersion() or "0"
if not checkout.chrome_src_dir:
raise chrome_lkgm.NoChromiumSrcDir(checkout_dir)
target = chrome_lkgm.GetChromeLkgm(checkout.chrome_src_dir)
if target is None:
raise chrome_lkgm.MissingLkgmFile(checkout.chrome_src_dir)
self._SetDefaultVersion(target)
return target, target != current
def GetFullVersion(self, version):
"""Get the full ChromeOS version.
Add the release branch and build number to a ChromeOS platform version.
This will specify where you can get the latest build for the given
version for the current board.
Args:
version: A ChromeOS platform number of the form XXXX.XX.XX, i.e.,
3918.0.0. If a full version is provided, it will be returned
unmodified.
Returns:
The version with release branch and build number added, as needed.
E.g. R28-3918.0.0-b1234.
"""
if version.startswith("R"):
return version
with self.misc_cache.Lookup(
("full-version", self.board, version)
) as ref:
if ref.Exists(lock=True):
return osutils.ReadFile(ref.path).strip()
full_version = self.version_finder.GetFullVersionFromLatest(version)
if full_version is None:
raise MissingSDK(self.config_name, self.board, version)
ref.AssignText(full_version)
return full_version
def _GetVersionGSBase(self, version):
"""The base path of the SDK for a particular version."""
if self.sdk_path is not None:
return self.sdk_path
full_version = self.GetFullVersion(version)
return os.path.join(self.gs_base, full_version)
def _GetTarballCacheKey(self, component, url):
"""Builds the cache key tuple for an SDK component.
Returns a key based of the component name + the URL of its location in
GS.
"""
key = self.sdk_path if self.sdk_path else url.strip("gs://")
key = key.replace("/", "-")
return (os.path.join(component, key),)
def _GetLinkNameForComponent(self, version, component):
"""Builds the human-readable symlink name for an SDK component."""
version_section = version
if self.sdk_path is not None:
version_section = self.sdk_path.replace("/", "__").replace(
":", "__"
)
return (self.board, version_section, component)
@contextlib.contextmanager
def Prepare(
self, components, version=None, target_tc=None, toolchain_url=None
):
"""Ensures the components of an SDK exist and are read-locked.
For a given SDK version, pulls down missing components, and provides a
context where the components are read-locked, which prevents the cache
from deleting them during its purge operations.
If both target_tc and toolchain_url arguments are provided, then this
does not download metadata.json for the given version. Otherwise, this
function requires metadata.json for the given version to exist.
Args:
gs_ctx: GSContext object.
components: A list of specific components(tarballs) to prepare.
version: The version to prepare. If not set, uses the version
returned by GetDefaultVersion(). If there is no default version
set (this is the first time we are being executed), then we
update the default version.
target_tc: Target toolchain name to use, e.g. x86_64-cros-linux-gnu
toolchain_url: Format pattern for path to fetch toolchain from,
e.g. 2014/04/%(target)s-2014.04.23.220740.tar.xz
Yields:
An SDKFetcher.SDKContext namedtuple object. The attributes of the
object are:
version: The version that was prepared.
target_tc: Target toolchain name.
key_map: Dictionary that contains CacheReference objects for the
SDK artifacts, indexed by cache key.
"""
if version is None and self.sdk_path is None:
version = self.GetDefaultVersion()
if version is None:
version, _ = self.UpdateDefaultVersion()
components = list(components)
key_map = {}
fetch_urls = {}
self._InstallSquashfsFromCipd()
self._InstallZstdFromCipd()
if not target_tc or not toolchain_url:
# Look-up the toolchain data in both metadata.json and
# build_report.json. We can stop using metadata.json once no one
# needs to build Simple Chrome using artifacts released from a
# cbuildbot build. Likely ~2023.
metadata = self._GetMetadata(version)
build_report = self._GetBuildReport(version)
if not target_tc:
if "toolchain-tuple" in metadata:
target_tc = metadata["toolchain-tuple"][0]
elif build_report and "toolchains" in build_report:
target_tc = build_report["toolchains"][0]
else:
cros_build_lib.Die(
"Toolchains not found in metadata or build report.\n"
f"Metadata: {json.dumps(metadata)}\n"
f"Build report: {json.dumps(build_report)}"
)
if not toolchain_url:
if "toolchain-url" in metadata:
toolchain_url = metadata["toolchain-url"]
elif build_report and "toolchainUrl" in build_report:
toolchain_url = build_report["toolchainUrl"]
else:
cros_build_lib.Die(
"Toolchain URL not found in metadata or build report.\n"
f"Metadata: {json.dumps(metadata)}\n"
f"Build report: {json.dumps(build_report)}"
)
# Fetch Arm32 toolchain for NaCl in Arm64 builds.
if target_tc == self.ARM64_TUPLE:
fetch_urls[self.NACL_ARM32_TOOLCHAIN_KEY] = os.path.join(
self.toolchain_path,
toolchain_url % {"target": self.ARM32_TUPLE},
)
# Fetch toolchains from separate location.
if self.TARGET_TOOLCHAIN_KEY in components:
fetch_urls[self.TARGET_TOOLCHAIN_KEY] = os.path.join(
self.toolchain_path, toolchain_url % {"target": target_tc}
)
components.remove(self.TARGET_TOOLCHAIN_KEY)
# Also fetch QEMU binary if VM download is requested. We don't use it
# directly, but we want to cache the artifacts in case people run `cros
# vm` later on, especially on Chrome bots that want to seed the cache
# before executing tests.
if constants.TEST_IMAGE_TAR in components:
qemu.InstallFromCipd()
version_base = self._GetVersionGSBase(version)
fetch_urls.update(
(t, os.path.join(version_base, t)) for t in components
)
inputs_list = []
try:
for key, url in fetch_urls.items():
tarball_cache_key = self._GetTarballCacheKey(key, url)
tarball_ref = self.tarball_cache.Lookup(tarball_cache_key)
key_map[tarball_cache_key] = tarball_ref
tarball_ref.Acquire()
# Starting with the larger components first when fetching the
# SDK helps ensure we don't save them for a single thread at the
# very end while the remaining threads sit idle. Put the VM
# image first (if we're downloading it), then the sysroot, then
# everything else.
if not tarball_ref.Exists(lock=True):
input_arg = (key, url, tarball_ref)
if key == constants.TEST_IMAGE_TAR:
inputs_list.insert(0, input_arg)
elif key == constants.CHROME_SYSROOT_TAR:
if (
inputs_list
and inputs_list[0][0] == constants.TEST_IMAGE_TAR
):
inputs_list.insert(1, input_arg)
else:
inputs_list.insert(0, input_arg)
else:
inputs_list.append(input_arg)
# Create a symlink in a separate cache dir that points to the
# tarball component. Since the tarball cache is keyed based off
# of GS URLs, these symlinks can be used to identify tarball
# components without knowing the GS URL. This can safely be done
# before actually fetching the SDK components.
link_name = self._GetLinkNameForComponent(version, key)
link_ref = self.symlink_cache.Lookup(link_name)
key_map[key] = link_ref
link_ref.Acquire()
# If the link exists but points to the wrong tarball, we might
# be overriding a component via --toolchain-url or --target-tc.
# In that case, just clobber the symlink and recreate it.
if (
link_ref.Exists()
and osutils.ExpandPath(link_ref.path) != tarball_ref.path
):
link_ref.Remove()
if not link_ref.Exists(lock=True):
self._UpdateCacheSymlink(link_ref, tarball_ref.path)
parallel.RunTasksInProcessPool(
self._UpdateTarball, inputs_list, processes=2
)
ctx_version = version
if self.sdk_path is not None:
ctx_version = CUSTOM_VERSION
yield self.SDKContext(ctx_version, target_tc, key_map)
finally:
# TODO(rcui): Move to using contextlib.ExitStack().
memoize.SafeRun(ref.Release for ref in key_map.values())
class GomaError(Exception):
"""Indicates error with setting up Goma."""
@command.command_decorator(COMMAND_NAME)
class ChromeSDKCommand(command.CliCommand):
"""Set up an environment for building Chrome on Chrome OS.
Pulls down SDK components for building and testing Chrome for Chrome OS,
sets up the environment for building Chrome, and runs a command in the
environment, starting a bash session if no command is specified.
The bash session environment is set up by a user-configurable rc file.
"""
_CHROME_CLANG_DIR = "third_party/llvm-build/Release+Asserts/bin"
_BUILD_ARGS_DIR = "build/args/chromeos/"
EBUILD_ENV_PATHS = (
# Compiler tools.
"CXX",
"CC",
"AR",
"AS",
"LD",
"NM",
"RANLIB",
"READELF",
)
EBUILD_ENV = EBUILD_ENV_PATHS + (
# Compiler flags.
"CFLAGS",
"CXXFLAGS",
"CPPFLAGS",
"LDFLAGS",
# Misc settings.
"GN_ARGS",
"GOLD_SET",
"USE",
)
SDK_GOMA_PORT_ENV = "SDK_GOMA_PORT"
SDK_GOMA_DIR_ENV = "SDK_GOMA_DIR"
GOMACC_PORT_CMD = ["./gomacc", "port"]
# Override base class property to use cache related commandline options.
use_caching_options = True
@staticmethod
def ValidateVersion(version):
"""Ensures that the version arg is potentially valid.
See the argument description for supported version formats.
"""
if not re.match(r"^[0-9]+\.0\.0$", version) and not re.match(
r"^R[0-9]+-[0-9]+\.[0-9]+\.[0-9]+", version
):
raise argparse.ArgumentTypeError(
"--version should be in the format 1234.0.0 or R56-1234.0.0"
)
return version
@classmethod
def AddParser(cls, parser) -> None:
super(ChromeSDKCommand, cls).AddParser(parser)
parser.add_argument(
"--board", required=False, help="The board SDK to use."
)
parser.add_argument(
"--boards",
required=False,
help="Colon-separated list of boards to fetch SDKs for. Implies "
"--no-shell since a shell is tied to a single board. Used to "
"quickly setup cache and build dirs for multiple boards at once.",
)
parser.add_argument(
"--build-label",
default="Release",
help="The label for this build. Used as a subdirectory name under "
"out_${BOARD}/",
)
parser.add_argument(
"--bashrc",
type="str_path",
default=chromite_config.CHROME_SDK_BASHRC,
help="A bashrc file used to set up the SDK shell environment. "
"(default: %(default)s",
)
parser.add_argument(
"--chroot",
type="str_path",
help="Path to a ChromeOS chroot to use. If set, "
"<chroot>/build/<board> will be used as the sysroot that Chrome "
"is built against. If chromeos-chrome was built, the build "
"environment from the chroot will also be used. The version shown "
"in the SDK shell prompt will have an asterisk prepended to it.",
)
parser.add_argument(
"--chrome-src",
type="str_path",
help="Specifies the location of a Chrome src/ directory. Required "
"if not running from a Chrome checkout.",
)
parser.add_argument(
"--cwd",
type="str_path",
help="Specifies a directory to switch to after setting up the SDK "
"shell. Defaults to the current directory.",
)
parser.add_argument(
"--internal",
action="store_true",
default=False,
help="Enables --chrome-branding and --official.",
)
parser.add_argument(
"--chrome-branding",
action="store_true",
default=False,
help="Sets up SDK for building internal Chrome using src-internal, "
"rather than Chromium.",
)
parser.add_argument(
"--official",
action="store_true",
default=False,
help="Enables the official build level of optimization. This "
"removes development conveniences like runtime stack traces, and "
"should be used for performance testing rather than debugging.",
)
parser.add_argument(
"--use-external-config",
action="store_true",
default=False,
help="Use the external configuration for the specified board, even "
"if an internal configuration is avalable.",
)
parser.add_argument(
"--sdk-path",
type="local_or_gs_path",
help="Provides a path, whether a local directory or a gs:// path, "
"to pull SDK components from.",
)
parser.add_argument(
"--toolchain-path",
type="local_or_gs_path",
help="Provides a path, whether a local directory or a gs:// path, "
"to pull toolchain components from.",
)
parser.add_argument(
"--no-shell",
action="store_false",
default=True,
dest="use_shell",
help="Skips the interactive shell. When this arg is passed, the "
"needed toolchain will still be downloaded. However, no //out* "
"dir will automatically be created. The args.gn file will "
"instead be downloaded at a shareable location in //%s, and the "
"SDK will simply exit after that." % cls._BUILD_ARGS_DIR,
)
parser.add_argument(
"--gn-extra-args",
help='Provides extra args to "gn gen". Uses the same format as '
'gn gen, e.g. "foo = true bar = 1".',
)
parser.add_argument(
"--gn-gen",
action="store_true",
default=True,
dest="gn_gen",
help='Run "gn gen" if args.gn is stale.',
)
parser.add_argument(
"--nogn-gen",
action="store_false",
dest="gn_gen",
help='Do not run "gn gen", warns if args.gn is stale.',
)
parser.add_argument(
"--goma",
action="store_true",
default=False,
help="Enables Goma in the shell by adding it to the PATH and "
"set use_goma=true to GN_ARGS.",
deprecated="Goma is deprecated. "
"Please use --use-remoteexec instead.",
)
parser.add_argument(
"--nostart-goma",
action="store_false",
default=True,
dest="start_goma",
help="Skip starting goma and hope somebody else starts goma later.",
deprecated="Goma is deprecated. "
"Please use --use-remoteexec instead.",
)
parser.add_argument(
"--gomadir",
type="str_path",
help="Use the goma installation at the specified PATH.",
deprecated="Goma is deprecated. "
"Please use --use-remoteexec instead.",
)
parser.add_bool_argument(
"--use-remoteexec",
default=True,
enabled_desc="Enable RBE client for the build.",
disabled_desc="Disable RBE client for the build.",
)
parser.add_argument(
"--version",
default=None,
type=cls.ValidateVersion,
help="Specify the SDK version to use. This can be a platform "
"version ending in .0.0, e.g. 1234.0.0, in which case the full "
"version will be extracted from the corresponding LATEST file "
"for the specified board. If no LATEST file exists, an older "
"version will be used if available. Alternatively, a full "
"version may be specified, e.g. R56-1234.0.0, in which case that "
"exact version will be used. Defaults to using the version "
"specified in the CHROMEOS_LKGM file in the chromium checkout.",
)
parser.add_argument(
"--fallback-versions",
type=int,
default=SDKFetcher.VERSIONS_TO_CONSIDER,
help="The number of recent LATEST files to consider in the case "
"that the specified version is missing.",
)
parser.add_argument(
"cmd",
nargs="*",
default=None,
help="The command to execute in the SDK environment. Defaults to "
"starting a bash shell.",
)
parser.add_argument(
"--download-vm",
action="store_true",
default=False,
help="Additionally downloads a VM image from cloud storage.",
)
parser.add_argument(
"--thinlto",
action="store_true",
default=False,
help="Enable ThinLTO in build.",
)
parser.add_argument(
"--cfi",
action="store_true",
default=False,
help="Enable CFI in build.",
)
parser.add_argument(
"--is-lacros",
action="store_true",
default=False,
help="Whether it is Lacros-Chrome build or not. This is "
"temporarily added to work around a Lacros CrOS toolchain bug "
"due to version skew, and should be removed once Lacros is "
"swiched to use Chromium toolchain: crbug.com/1275386.",
)
parser.caching_group.add_argument(
"--clear-sdk-cache",
action="store_true",
default=False,
help="Removes everything in the SDK cache before starting.",
)
group = parser.add_argument_group(
"Metadata Overrides (Advanced)",
description="Provide all of these overrides in order to remove "
"dependencies on metadata.json existence.",
)
group.add_argument(
"--target-tc",
action="store",
default=None,
help="Override target toolchain name, e.g. x86_64-cros-linux-gnu",
)
group.add_argument(
"--toolchain-url",
action="store",
default=None,
help="Override toolchain url format pattern, e.g. "
"2014/04/%%(target)s-2014.04.23.220740.tar.xz",
)
@classmethod
def ProcessOptions(cls, parser, options) -> None:
"""Post process options."""
if bool(options.board) == bool(options.boards):
parser.error("Must specify either one of --board or --boards.")
if options.boards and options.use_shell:
parser.error(
"Must specify --no-shell when preparing multiple boards."
)
if options.is_lacros and not options.version:
parser.error(
"Must specify --version for --is-lacros because Lacros-Chrome "
"build does not use the CHROMEOS_LKGM version for compilation"
)
src_path = options.chrome_src or os.getcwd()
checkout = path_util.DetermineCheckout(src_path)
if not checkout.chrome_src_dir:
parser.error(f"Chrome checkout not found at {src_path}")
options.chrome_src = checkout.chrome_src_dir
if options.boards:
options.boards = options.boards.split(":")
def __init__(self, options) -> None:
super().__init__(options)
self.board = options.board
# Lazy initialized.
self.sdk = None
# Initialized later based on options passed in.
self.silent = True
@staticmethod
def _PS1Prefix(board, version, chroot=None):
"""Returns a string describing the sdk environment for use in PS1."""
chroot_star = "*" if chroot else ""
return "(sdk %s %s%s)" % (board, chroot_star, version)
@staticmethod
def _CreatePS1(board, version, chroot=None):
"""Returns PS1 string that sets commandline and xterm window caption.
If a chroot path is set, then indicate we are using the sysroot from
there instead of the stock sysroot by prepending an asterisk to the
version.
Args:
board: The SDK board.
version: The SDK version.
chroot: The path to the chroot, if set.
"""
current_ps1 = cros_build_lib.run(
["bash", "-l", "-c", 'echo "$PS1"'],
print_cmd=False,
encoding="utf-8",
capture_output=True,
).stdout.splitlines()
if current_ps1:
current_ps1 = current_ps1[-1]
if not current_ps1:
# Something went wrong, so use a fallback value.
current_ps1 = r"\u@\h \w $ "
ps1_prefix = ChromeSDKCommand._PS1Prefix(board, version, chroot)
return "%s %s" % (ps1_prefix, current_ps1)
def _SaveSharedGnArgs(self, gn_args, board) -> None:
"""Saves the new gn args data to the shared location."""
shared_dir = os.path.join(self.options.chrome_src, self._BUILD_ARGS_DIR)
if not self.options.is_lacros:
file_path = os.path.join(shared_dir, board + ".gni")
osutils.WriteFile(file_path, gn_helpers.ToGNString(gn_args))
return
# If the board is a generic family, generate -crostoolchain.gni files,
# too, which is used by Lacros build.
if board.endswith("-generic"):
# pylint: disable=line-too-long
toolchain_key_pattern = re.compile(
r"^(%s)$"
% "|".join(
[
"cros_board",
"cros_sdk_version",
"is_clang",
"target_cpu",
"cros_host_(cc|cxx|ld|extra_(c|cpp|cxx|ld)flags)",
"cros_target_(ar|cc|cxx|ld|nm|readelf|extra_(c|cpp|cxx|ld)flags)",
"cros_v8_snapshot_(cc|cxx|ld|extra_(c|cpp|cxx|ld)flags)",
"cros_nacl_helper_arm32_(ar|cc|cxx|ld|readelf|sysroot)",
"(custom|host|v8_snapshot)_toolchain",
"rbe_cros_cc_wrapper",
"system_libdir",
"target_sysroot",
"arm_(float_abi|use_neon)",
]
)
)
# pylint: enable=line-too-long
toolchain_gn_args = {
k: v
for k, v in gn_args.items()
if toolchain_key_pattern.match(k)
}
file_path = os.path.join(shared_dir, board + "-crostoolchain.gni")
osutils.WriteFile(
file_path, gn_helpers.ToGNString(toolchain_gn_args)
)
def _UpdateGnArgsIfStale(
self, out_dir, build_label, gn_args, board
) -> None:
"""Runs 'gn gen' if gn args are stale or logs a warning."""
build_dir = os.path.join(out_dir, build_label)
gn_args_file_path = os.path.join(
self.options.chrome_src, build_dir, "args.gn"
)
if not self.options.use_shell:
import_line = 'import("//%s%s.gni")' % (self._BUILD_ARGS_DIR, board)
if os.path.exists(
gn_args_file_path
) and not import_line in osutils.ReadFile(gn_args_file_path):
logging.warning(
"Stale or malformed args.gn file at %s. Regenerating.",
gn_args_file_path,
)
osutils.SafeUnlink(gn_args_file_path)
if not os.path.exists(gn_args_file_path):
osutils.WriteFile(
gn_args_file_path,
textwrap.dedent(
"""\
%s
# Place any additional args or overrides below:
"""
% import_line
),
makedirs=True,
)
return
if not self._StaleGnArgs(gn_args, gn_args_file_path):
return
if not self.options.gn_gen:
logging.warning("To update gn args run:")
logging.warning('gn gen %s --args="$GN_ARGS"', build_dir)
return
logging.warning("Running gn gen")
cros_build_lib.run(
[
"gn",
"gen",
build_dir,
"--args=%s" % gn_helpers.ToGNString(gn_args),
],
print_cmd=logging.getLogger().isEnabledFor(logging.DEBUG),
cwd=self.options.chrome_src,
)
def _StaleGnArgs(self, new_gn_args, gn_args_file_path):
"""Returns True if args.gn needs to be updated."""
if not os.path.exists(gn_args_file_path):
logging.warning("No args.gn file: %s", gn_args_file_path)
return True
parser = gn_helpers.GNValueParser(
osutils.ReadFile(gn_args_file_path),
checkout_root=self.options.chrome_src,
)
old_gn_args = parser.ParseArgs()
if new_gn_args == old_gn_args:
return False
logging.warning("Stale args.gn file: %s", gn_args_file_path)
self._LogArgsDiff(old_gn_args, new_gn_args)
return True
def _LogArgsDiff(self, cur_args, new_args) -> None:
"""Logs the differences between |cur_args| and |new_args|."""
cur_keys = set(cur_args.keys())
new_keys = set(new_args.keys())
for k in new_keys - cur_keys:
logging.info("MISSING ARG: %s = %s", k, new_args[k])
for k in cur_keys - new_keys:
logging.info("EXTRA ARG: %s = %s", k, cur_args[k])
for k in new_keys & cur_keys:
v_cur = cur_args[k]
v_new = new_args[k]
if v_cur != v_new:
logging.info("MISMATCHED ARG: %s: %s != %s", k, v_cur, v_new)
def _SetupTCEnvironment(self, options, env) -> None:
"""Sets up toolchain-related environment variables."""
chrome_clang_path = os.path.join(
options.chrome_src, self._CHROME_CLANG_DIR
)
# For host compiler, we use the compiler that comes with Chrome
# instead of the target compiler.
env["CC_host"] = os.path.join(chrome_clang_path, "clang")
env["CXX_host"] = os.path.join(chrome_clang_path, "clang++")
env["LD_host"] = env["CXX_host"]
def _AbsolutizeBinaryPath(self, binary, tc_path):
"""Modify toolchain path for goma build.
This function absolutizes the path to the given toolchain binary, which
will then be relativized in build/toolchain/cros/BUILD.gn. This ensures
the paths are the same across different machines & checkouts, which
improves cache hit rate in distributed build systems (i.e. goma).
Args:
binary: Name of toolchain binary.
tc_path: Path to toolchain directory.
Returns:
Absolute path to the binary in the toolchain dir.
"""
# If binary doesn't contain a '/', assume it's located in the toolchain
# dir.
if os.path.basename(binary) == binary:
return os.path.join(tc_path, "bin", binary)
return binary
def _GenerateReclientWrapper(self, board):
"""Generate a wrapper for reclient.
This function generates a wrapper script for the rewrapper to make it
passed with --gomacc-path (rewrapper_<board>).
The wrapper adds a flag to preserve symlinks which are used by CrOS
clang.
Args:
board: Target board name to be used as a config and wrapper name.
Returns:
Absolute path to the wrapper script to be used as --gomacc-path.
"""
shared_dir = os.path.join(self.options.chrome_src, self._BUILD_ARGS_DIR)
# TODO(b:190741226): remove the wrapper if the compiler wrapper supports
# flags for reclient.
wrapper_path = os.path.join(shared_dir, "rewrapper_%s" % board)
wrapper_content = [
"#!/bin/sh\n",
"%(rewrapper_dir)s/rewrapper -preserve_symlink=true "
'-exec_root="%(chrome_src)s" "$@"\n'
% {
"rewrapper_dir": os.path.join(
self.options.chrome_src, "buildtools", "reclient"
),
"chrome_src": self.options.chrome_src,
},
]
osutils.WriteFile(wrapper_path, wrapper_content, chmod=0o755)
Log("generated rewrapper wrapper %s", wrapper_path, silent=self.silent)
return wrapper_path
def _SetupEnvironment(
self, board, sdk_ctx, options, goma_dir=None, goma_port=None
):
"""Sets environment variables to export to the SDK shell."""
if options.chroot:
sysroot = os.path.join(options.chroot, "build", board)
if not os.path.isdir(sysroot) and not options.cmd:
logging.warning(
"Because --chroot is set, expected a sysroot to be at "
"%s, but couldn't find one.",
sysroot,
)
else:
sysroot = sdk_ctx.key_map[constants.CHROME_SYSROOT_TAR].path
environment = os.path.join(
sdk_ctx.key_map[constants.CHROME_ENV_TAR].path, "environment"
)
if options.chroot:
# Override with the environment from the chroot if available (i.e.
# build_packages or emerge chromeos-chrome has been run for
# |board|).
env_path = os.path.join(
sysroot,
portage_util.VDB_PATH,
"chromeos-base",
"chromeos-chrome-*",
)
env_glob = glob.glob(env_path)
if len(env_glob) != 1:
logging.warning(
"Multiple Chrome versions in %s. This can be resolved"
' by running "eclean-$BOARD -d packages". Using'
" environment from: %s",
env_path,
environment,
)
elif not os.path.isdir(env_glob[0]):
logging.warning(
"Environment path not found: %s. Using enviroment from:"
" %s.",
env_path,
environment,
)
else:
chroot_env_file = os.path.join(env_glob[0], "environment.bz2")
if os.path.isfile(chroot_env_file):
# Log a warning here since this is new behavior that is not
# obvious.
logging.notice(
"Environment fetched from: %s", chroot_env_file
)
# Uncompress enviornment.bz2 to pass to
# osutils.SourceEnvironment.
chroot_cache = os.path.join(
options.cache_dir, COMMAND_NAME, "chroot"
)
osutils.SafeMakedirs(chroot_cache)
environment = os.path.join(
chroot_cache, "environment_%s" % board
)
cros_build_lib.UncompressFile(chroot_env_file, environment)
env = osutils.SourceEnvironment(environment, self.EBUILD_ENV)
gn_args = gn_helpers.FromGNArgs(env["GN_ARGS"])
self._SetupTCEnvironment(options, env)
# Add managed components to the PATH.
path = os.environ["PATH"].split(os.pathsep)
path.insert(0, str(constants.CHROMITE_BIN_DIR))
env["PATH"] = os.pathsep.join(path)
# Export internally referenced variables.
os.environ[self.sdk.SDK_BOARD_ENV] = board
if options.sdk_path:
os.environ[self.sdk.SDK_PATH_ENV] = options.sdk_path
os.environ[self.sdk.SDK_VERSION_ENV] = sdk_ctx.version
# Add board and sdk version as gn args so that tests can bind them in
# test wrappers generated at compile time.
gn_args["cros_board"] = board
if options.is_lacros:
# The 'cros_sdk_version' is used by the chromium BUILD files to
# decide the runtime dependencies to isolate for swarming based
# testing, and given that Lacros uses CHROMEOS_LKGM for testing
# regardless of the version used for compilation, so always set the
# value as CHROME_LKGM.
gn_args["cros_sdk_version"] = chrome_lkgm.GetChromeLkgm(
options.chrome_src
)
else:
gn_args["cros_sdk_version"] = sdk_ctx.version
# Export the board/version info in a more accessible way, so developers
# can reference them in their chrome_sdk.bashrc files, as well as within
# the chrome-sdk shell.
for var in [self.sdk.SDK_VERSION_ENV, self.sdk.SDK_BOARD_ENV]:
env[var.lstrip("%")] = os.environ[var]
# Export Goma information.
if goma_dir:
env[self.SDK_GOMA_DIR_ENV] = goma_dir
if goma_port:
env[self.SDK_GOMA_PORT_ENV] = goma_port
# SYSROOT is necessary for Goma and the sysroot wrapper.
env["SYSROOT"] = sysroot
gn_args["target_sysroot"] = sysroot
# Use Chrome's host sysroot settings and pkg_config for building outside
# the chroot.
gn_args.pop("use_sysroot", None)
gn_args.pop("pkg_config", None)
gn_args.pop("host_pkg_config", None)
# --internal == --chrome-branding + --official
if options.chrome_branding or options.internal:
gn_args["is_chrome_branded"] = True
else:
gn_args.pop("is_chrome_branded", None)
gn_args.pop("internal_gles2_conform_tests", None)
if options.official or options.internal:
gn_args["is_official_build"] = True
else:
gn_args.pop("is_official_build", None)
if not options.internal:
gn_args.pop("enable_hevc_parser_and_hw_decoder", None)
target_tc_path = sdk_ctx.key_map[self.sdk.TARGET_TOOLCHAIN_KEY].path
for env_path in self.EBUILD_ENV_PATHS:
env[env_path] = self._AbsolutizeBinaryPath(
env[env_path], target_tc_path
)
# Add Arm32 toolchain GN flags for building nacl_helper on Arm64.
if self.sdk.NACL_ARM32_TOOLCHAIN_KEY in sdk_ctx.key_map:
nacl_helper_tc_path = sdk_ctx.key_map[
self.sdk.NACL_ARM32_TOOLCHAIN_KEY
].path
gn_args["cros_nacl_helper_arm32_ar"] = self._AbsolutizeBinaryPath(
"llvm-ar", nacl_helper_tc_path
)
gn_args["cros_nacl_helper_arm32_cc"] = self._AbsolutizeBinaryPath(
self.sdk.ARM32_TUPLE + "-clang", nacl_helper_tc_path
)
gn_args["cros_nacl_helper_arm32_cxx"] = self._AbsolutizeBinaryPath(
self.sdk.ARM32_TUPLE + "-clang++", nacl_helper_tc_path
)
gn_args["cros_nacl_helper_arm32_ld"] = self._AbsolutizeBinaryPath(
self.sdk.ARM32_TUPLE + "-clang++", nacl_helper_tc_path
)
gn_args[
"cros_nacl_helper_arm32_readelf"
] = self._AbsolutizeBinaryPath("llvm-readelf", nacl_helper_tc_path)
gn_args["cros_nacl_helper_arm32_sysroot"] = os.path.join(
nacl_helper_tc_path, "usr", self.sdk.ARM32_TUPLE
)
gn_args["cros_target_cc"] = env["CC"]
gn_args["cros_target_cxx"] = env["CXX"]
gn_args["cros_target_ld"] = env["LD"]
gn_args["cros_target_nm"] = env["NM"]
gn_args["cros_target_ar"] = env["AR"]
gn_args["cros_target_readelf"] = env["READELF"]
gn_args["cros_target_extra_cflags"] = env.get("CFLAGS", "")
gn_args["cros_target_extra_cxxflags"] = env.get("CXXFLAGS", "")
gn_args["cros_host_cc"] = env["CC_host"]
gn_args["cros_host_cxx"] = env["CXX_host"]
gn_args["cros_host_ld"] = env["LD_host"]
gn_args["cros_v8_snapshot_cc"] = env["CC_host"]
gn_args["cros_v8_snapshot_cxx"] = env["CXX_host"]
gn_args["cros_v8_snapshot_ld"] = env["LD_host"]
# Let Chromium's build files pick defaults for the following.
gn_args.pop("cros_host_nm", None)
gn_args.pop("cros_host_ar", None)
gn_args.pop("cros_host_readelf", None)
gn_args.pop("cros_v8_snapshot_nm", None)
gn_args.pop("cros_v8_snapshot_ar", None)
gn_args.pop("cros_v8_snapshot_readelf", None)
# No need to adjust CFLAGS and CXXFLAGS for GN since the only
# adjustment made in _SetupTCEnvironment is for split debug which
# is done with 'use_debug_fission'.
gn_args["use_remoteexec"] = options.use_remoteexec
# Enable goma if requested.
if not options.goma or options.use_remoteexec:
# If --nogoma option is explicitly set, disable goma, even if it is
# used in the original GN_ARGS.
gn_args["use_goma"] = False
gn_args.pop("goma_dir", None)
elif goma_dir:
gn_args["use_goma"] = True
gn_args["goma_dir"] = goma_dir
gn_args.pop("internal_khronos_glcts_tests", None) # crbug.com/588080
# The ebuild sets dcheck_always_on to false to avoid a default value of
# true for bots. But we'd like developers using DCHECKs when possible,
# so we let dcheck_always_on use the default value for Simple Chrome.
gn_args.pop("dcheck_always_on", None)
# "rbe_cfg_dir" and "rbe_exec_root" defined in chromeos-chrome ebuild
# is only relevant for builds done within chroot via portage. So we
# need to remove them and use the ones defined in chromium.
gn_args.pop("rbe_cfg_dir", None)
gn_args.pop("rbe_exec_root", None)
# Disable ThinLTO and CFI for simplechrome. Tryjob machines do not have
# enough file descriptors to use. crbug.com/789607
if not options.thinlto:
gn_args["use_thin_lto"] = False
if not options.cfi:
gn_args["is_cfi"] = False
# When using Goma and ThinLTO, distribute ThinLTO code generation on
# Goma.
gn_args["use_goma_thin_lto"] = gn_args.get(
"use_goma", False
) and gn_args.get("use_thin_lto", False)
# We need to remove the flag -Wl,-plugin-opt,-import-instr-limit=$num
# from cros_target_extra_ldflags if options.thinlto is not set.
# The format of ld flags is something like
# '-Wl,-O1 -Wl,-O2 -Wl,--as-needed -stdlib=libc++'
if not options.thinlto:
extra_ldflags = gn_args.get("cros_target_extra_ldflags", "")
ld_flags_list = extra_ldflags.split()
ld_flags_list = [
f
for f in ld_flags_list
if not f.startswith("-Wl,-plugin-opt,-import-instr-limit")
]
if extra_ldflags:
gn_args["cros_target_extra_ldflags"] = " ".join(ld_flags_list)
# We removed blink symbols on release builds on arm, see
# https://crbug.com/792999. However, we want to keep the symbols
# for simplechrome builds.
gn_args["blink_symbol_level"] = -1
# Remove symbol_level specified in the ebuild to use the default.
# Currently that is 1 when is_debug=false, instead of 2 specified by the
# ebuild. This results in faster builds in Simple Chrome.
if "symbol_level" in gn_args:
symbol_level = gn_args.pop("symbol_level")
logging.info(
"Removing symbol_level = %d from gn args, use "
"--gn-extra-args to specify a non default value.",
symbol_level,
)
gn_args["rbe_cros_cc_wrapper"] = self._GenerateReclientWrapper(board)
if options.gn_extra_args:
gn_args.update(gn_helpers.FromGNArgs(options.gn_extra_args))
gn_args_env = gn_helpers.ToGNString(gn_args)
env["GN_ARGS"] = gn_args_env
# PS1 sets the command line prompt and xterm window caption.
full_version = sdk_ctx.version
if full_version != CUSTOM_VERSION:
full_version = self.sdk.GetFullVersion(sdk_ctx.version)
env["PS1"] = self._CreatePS1(board, full_version, chroot=options.chroot)
# Set the useful part of PS1 for users with a custom PROMPT_COMMAND.
env["CROS_PS1_PREFIX"] = self._PS1Prefix(
board, full_version, chroot=options.chroot
)
out_dir = "out_%s" % board
env["builddir_name"] = out_dir
# This is used by landmines.py to prevent collisions when building both
# chromeos and android from shared source.
# For context, see crbug.com/407417
env["CHROMIUM_OUT_DIR"] = os.path.join(options.chrome_src, out_dir)
if not self.options.use_shell:
self._SaveSharedGnArgs(gn_args, board)
self._UpdateGnArgsIfStale(out_dir, options.build_label, gn_args, board)
return env
@staticmethod
def _VerifyGoma(user_rc) -> None:
"""Verify that the user has no goma installations set up in user_rc.
If the user does have a goma installation set up, verify that it's for
ChromeOS.
Args:
user_rc: User-supplied rc file.
"""
user_env = osutils.SourceEnvironment(user_rc, ["PATH"])
goma_ctl = osutils.Which("goma_ctl.py", user_env.get("PATH"))
if goma_ctl is not None:
logging.warning(
"%s is adding Goma to the PATH. Using that Goma instead of the "
"managed Goma install.",
user_rc,
)
@staticmethod
def _VerifyChromiteBin(user_rc) -> None:
"""Verify that the user has not set a chromite bin/ dir in user_rc.
Args:
user_rc: User-supplied rc file.
"""
user_env = osutils.SourceEnvironment(user_rc, ["PATH"])
chromite_bin = osutils.Which("parallel_emerge", user_env.get("PATH"))
if chromite_bin is not None:
logging.warning(
"%s is adding chromite/bin to the PATH. Remove it from the "
"PATH to use the the default Chromite.",
user_rc,
)
@contextlib.contextmanager
def _GetRCFile(self, env, user_rc):
"""Returns path to dynamically created bashrc file.
The bashrc file sets the environment variables contained in |env|, as
well as sources the user-editable chrome_sdk.bashrc file in the user's
home directory. That rc file is created if it doesn't already exist.
Args:
env: A dictionary of environment variables that will be set by the
rc file.
user_rc: User-supplied rc file.
"""
if not os.path.exists(user_rc):
osutils.Touch(user_rc, makedirs=True)
self._VerifyGoma(user_rc)
self._VerifyChromiteBin(user_rc)
# We need a temporary rc file to 'wrap' the user configuration file,
# because running with '--rcfile' causes bash to ignore bash special
# variables passed through subprocess.Popen, such as PS1. So we set
# them here.
#
# Having a wrapper rc file will also allow us to inject bash functions
# into the environment, not just variables.
with osutils.TempDir() as tempdir:
# Only source the user's ~/.bashrc if running in interactive mode.
contents = [
"[[ -e ~/.bashrc && $- == *i* ]] && . ~/.bashrc\n",
]
for key, value in env.items():
contents.append("export %s='%s'\n" % (key, value))
contents.append('. "%s"\n' % user_rc)
rc_file = os.path.join(tempdir, "rcfile")
osutils.WriteFile(rc_file, contents)
yield rc_file
def _GomaPort(self, goma_dir):
"""Returns current active Goma port."""
port = cros_build_lib.run(
self.GOMACC_PORT_CMD,
cwd=goma_dir,
debug_level=logging.DEBUG,
check=False,
encoding="utf-8",
capture_output=True,
).stdout.strip()
return port
def _GomaDir(self, goma_dir):
"""Returns current active Goma directory."""
if not goma_dir:
goma_dir_cmd = ["goma_ctl", "goma_dir"]
goma_dir = cros_build_lib.run(
goma_dir_cmd, check=False, capture_output=True, encoding="utf-8"
).stdout.strip()
if goma_dir and os.path.exists(os.path.join(goma_dir, "gomacc")):
return goma_dir
def _SetupGoma(self):
"""Find installed Goma and start Goma.
Returns:
A tuple (dir, port) containing the path to the cached goma/ dir and
the Goma port.
"""
goma_dir = self._GomaDir(self.options.gomadir)
if not goma_dir:
raise GomaError(
"Failed to find the Goma client."
" Please confirm depot_tools is in PATH,"
" and you do not set GOMA_DIR."
)
port = None
if self.options.start_goma:
Log("Starting Goma.", silent=self.silent)
cros_build_lib.dbg_run(
[os.path.join(goma_dir, "goma_ctl.py"), "ensure_start"],
extra_env={"GOMA_ARBITRARY_TOOLCHAIN_SUPPORT": "true"},
)
port = self._GomaPort(goma_dir)
Log("Goma is started on port %s", port, silent=self.silent)
if not port:
raise GomaError("No Goma port detected")
return goma_dir, port
def Run(self):
"""Perform the command."""
if os.environ.get(SDKFetcher.SDK_VERSION_ENV) is not None:
cros_build_lib.Die("Already in an SDK shell.")
# Migrate config file from old to new path.
old_config = Path("~/.chromite/chrome_sdk.bashrc").expanduser()
if (
old_config.exists()
and not chromite_config.CHROME_SDK_BASHRC.exists()
):
chromite_config.initialize()
old_config.rename(chromite_config.CHROME_SDK_BASHRC)
try:
old_config.parent.rmdir()
except OSError:
pass
if self.options.chrome_branding or self.options.internal:
gclient_path = gclient.FindGclientFile(self.options.chrome_src)
if not gclient_path:
cros_build_lib.Die(
"Found a Chrome checkout at %s with no .gclient file.",
self.options.chrome_src,
)
gclient_solutions = gclient.LoadGclientFile(gclient_path)
for solution in gclient_solutions:
if not solution.get("url", "").startswith(
gclient.CHROME_COMMITTER_URL
):
continue
if solution.get("custom_vars", {}).get("checkout_src_internal"):
break
cros_build_lib.Die(
"You've passed in '--chrome-branding' or '--internal' to "
"Simple Chrome, but your .gclient file at %s lacks "
"'checkout_src_internal'. Set that var to True in the "
"'custom_vars' section of your .gclient file and re-sync.",
gclient_path,
)
if self.options.version and self.options.sdk_path:
cros_build_lib.Die("Cannot specify both --version and --sdk-path.")
if self.options.cfi and not self.options.thinlto:
cros_build_lib.Die("CFI requires ThinLTO.")
# Fix read-only dirs in the cache.
SDKFetcher.FixCachePermissions(self.options.cache_dir)
# Remove old SDKs from the cache to avoid wasting disk space.
SDKFetcher.ClearOldItems(self.options.cache_dir)
if self.options.board:
return self._RunOnceForBoard(self.options.board)
else:
for board in self.options.boards:
start = datetime.datetime.now()
self._RunOnceForBoard(board)
duration = datetime.datetime.now() - start
if duration > datetime.timedelta(minutes=1):
logging.warning(
"It took %s to fetch the SDK for %s. Consider removing "
"it from your .gclient file if you no longer need to "
"build for it.",
pformat.timedelta(duration),
board,
)
def _RunOnceForBoard(self, board):
"""Internal implementation of Run() above for a single board."""
self.silent = bool(self.options.cmd)
# Lazy initialize because SDKFetcher creates a GSContext() object in its
# constructor, which may block on user input.
self.sdk = SDKFetcher(
self.options.cache_dir,
board,
clear_cache=self.options.clear_sdk_cache,
chrome_src=self.options.chrome_src,
sdk_path=self.options.sdk_path,
toolchain_path=self.options.toolchain_path,
silent=self.silent,
use_external_config=self.options.use_external_config,
fallback_versions=self.options.fallback_versions,
is_lacros=self.options.is_lacros,
)
prepare_version = self.options.version
if not prepare_version and not self.options.sdk_path:
prepare_version, _ = self.sdk.UpdateDefaultVersion()
components = [self.sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR]
if not self.options.chroot:
components.append(constants.CHROME_SYSROOT_TAR)
components.append("autotest_server_package.tar.bz2")
if self.options.download_vm:
components.append(constants.TEST_IMAGE_TAR)
goma_dir = None
goma_port = None
if self.options.goma and not self.options.use_remoteexec:
try:
goma_dir, goma_port = self._SetupGoma()
except GomaError as e:
logging.error("Goma: %s. Bypass by running with --nogoma.", e)
with self.sdk.Prepare(
components,
version=prepare_version,
target_tc=self.options.target_tc,
toolchain_url=self.options.toolchain_url,
) as ctx:
env = self._SetupEnvironment(
board, ctx, self.options, goma_dir=goma_dir, goma_port=goma_port
)
if not self.options.use_shell:
return 0
with self._GetRCFile(env, self.options.bashrc) as rcfile:
bash_cmd = ["/bin/bash"]
extra_env = None
if not self.options.cmd:
bash_cmd.extend(["--rcfile", rcfile, "-i"])
else:
# The '"$@"' expands out to the properly quoted positional
# args coming after the '--'.
bash_cmd.extend(["-c", '"$@"', "--"])
bash_cmd.extend(self.options.cmd)
# When run in noninteractive mode, bash sources the rc file
# set in BASH_ENV, and ignores the --rcfile flag.
extra_env = {"BASH_ENV": rcfile}
# Bash behaves differently when it detects that it's being
# launched by sshd - it ignores the BASH_ENV variable. So
# prevent ssh-related environment variables from being passed
# through.
os.environ.pop("SSH_CLIENT", None)
os.environ.pop("SSH_CONNECTION", None)
os.environ.pop("SSH_TTY", None)
cmd_result = cros_build_lib.run(
bash_cmd,
print_cmd=False,
debug_level=logging.CRITICAL,
check=False,
extra_env=extra_env,
cwd=self.options.cwd,
)
if self.options.cmd:
return cmd_result.returncode