blob: 7821ea68f4dab4ee0a8b0f0a25cdaf3453f989d3 [file] [log] [blame]
# 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.
"""
import functools
import os
import types
from chromite.buildbot import cbuildbot_archive
from chromite.buildbot import manifest_version
class RunAttributes(object):
"""Hold all run attributes for a particular builder run."""
# A word of warning about the way run attributes currently work. The
# value set (or altered) by a given stage can be seen by any subsequent
# stage, including stages on child processes. Any value set (or
# altered) by a stage in a subprocess will not be seen by co-subprocesses
# (other stages being run in parallel at the same time) and will
# be *discarded* when that subprocess ends (and parent process resumes).
# This is expected behavior for forked processes.
# TODO(mtennant): Possibly design a mechanism to preserve run attribute
# changes in subprocess stages for subsequent stages (but probably not for
# co-subprocess stages).
__slots__ = (
'chrome_version', # Set by SyncChromeStage, if it runs.
'manifest_manager', # Set by ManifestVersionedSyncStage.
'release_tag', # Set by cbuildbot after sync stage.
)
# 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__ = (
'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.
'debug', # Boolean, represents "dry run" concept, really.
'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.
# TODO(mtennant): Other candidates here include:
# trybot, buildbot, remote_trybot, chrome_root, chrome_version,
# test = (config build_tests AND option tests)
)
def __init__(self, options):
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()
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
# For remote_trybot runs, options.debug is implied, but we want true dryrun
# mode only if --debug was actually specified (i.e. options.debug_forced).
# TODO(mtennant): Get rid of confusing debug and debug_forced, if at all
# possible. Also, eventually use "dry_run" and "verbose" options instead to
# represent two distinct concepts.
self.debug = self.options.debug
if self.options.remote_trybot:
self.debug = self.options.debug_forced
# 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
@property
def bot_id(self):
"""Return the bot_id for this run."""
return self.config.GetBotId(remote_trybot=self.options.remote_trybot)
@property
def attrs(self):
"""Look up the RunAttributes object for this BuilderRun object."""
return self._ATTRS[self._attrs_id]
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 cbuildbot_archive.Archive(self.bot_id, self.GetVersion,
self.options, self.config)
def ShouldUploadPrebuilts(self):
"""Return True if this run should upload prebuilts."""
return self.options.prebuilts and self.config.prebuilts
def ShouldReexecAfterSync(self):
"""Return True if this run should re-exec itself after sync stage."""
if self.options.postsync_reexec and self.config.postsync_reexec:
# Return True if this source is not in designated buildroot.
abs_buildroot = os.path.abspath(self.buildroot)
return not os.path.abspath(__file__).startswith(abs_buildroot)
return False
def ShouldPatchAfterSync(self):
"""Return True if this run should patch changes after sync stage."""
return self.options.postsync_patch and self.config.postsync_patch
@classmethod
def GetVersionInfo(cls, buildroot):
"""Helper for picking apart various version bits.
This method only exists so that tests can override it.
"""
return manifest_version.VersionInfo.from_repo(buildroot)
def GetVersion(self):
"""Calculate full R<chrome_version>-<chromeos_version> version string.
It is required that the sync stage be run before this method is called.
Returns:
The version string for this run.
Raises:
AssertionError if the sync stage has not been run first.
"""
# This method should never be called before the sync stage has run, or
# it would return a confusing value.
assert hasattr(self.attrs, 'release_tag'), 'Sync stage must run first.'
verinfo = self.GetVersionInfo(self.buildroot)
release_tag = self.attrs.release_tag
if release_tag:
calc_version = 'R%s-%s' % (verinfo.chrome_branch, release_tag)
else:
# Non-versioned builds need the build number to uniquify the image.
calc_version = 'R%s-%s-b%s' % (verinfo.chrome_branch,
verinfo.VersionString(),
self.buildnumber)
return calc_version
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', # Config to use for dynamically overriding self.config.
)
def __init__(self, run_base, build_config):
self._run_base = run_base
self._config = build_config
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')
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. None
otherwise.
"""
# If there are child configs, construct a list of ChildBuilderRun objects
# for those child configs and return that.
if self.config.child_configs:
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]
class BuilderRun(_RealBuilderRun):
"""A standard BuilderRun for a top-level build config."""
def __init__(self, options, build_config):
"""Initialize.
Args:
options: Command line options from this cbuildbot run.
build_config: Build config for this cbuildbot run.
"""
run_base = _BuilderRunBase(options)
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=W0212
run_base = builder_run._run_base
config = builder_run.config.child_configs[child_index]
super(ChildBuilderRun, self).__init__(run_base, config)