blob: 0b7ac03e06eb74195843e008cd357e08aecb5ac6 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provide a class for collecting info on one builder run.
There are two public classes, BuilderRun and ChildBuilderRun, that serve
this function. The first is for most situations, the second is for "child"
configs within a builder config that has entries in "child_configs".
Almost all functionality is within the common _BuilderRunBase class. The
only thing the BuilderRun and ChildBuilderRun classes are responsible for
is overriding the self.config value in the _BuilderRunBase object whenever
it is accessed.
It is important to note that for one overall run, there will be one
BuilderRun object and zero or more ChildBuilderRun objects, but they
will all share the same _BuilderRunBase *object*. This means, for example,
that run attributes (e.g. self.attrs.release_tag) are shared between them
all, as intended.
"""
from __future__ import print_function
import functools
import os
import pickle
import re
import types
from six.moves import queue as Queue
from chromite.cbuildbot import archive_lib
from chromite.lib import buildstore
from chromite.lib import constants
from chromite.lib import metadata_lib
from chromite.lib import cidb
from chromite.lib import cros_build_lib
from chromite.lib import osutils
from chromite.lib import path_util
from chromite.lib import portage_util
from chromite.lib import uri_lib
class RunAttributesError(Exception):
"""Base class for exceptions related to RunAttributes behavior."""
def __str__(self):
"""Handle stringify because base class will just spit out self.args."""
return self.msg
class VersionNotSetError(RuntimeError):
"""Error raised if trying to access version_info before it's set."""
class ParallelAttributeError(AttributeError):
"""Custom version of AttributeError."""
def __init__(self, attr, board=None, target=None, *args):
if board or target:
self.msg = ('No such board-specific parallel run attribute %r for %s/%s' %
(attr, board, target))
else:
self.msg = 'No such parallel run attribute %r' % attr
super(ParallelAttributeError, self).__init__(self.msg, *args)
self.args = (attr, board, target) + tuple(args)
def __str__(self):
return self.msg
class AttrSepCountError(ValueError):
"""Custom version of ValueError for when BOARD_ATTR_SEP is misused."""
def __init__(self, attr, *args):
self.msg = ('Attribute name has an unexpected number of "%s" occurrences'
' in it: %s' % (RunAttributes.BOARD_ATTR_SEP, attr))
super(AttrSepCountError, self).__init__(self.msg, *args)
self.args = (attr, ) + tuple(args)
def __str__(self):
return self.msg
class AttrNotPickleableError(RunAttributesError):
"""For when attribute value to queue is not pickleable."""
def __init__(self, attr, value, *args):
self.msg = 'Run attribute "%s" value cannot be pickled: %r' % (attr, value)
super(AttrNotPickleableError, self).__init__(self.msg, *args)
self.args = (attr, value) + tuple(args)
class AttrTimeoutError(RunAttributesError):
"""For when timeout is reached while waiting for attribute value."""
def __init__(self, attr, *args):
self.msg = 'Timed out waiting for value for run attribute "%s".' % attr
super(AttrTimeoutError, self).__init__(self.msg, *args)
self.args = (attr, ) + tuple(args)
class NoAndroidBranchError(Exception):
"""For when Android branch cannot be determined."""
class NoAndroidABIError(Exception):
"""For when Android ABI cannot be determined."""
class NoAndroidVariantError(Exception):
"""For when Android variant cannot be determined."""
class NoAndroidTargetError(Exception):
"""For when Android target cannot be determined."""
class NoAndroidVersionError(Exception):
"""For when Android version cannot be determined."""
class LockableQueue(object):
"""Multiprocessing queue with associated recursive lock.
Objects of this class function just like a regular multiprocessing Queue,
except that there is also an rlock attribute for getting a multiprocessing
RLock associated with this queue. Actual locking must still be handled by
the calling code.
Examples:
with queue.rlock:
... process the queue in some way.
"""
def __init__(self, manager):
self._queue = manager.Queue()
self.rlock = manager.RLock()
def __getattr__(self, attr):
"""Relay everything to the underlying Queue object at self._queue."""
return getattr(self._queue, attr)
class RunAttributes(object):
"""Hold all run attributes for a particular builder run.
There are two supported flavors of run attributes: REGULAR attributes are
available only to stages that are run sequentially as part of the main (top)
process and PARALLEL attributes are available to all stages, no matter what
process they are in. REGULAR attributes are accessed directly as normal
attributes on a RunAttributes object, while PARALLEL attributes are accessed
through the {Set|Has|Get}Parallel methods. PARALLEL attributes also have the
restriction that their values must be pickle-able (in order to be sent
through multiprocessing queue).
The currently supported attributes of each kind are listed in REGULAR_ATTRS
and PARALLEL_ATTRS below. To add support for a new run attribute simply
add it to one of those sets.
A subset of PARALLEL_ATTRS is BOARD_ATTRS. These attributes only have meaning
in the context of a specific board and config target. The attributes become
available once a board/config is registered for a run, and then they can be
accessed through the {Set|Has|Get}BoardParallel methods or through the
{Get|Set|Has}Parallel methods of a BoardRunAttributes object. The latter is
encouraged.
To add a new BOARD attribute simply add it to the BOARD_ATTRS set below, which
will also add it to PARALLEL_ATTRS (all BOARD attributes are assumed to need
PARALLEL support).
"""
REGULAR_ATTRS = frozenset((
'chrome_version', # Set by SyncChromeStage, if it runs.
'manifest_manager', # Set by ManifestVersionedSyncStage.
'release_tag', # Set by cbuildbot after sync stage.
'version_info', # Set by the builder after sync+patch stage.
'metadata', # Used by various build stages to record metadata.
))
# TODO(mtennant): It might be useful to have additional info for each board
# attribute: 1) a log-friendly pretty name, 2) a rough upper bound timeout
# value for consumers of the attribute to use when waiting for it.
BOARD_ATTRS = frozenset((
'breakpad_symbols_generated', # Set by DebugSymbolsStage.
'debug_tarball_generated', # Set by DebugSymbolsStage.
'images_generated', # Set by BuildImageStage.
'test_artifacts_uploaded', # Set by UploadHWTestArtifacts.
'autotest_tarball_generated', # Set by ArchiveStage.
'instruction_urls_per_channel', # Set by ArchiveStage
'success', # Set by cbuildbot.py:Builder
'packages_under_test', # Set by BuildPackagesStage.
'signed_images_ready', # Set by SigningStage
'paygen_test_payloads_ready', # Set by PaygenStage
'unittest_completed', # Set by UnitTestStage.
))
# Attributes that need to be set by stages that can run in parallel
# (i.e. in a subprocess) must be included here. All BOARD_ATTRS are
# assumed to fit into this category.
PARALLEL_ATTRS = BOARD_ATTRS | frozenset((
'unittest_value', # For unittests. An example of a PARALLEL attribute
# that is not also a BOARD attribute.
))
# This separator is used to create a unique attribute name for any
# board-specific attribute. For example:
# breakpad_symbols_generated||stumpy||stumpy-full-config
BOARD_ATTR_SEP = '||'
# Sanity check, make sure there is no overlap between the attr groups.
assert not REGULAR_ATTRS & PARALLEL_ATTRS
# REGULAR_ATTRS show up as attributes directly on the RunAttributes object.
__slots__ = tuple(REGULAR_ATTRS) + (
'_board_targets', # Set of registered board/target combinations.
'_manager', # The multiprocessing.Manager to use.
'_queues', # Dict of parallel attribute names to LockableQueues.
)
def __init__(self, multiprocess_manager):
# The __slots__ logic above confuses pylint.
# https://bitbucket.org/logilab/pylint/issue/380/
# pylint: disable=assigning-non-slot
# Create queues for all non-board-specific parallel attributes now.
# Parallel board attributes must wait for the board to be registered.
self._manager = multiprocess_manager
self._queues = {}
for attr in RunAttributes.PARALLEL_ATTRS:
if attr not in RunAttributes.BOARD_ATTRS:
self._queues[attr] = LockableQueue(self._manager)
# Set of known <board>||<target> combinations.
self._board_targets = set()
def RegisterBoardAttrs(self, board, target):
"""Register a new valid board/target combination. Safe to repeat.
Args:
board: Board name to register.
target: Build config name to register.
Returns:
A new BoardRunAttributes object for more convenient access to the newly
registered attributes specific to this board/target combination.
"""
board_target = RunAttributes.BOARD_ATTR_SEP.join((board, target))
if not board_target in self._board_targets:
# Register board/target as a known board/target.
self._board_targets.add(board_target)
# For each board attribute that should be queue-able, create its queue
# now. Queues are kept by the uniquified run attribute name.
for attr in RunAttributes.BOARD_ATTRS:
# Every attr in BOARD_ATTRS is in PARALLEL_ATTRS, by construction.
uniquified_attr = self._GetBoardAttrName(attr, board, target)
self._queues[uniquified_attr] = LockableQueue(self._manager)
return BoardRunAttributes(self, board, target)
# TODO(mtennant): Complain if a child process attempts to set a non-parallel
# run attribute? It could be done something like this:
# def __setattr__(self, attr, value):
# """Override __setattr__ to prevent misuse of run attributes."""
# if attr in self.REGULAR_ATTRS:
# assert not self._IsChildProcess()
# super(RunAttributes, self).__setattr__(attr, value)
def _GetBoardAttrName(self, attr, board, target):
"""Translate plain |attr| to uniquified board attribute name.
Args:
attr: Plain run attribute name.
board: Board name.
target: Build config name.
Returns:
The uniquified board-specific attribute name.
Raises:
AssertionError if the board/target combination does not exist.
"""
board_target = RunAttributes.BOARD_ATTR_SEP.join((board, target))
assert board_target in self._board_targets, \
'Unknown board/target combination: %s/%s' % (board, target)
# Translate to the unique attribute name for attr/board/target.
return RunAttributes.BOARD_ATTR_SEP.join((attr, board, target))
def SetBoardParallel(self, attr, value, board, target):
"""Set board-specific parallel run attribute value.
Args:
attr: Plain board run attribute name.
value: Value to set.
board: Board name.
target: Build config name.
"""
unique_attr = self._GetBoardAttrName(attr, board, target)
self.SetParallel(unique_attr, value)
def HasBoardParallel(self, attr, board, target):
"""Return True if board-specific parallel run attribute is known and set.
Args:
attr: Plain board run attribute name.
board: Board name.
target: Build config name.
"""
unique_attr = self._GetBoardAttrName(attr, board, target)
return self.HasParallel(unique_attr)
def SetBoardParallelDefault(self, attr, default_value, board, target):
"""Set board-specific parallel run attribute value, if not already set.
Args:
attr: Plain board run attribute name.
default_value: Value to set.
board: Board name.
target: Build config name.
"""
if not self.HasBoardParallel(attr, board, target):
self.SetBoardParallel(attr, default_value, board, target)
def GetBoardParallel(self, attr, board, target, timeout=0):
"""Get board-specific parallel run attribute value.
Args:
attr: Plain board run attribute name.
board: Board name.
target: Build config name.
timeout: See GetParallel for description.
Returns:
The value found.
"""
unique_attr = self._GetBoardAttrName(attr, board, target)
return self.GetParallel(unique_attr, timeout=timeout)
def _GetQueue(self, attr, strict=False):
"""Return the queue for the given attribute, if it exists.
Args:
attr: The run attribute name.
strict: If True, then complain if queue for |attr| is not found.
Returns:
The LockableQueue for this attribute, if it has one, or None
(assuming strict is False).
Raises:
ParallelAttributeError if no queue for this attribute is registered,
meaning no parallel attribute by this name is known.
"""
queue = self._queues.get(attr)
if queue is None and strict:
raise ParallelAttributeError(attr)
return queue
def SetParallel(self, attr, value):
"""Set the given parallel run attribute value.
Called to set the value of any parallel run attribute. The value is
saved onto a multiprocessing queue for that attribute.
Args:
attr: Name of the attribute.
value: Value to give the attribute. This value must be pickleable.
Raises:
ParallelAttributeError if attribute is not a valid parallel attribute.
AttrNotPickleableError if value cannot be pickled, meaning it cannot
go through the queue system.
"""
# Confirm that value can be pickled, because otherwise it will fail
# in the queue.
try:
pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
except pickle.PicklingError:
raise AttrNotPickleableError(attr, value)
queue = self._GetQueue(attr, strict=True)
with queue.rlock:
# First empty the queue. Any value already on the queue is now stale.
while True:
try:
queue.get(False)
except Queue.Empty:
break
queue.put(value)
def HasParallel(self, attr):
"""Return True if the given parallel run attribute is known and set.
Args:
attr: Name of the attribute.
"""
try:
queue = self._GetQueue(attr, strict=True)
with queue.rlock:
return not queue.empty()
except ParallelAttributeError:
return False
def SetParallelDefault(self, attr, default_value):
"""Set the given parallel run attribute only if it is not already set.
This leverages HasParallel and SetParallel in a convenient pattern.
Args:
attr: Name of the attribute.
default_value: Value to give the attribute if it is not set. This value
must be pickleable.
Raises:
ParallelAttributeError if attribute is not a valid parallel attribute.
AttrNotPickleableError if value cannot be pickled, meaning it cannot
go through the queue system.
"""
if not self.HasParallel(attr):
self.SetParallel(attr, default_value)
# TODO(mtennant): Add an option to log access, including the time to wait
# or waited. It could be enabled with an optional announce=False argument.
# See GetParallel helper on BoardSpecificBuilderStage class for ideas.
def GetParallel(self, attr, timeout=0):
"""Get value for the given parallel run attribute, optionally waiting.
If the given parallel run attr already has a value in the queue it will
return that value right away. Otherwise, it will wait for a value to
appear in the queue up to the timeout specified (timeout of None means
wait forever) before returning the value found or raising AttrTimeoutError
if a timeout was reached.
Args:
attr: The name of the run attribute.
timeout: Timeout, in seconds. A None value means wait forever,
which is probably never a good idea. A value of 0 does not wait at all.
Raises:
ParallelAttributeError if attribute is not set and timeout was 0.
AttrTimeoutError if timeout is greater than 0 and timeout is reached
before a value is available on the queue.
"""
got_value = False
queue = self._GetQueue(attr, strict=True)
# First attempt to get a value off the queue, without the lock. This
# allows a blocking get to wait for a value to appear.
try:
value = queue.get(True, timeout)
got_value = True
except Queue.Empty:
# This means there is nothing on the queue. Let this fall through to
# the locked code block to see if another process is in the process
# of re-queuing a value. Any process doing that will have a lock.
pass
# Now grab the queue lock and flush any other values that are on the queue.
# This should only happen if another process put a value in after our first
# queue.get above. If so, accept the updated value.
with queue.rlock:
while True:
try:
value = queue.get(False)
got_value = True
except Queue.Empty:
break
if got_value:
# First re-queue the value, then return it.
queue.put(value)
return value
else:
# Handle no value differently depending on whether timeout is 0.
if timeout == 0:
raise ParallelAttributeError(attr)
else:
raise AttrTimeoutError(attr)
class BoardRunAttributes(object):
"""Convenience class for accessing board-specific run attributes.
Board-specific run attributes (actually board/target-specific) are saved in
the RunAttributes object but under uniquified names. A BoardRunAttributes
object provides access to these attributes using their plain names by
providing the board/target information where needed.
For example, to access the breakpad_symbols_generated board run attribute on
a regular RunAttributes object requires this:
value = attrs.GetBoardParallel('breakpad_symbols_generated', board, target)
But on a BoardRunAttributes object:
boardattrs = BoardRunAttributes(attrs, board, target)
...
value = boardattrs.GetParallel('breakpad_symbols_generated')
The same goes for setting values.
"""
__slots__ = ('_attrs', '_board', '_target')
def __init__(self, attrs, board, target):
"""Initialize.
Args:
attrs: The main RunAttributes object.
board: The board name this is specific to.
target: The build config name this is specific to.
"""
self._attrs = attrs
self._board = board
self._target = target
def SetParallel(self, attr, value, *args, **kwargs):
"""Set the value of parallel board attribute |attr| to |value|.
Relay to SetBoardParallel on self._attrs, supplying board and target.
See documentation on RunAttributes.SetBoardParallel for more details.
"""
self._attrs.SetBoardParallel(attr, value, self._board, self._target,
*args, **kwargs)
def HasParallel(self, attr, *args, **kwargs):
"""Return True if parallel board attribute |attr| exists.
Relay to HasBoardParallel on self._attrs, supplying board and target.
See documentation on RunAttributes.HasBoardParallel for more details.
"""
return self._attrs.HasBoardParallel(attr, self._board, self._target,
*args, **kwargs)
def SetParallelDefault(self, attr, default_value, *args, **kwargs):
"""Set the value of parallel board attribute |attr| to |value|, if not set.
Relay to SetBoardParallelDefault on self._attrs, supplying board and target.
See documentation on RunAttributes.SetBoardParallelDefault for more details.
"""
self._attrs.SetBoardParallelDefault(attr, default_value, self._board,
self._target, *args, **kwargs)
def GetParallel(self, attr, *args, **kwargs):
"""Get the value of parallel board attribute |attr|.
Relay to GetBoardParallel on self._attrs, supplying board and target.
See documentation on RunAttributes.GetBoardParallel for more details.
"""
return self._attrs.GetBoardParallel(attr, self._board, self._target,
*args, **kwargs)
# TODO(mtennant): Consider renaming this _BuilderRunState, then renaming
# _RealBuilderRun to _BuilderRunBase.
class _BuilderRunBase(object):
"""Class to represent one run of a builder.
This class should never be instantiated directly, but instead be
instantiated as part of a BuilderRun object.
"""
# Class-level dict of RunAttributes objects to make it less
# problematic to send BuilderRun objects between processes through
# pickle. The 'attrs' attribute on a BuilderRun object will look
# up the RunAttributes for that particular BuilderRun here.
_ATTRS = {}
__slots__ = (
'site_config', # SiteConfig for this run.
'config', # BuildConfig for this run.
'options', # The cbuildbot options object for this run.
# Run attributes set/accessed by stages during the run. To add support
# for a new run attribute add it to the RunAttributes class above.
'_attrs_id', # Object ID for looking up self.attrs.
# Some pre-computed run configuration values.
'buildnumber', # The build number for this run.
'buildroot', # The build root path for this run.
'manifest_branch', # The manifest branch to build and test for this run.
# Some attributes are available as properties. In particular, attributes
# that use self.config must be determined after __init__.
# self.bot_id # Effective name of builder for this run.
)
def __init__(self, site_config, options, multiprocess_manager):
self.site_config = site_config
self.options = options
# Note that self.config is filled in dynamically by either of the classes
# that are actually instantiated: BuilderRun and ChildBuilderRun. In other
# words, self.config can be counted on anywhere except in this __init__.
# The implication is that any plain attributes that are calculated from
# self.config contents must be provided as properties (or methods).
# See the _RealBuilderRun class and its __getattr__ method for details.
self.config = None
# Create the RunAttributes object for this BuilderRun and save
# the id number for it in order to look it up via attrs property.
attrs = RunAttributes(multiprocess_manager)
self._ATTRS[id(attrs)] = attrs
self._attrs_id = id(attrs)
# Fill in values for all pre-computed "run configs" now, which are frozen
# by this time.
# TODO(mtennant): Should this use os.path.abspath like builderstage does?
self.buildroot = self.options.buildroot
self.buildnumber = self.options.buildnumber
self.manifest_branch = self.options.branch
# The __slots__ logic above confuses pylint.
# https://bitbucket.org/logilab/pylint/issue/380/
# pylint: disable=assigning-non-slot
# Certain run attributes have sensible defaults which can be set here.
# This allows all code to safely assume that the run attribute exists.
attrs.chrome_version = None
attrs.metadata = metadata_lib.CBuildbotMetadata(
multiprocess_manager=multiprocess_manager)
@property
def bot_id(self):
"""Return the bot_id for this run."""
return self.config.name
@property
def attrs(self):
"""Look up the RunAttributes object for this BuilderRun object."""
return self._ATTRS[self._attrs_id]
def IsToTBuild(self):
"""Returns True if Builder is running on ToT."""
return self.manifest_branch == 'master'
def GetArchive(self):
"""Create an Archive object for this BuilderRun object."""
# The Archive class is very lightweight, and is read-only, so it
# is ok to generate a new one on demand. This also avoids worrying
# about whether it can go through pickle.
# Almost everything the Archive class does requires GetVersion(),
# which means it cannot be used until the version has been settled on.
# However, because it does have some use before then we provide
# the GetVersion function itself to be called when needed later.
return archive_lib.Archive(self.bot_id, self.GetVersion, self.options,
self.config)
def GetBoardRunAttrs(self, board):
"""Create a BoardRunAttributes object for this run and given |board|."""
return BoardRunAttributes(self.attrs, board, self.config.name)
def GetBuilderName(self):
"""Get the name of this builder on the current waterfall."""
return os.environ.get('BUILDBOT_BUILDERNAME', self.config.name)
def ConstructDashboardURL(self, stage=None):
"""Return the dashboard URL
This is the direct link to logdog logs if given a stage, or the link to the
legoland build page for the build.
Args:
stage: Link to a specific |stage|, otherwise the general buildbot log
Returns:
The fully formed URL
"""
if stage:
return uri_lib.ConstructLogDogUri(self.options.buildnumber, stage)
else:
return uri_lib.ConstructMiloBuildUri(self.options.buildbucket_id)
def ShouldBuildAutotest(self):
"""Return True if this run should build autotest and artifacts."""
return self.options.tests
def ShouldUploadPrebuilts(self):
"""Return True if this run should upload prebuilts."""
return self.options.prebuilts and self.config.prebuilts
def GetCIDBHandle(self):
"""Get the build_identifier and cidb handle, if available.
Returns:
A (BuildIdentifier, CIDBConnection) tuple if cidb is set up and
a build_id is known in metadata. Otherwise,
(BuildIdentifier(None, None), None).
"""
try:
build_id = self.attrs.metadata.GetValue('build_id')
buildbucket_id = self.options.buildbucket_id
build_identifier = buildstore.BuildIdentifier(
cidb_id=build_id, buildbucket_id=buildbucket_id)
except KeyError:
return (buildstore.BuildIdentifier(None, None), None)
except AttributeError:
return (buildstore.BuildIdentifier(None, None), None)
if not cidb.CIDBConnectionFactory.IsCIDBSetup():
return (buildstore.BuildIdentifier(None, None), None)
cidb_handle = cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()
if cidb_handle:
return (build_identifier, cidb_handle)
else:
return (buildstore.BuildIdentifier(None, None), None)
def ShouldReexecAfterSync(self):
"""Return True if this run should re-exec itself after sync stage."""
return (self.options.postsync_reexec and self.config.postsync_reexec and
not self.options.resume)
def ShouldPatchAfterSync(self):
"""Return True if this run should patch changes after sync stage."""
return self.options.postsync_patch and self.config.postsync_patch
def InProduction(self):
"""Return True if this is a production run."""
return cidb.CIDBConnectionFactory.GetCIDBConnectionType() == 'prod'
def InEmailReportingEnvironment(self):
"""Return True if this run should send reporting emails.."""
return self.InProduction()
def GetVersionInfo(self):
"""Helper for picking apart various version bits.
The Builder must set attrs.version_info before calling this. Further, it
should do so only after the sources have been fully synced & patched, else
it could return a confusing value.
Returns:
A manifest_version.VersionInfo object.
Raises:
VersionNotSetError if the version has not yet been set.
"""
if not hasattr(self.attrs, 'version_info'):
raise VersionNotSetError('builder must call SetVersionInfo first')
return self.attrs.version_info
def GetVersion(self, include_chrome=True):
"""Calculate full R<chrome_version>-<chromeos_version> version string.
See GetVersionInfo() notes about runtime usage.
Args:
include_chrome: Whether to include the Chrome version.
Returns:
The version string for this run.
"""
verinfo = self.GetVersionInfo()
release_tag = self.attrs.release_tag
# Use a default of zero, in case we are a local tryjob or other build
# without a CIDB id.
build_id = self.attrs.metadata.GetValueWithDefault('build_id', 0)
calc_version = ''
if include_chrome:
calc_version += 'R%s-' % (verinfo.chrome_branch,)
if release_tag:
calc_version += release_tag
else:
# Non-versioned builds need the build number to uniquify the image.
calc_version += '%s-b%s' % (verinfo.VersionString(), build_id)
return calc_version
def HasUseFlag(self, board, use_flag):
"""Return the state of a USE flag for a board as a boolean."""
return use_flag in portage_util.GetBoardUseFlags(board)
def DetermineAndroidBranch(self, board):
"""Returns the Android branch in use by the active container ebuild."""
try:
android_package = self.DetermineAndroidPackage(board)
except cros_build_lib.RunCommandError:
raise NoAndroidBranchError(
'Android branch could not be determined for %s' % board)
if not android_package:
raise NoAndroidBranchError(
'Android branch could not be determined for %s (no package?)' % board)
ebuild_path = portage_util.FindEbuildForBoardPackage(android_package, board)
host_ebuild_path = path_util.FromChrootPath(ebuild_path)
# We assume all targets pull from the same branch and that we always
# have an ARM_TARGET, ARM_USERDEBUG_TARGET, or an X86_USERDEBUG_TARGET.
targets = ['ARM_TARGET', 'ARM_USERDEBUG_TARGET', 'X86_USERDEBUG_TARGET']
ebuild_content = osutils.SourceEnvironment(host_ebuild_path, targets)
for target in targets:
if target in ebuild_content:
branch = re.search(r'(.*?)-linux-', ebuild_content[target])
if branch is not None:
return branch.group(1)
raise NoAndroidBranchError(
'Android branch could not be determined for %s (ebuild empty?)' % board)
def DetermineAndroidABI(self, board):
"""Returns the Android ABI in use by the active container ebuild."""
use_flags = portage_util.GetInstalledPackageUseFlags(
'sys-devel/arc-build', board)
if 'abi_x86_64' in use_flags.get('sys-devel/arc-build', []):
return 'x86_64'
elif 'abi_x86_32' in use_flags.get('sys-devel/arc-build', []):
return 'x86'
else:
# ARM only supports 32-bit so it does not have abi_x86_{32,64} set. But it
# is also the last possible ABI, so returning by default.
return 'arm'
def DetermineAndroidVariant(self, board):
"""Returns the Android variant in use by the active container ebuild."""
try:
android_package = self.DetermineAndroidPackage(board)
except cros_build_lib.RunCommandError as rce:
raise NoAndroidVariantError(
'Android Variant could not be determined for %s; original error: %s' %
(board, rce))
if not android_package:
raise NoAndroidVariantError(
'Android Variant could not be determined for %s (no package?)' %
board)
all_use_flags = portage_util.GetInstalledPackageUseFlags(
android_package, board)
for use_flags in all_use_flags.values():
for use_flag in use_flags:
if 'cheets_userdebug' in use_flag or 'cheets_sdk_userdebug' in use_flag:
return 'userdebug'
elif 'cheets_user' in use_flag or 'cheets_sdk_user' in use_flag:
# TODO(b/120999609): bertha builds always download userdebug builds
# at the moment because user builds are broken. Remove this clause
# when resolved.
if self.DetermineAndroidTarget(board) == 'bertha':
return 'userdebug'
return 'user'
# We iterated through all the flags and could not find user or userdebug.
# This should not be possible given that this code is only ran by
# builders, which will never use local images.
raise NoAndroidVariantError(
'Android Variant cannot be deteremined for the package: %s' %
android_package)
def DetermineAndroidTarget(self, board):
try:
android_package = self.DetermineAndroidPackage(board)
except cros_build_lib.RunCommandError:
raise NoAndroidTargetError(
'Android Target could not be determined for %s' % board)
if not android_package:
raise NoAndroidTargetError(
'Android Target could not be determined for %s (no package?)' %
board)
if android_package.startswith('chromeos-base/android-vm-'):
return 'bertha'
elif android_package.startswith('chromeos-base/android-container-'):
return 'cheets'
raise NoAndroidTargetError(
'Android Target cannot be determined for the package: %s' %
android_package)
def DetermineAndroidPackage(self, board):
"""Returns the active Android container package in use by the board."""
packages = portage_util.GetPackageDependencies(board, 'virtual/target-os')
# We assume there is only one Android package in the depgraph.
for package in packages:
if package.startswith('chromeos-base/android-container-') or \
package.startswith('chromeos-base/android-vm-'):
return package
return None
def DetermineAndroidVersion(self, boards=None):
"""Determine the current Android version in buildroot now and return it.
This uses the typical portage logic to determine which version of Android
is active right now in the buildroot.
Args:
boards: List of boards to check version of.
Returns:
The Android build ID of the container for the boards.
Raises:
NoAndroidVersionError: if no unique Android version can be determined.
"""
if not boards:
return None
# Verify that all boards have the same version.
version = None
for board in boards:
package = self.DetermineAndroidPackage(board)
if not package:
raise NoAndroidVersionError(
'Android version could not be determined for %s' % boards)
cpv = portage_util.SplitCPV(package)
if not cpv:
raise NoAndroidVersionError(
'Android version could not be determined for %s' % board)
if not version:
version = cpv.version_no_rev
elif version != cpv.version_no_rev:
raise NoAndroidVersionError(
'Different Android versions (%s vs %s) for %s' %
(version, cpv.version_no_rev, boards))
return version
def DetermineChromeVersion(self):
"""Determine the current Chrome version in buildroot now and return it.
This uses the typical portage logic to determine which version of Chrome
is active right now in the buildroot.
Returns:
The new value of attrs.chrome_version (e.g. "35.0.1863.0").
"""
cpv = portage_util.PortageqBestVisible(constants.CHROME_CP,
cwd=self.buildroot)
return cpv.version_no_rev.partition('_')[0]
class _RealBuilderRun(object):
"""Base BuilderRun class that manages self.config access.
For any builder run, sometimes the build config is the top-level config and
sometimes it is a "child" config. In either case, the config to use should
override self.config for all cases. This class provides a mechanism for
overriding self.config access generally.
Also, methods that do more than access state for a BuilderRun should
live here. In particular, any method that uses 'self' as an object
directly should be here rather than _BuilderRunBase.
"""
__slots__ = _BuilderRunBase.__slots__ + (
'_run_base', # The _BuilderRunBase object where most functionality is.
'_config', # BuildConfig to use for dynamically overriding self.config.
)
def __init__(self, run_base, build_config):
"""_RealBuilderRun constructor.
Args:
run_base: _BuilderRunBase object.
build_config: BuildConfig object.
"""
self._run_base = run_base
self._config = build_config
# Make sure self.attrs has board-specific attributes for each board
# in build_config.
for board in build_config.boards:
self.attrs.RegisterBoardAttrs(board, build_config.name)
def __getattr__(self, attr):
# Remember, __getattr__ only called if attribute was not found normally.
# In normal usage, the __init__ guarantees that self._run_base and
# self._config will be present. However, the unpickle process bypasses
# __init__, and this object must be pickle-able. That is why we access
# self._run_base and self._config through __getattribute__ here, otherwise
# unpickling results in infinite recursion.
# TODO(mtennant): Revisit this if pickling support is changed to go through
# the __init__ method, such as by supplying __reduce__ method.
run_base = self.__getattribute__('_run_base')
config = self.__getattribute__('_config')
# TODO(akeshet): This logic seems to have a subtle flaky bug that only
# manifests itself when using unit tests with ParallelMock. As a workaround,
# we have simply eliminiated ParallelMock from the affected tests. See
# crbug.com/470907 for context.
try:
# run_base.config should always be None except when accessed through
# this routine. Override the value here, then undo later.
run_base.config = config
result = getattr(run_base, attr)
if isinstance(result, types.MethodType):
# Make sure run_base.config is also managed when the method is called.
@functools.wraps(result)
def FuncWrapper(*args, **kwargs):
run_base.config = config
try:
return result(*args, **kwargs)
finally:
run_base.config = None
# TODO(mtennant): Find a way to make the following actually work. It
# makes pickling more complicated, unfortunately.
# Cache this function wrapper to re-use next time without going through
# __getattr__ again. This ensures that the same wrapper object is used
# each time, which is nice for identity and equality checks. Subtle
# gotcha that we accept: if the function itself on run_base is replaced
# then this will continue to provide the behavior of the previous one.
# setattr(self, attr, FuncWrapper)
return FuncWrapper
else:
return result
finally:
run_base.config = None
def GetChildren(self):
"""Get ChildBuilderRun objects for child configs, if they exist.
Returns:
List of ChildBuilderRun objects if self.config has child_configs. []
otherwise.
"""
# If there are child configs, construct a list of ChildBuilderRun objects
# for those child configs and return that.
return [ChildBuilderRun(self, ix)
for ix in range(len(self.config.child_configs))]
def GetUngroupedBuilderRuns(self):
"""Same as GetChildren, but defaults to [self] if no children exist.
Returns:
Result of self.GetChildren, if children exist, otherwise [self].
"""
return self.GetChildren() or [self]
def GetBuilderIds(self):
"""Return a list of builder names for this config and the child configs."""
bot_ids = [self.config.name]
for config in self.config.child_configs:
if config.name:
bot_ids.append(config.name)
return bot_ids
class BuilderRun(_RealBuilderRun):
"""A standard BuilderRun for a top-level build config."""
def __init__(self, options, site_config, build_config, multiprocess_manager):
"""Initialize.
Args:
options: Command line options from this cbuildbot run.
site_config: Site config for this cbuildbot run.
build_config: Build config for this cbuildbot run.
multiprocess_manager: A multiprocessing.Manager.
"""
run_base = _BuilderRunBase(site_config, options, multiprocess_manager)
super(BuilderRun, self).__init__(run_base, build_config)
class ChildBuilderRun(_RealBuilderRun):
"""A BuilderRun for a "child" build config."""
def __init__(self, builder_run, child_index):
"""Initialize.
Args:
builder_run: BuilderRun for the parent (main) cbuildbot run. Extract
the _BuilderRunBase from it to make sure the same base is used for
both the main cbuildbot run and any child runs.
child_index: The child index of this child run, used to index into
the main run's config.child_configs.
"""
# pylint: disable=protected-access
run_base = builder_run._run_base
config = builder_run.config.child_configs[child_index]
super(ChildBuilderRun, self).__init__(run_base, config)