| Index: third_party/recipe_engine/main.py
|
| diff --git a/third_party/recipe_engine/main.py b/third_party/recipe_engine/main.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..97d0fb5c092bce0e824ca8f7f4ab896e1f8e49ad
|
| --- /dev/null
|
| +++ b/third_party/recipe_engine/main.py
|
| @@ -0,0 +1,939 @@
|
| +# Copyright (c) 2013-2015 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Entry point for fully-annotated builds.
|
| +
|
| +This script is part of the effort to move all builds to annotator-based
|
| +systems. Any builder configured to use the AnnotatorFactory.BaseFactory()
|
| +found in scripts/master/factory/annotator_factory.py executes a single
|
| +AddAnnotatedScript step. That step (found in annotator_commands.py) calls
|
| +this script with the build- and factory-properties passed on the command
|
| +line.
|
| +
|
| +The main mode of operation is for factory_properties to contain a single
|
| +property 'recipe' whose value is the basename (without extension) of a python
|
| +script in one of the following locations (looked up in this order):
|
| + * build_internal/scripts/slave-internal/recipes
|
| + * build_internal/scripts/slave/recipes
|
| + * build/scripts/slave/recipes
|
| +
|
| +For example, these factory_properties would run the 'run_presubmit' recipe
|
| +located in build/scripts/slave/recipes:
|
| + { 'recipe': 'run_presubmit' }
|
| +
|
| +TODO(vadimsh, iannucci, luqui): The following docs are very outdated.
|
| +
|
| +Annotated_run.py will then import the recipe and expect to call a function whose
|
| +signature is:
|
| + RunSteps(api, properties) -> None.
|
| +
|
| +properties is a merged view of factory_properties with build_properties.
|
| +
|
| +Items in iterable_of_things must be one of:
|
| + * A step dictionary (as accepted by annotator.py)
|
| + * A sequence of step dictionaries
|
| + * A step generator
|
| +Iterable_of_things is also permitted to be a raw step generator.
|
| +
|
| +A step generator is called with the following protocol:
|
| + * The generator is initialized with 'step_history' and 'failed'.
|
| + * Each iteration of the generator is passed the current value of 'failed'.
|
| +
|
| +On each iteration, a step generator may yield:
|
| + * A single step dictionary
|
| + * A sequence of step dictionaries
|
| + * If a sequence of dictionaries is yielded, and the first step dictionary
|
| + does not have a 'seed_steps' key, the first step will be augmented with
|
| + a 'seed_steps' key containing the names of all the steps in the sequence.
|
| +
|
| +For steps yielded by the generator, if annotated_run enters the failed state,
|
| +it will only continue to call the generator if the generator sets the
|
| +'keep_going' key on the steps which it has produced. Otherwise annotated_run
|
| +will cease calling the generator and move on to the next item in
|
| +iterable_of_things.
|
| +
|
| +'step_history' is an OrderedDict of {stepname -> StepData}, always representing
|
| + the current history of what steps have run, what they returned, and any
|
| + json data they emitted. Additionally, the OrderedDict has the following
|
| + convenience functions defined:
|
| + * last_step - Returns the last step that ran or None
|
| + * nth_step(n) - Returns the N'th step that ran or None
|
| +
|
| +'failed' is a boolean representing if the build is in a 'failed' state.
|
| +"""
|
| +
|
| +import collections
|
| +import contextlib
|
| +import copy
|
| +import functools
|
| +import json
|
| +import os
|
| +import subprocess
|
| +import sys
|
| +import threading
|
| +import traceback
|
| +
|
| +import cStringIO
|
| +
|
| +
|
| +from . import loader
|
| +from . import recipe_api
|
| +from . import recipe_test_api
|
| +from . import util
|
| +
|
| +
|
| +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
|
| +
|
| +BUILDBOT_MAGIC_ENV = set([
|
| + 'BUILDBOT_BLAMELIST',
|
| + 'BUILDBOT_BRANCH',
|
| + 'BUILDBOT_BUILDBOTURL',
|
| + 'BUILDBOT_BUILDERNAME',
|
| + 'BUILDBOT_BUILDNUMBER',
|
| + 'BUILDBOT_CLOBBER',
|
| + 'BUILDBOT_GOT_REVISION',
|
| + 'BUILDBOT_MASTERNAME',
|
| + 'BUILDBOT_REVISION',
|
| + 'BUILDBOT_SCHEDULER',
|
| + 'BUILDBOT_SLAVENAME',
|
| +])
|
| +
|
| +ENV_WHITELIST_WIN = BUILDBOT_MAGIC_ENV | set([
|
| + 'APPDATA',
|
| + 'AWS_CREDENTIAL_FILE',
|
| + 'BOTO_CONFIG',
|
| + 'BUILDBOT_ARCHIVE_FORCE_SSH',
|
| + 'CHROME_HEADLESS',
|
| + 'CHROMIUM_BUILD',
|
| + 'COMMONPROGRAMFILES',
|
| + 'COMMONPROGRAMFILES(X86)',
|
| + 'COMMONPROGRAMW6432',
|
| + 'COMSPEC',
|
| + 'COMPUTERNAME',
|
| + 'DBUS_SESSION_BUS_ADDRESS',
|
| + 'DEPOT_TOOLS_GIT_BLEEDING',
|
| + # TODO(maruel): Remove once everyone is on 2.7.5.
|
| + 'DEPOT_TOOLS_PYTHON_275',
|
| + 'DXSDK_DIR',
|
| + 'GIT_USER_AGENT',
|
| + 'HOME',
|
| + 'HOMEDRIVE',
|
| + 'HOMEPATH',
|
| + 'LOCALAPPDATA',
|
| + 'NUMBER_OF_PROCESSORS',
|
| + 'OS',
|
| + 'PATH',
|
| + 'PATHEXT',
|
| + 'PROCESSOR_ARCHITECTURE',
|
| + 'PROCESSOR_ARCHITEW6432',
|
| + 'PROCESSOR_IDENTIFIER',
|
| + 'PROGRAMFILES',
|
| + 'PROGRAMW6432',
|
| + 'PWD',
|
| + 'PYTHONPATH',
|
| + 'SYSTEMDRIVE',
|
| + 'SYSTEMROOT',
|
| + 'TEMP',
|
| + 'TESTING_MASTER',
|
| + 'TESTING_MASTER_HOST',
|
| + 'TESTING_SLAVENAME',
|
| + 'TMP',
|
| + 'USERNAME',
|
| + 'USERDOMAIN',
|
| + 'USERPROFILE',
|
| + 'VS100COMNTOOLS',
|
| + 'VS110COMNTOOLS',
|
| + 'WINDIR',
|
| +])
|
| +
|
| +ENV_WHITELIST_POSIX = BUILDBOT_MAGIC_ENV | set([
|
| + 'AWS_CREDENTIAL_FILE',
|
| + 'BOTO_CONFIG',
|
| + 'CCACHE_DIR',
|
| + 'CHROME_ALLOCATOR',
|
| + 'CHROME_HEADLESS',
|
| + 'CHROME_VALGRIND_NUMCPUS',
|
| + 'DISPLAY',
|
| + 'DISTCC_DIR',
|
| + 'GIT_USER_AGENT',
|
| + 'HOME',
|
| + 'HOSTNAME',
|
| + 'HTTP_PROXY',
|
| + 'http_proxy',
|
| + 'HTTPS_PROXY',
|
| + 'LANG',
|
| + 'LOGNAME',
|
| + 'PAGER',
|
| + 'PATH',
|
| + 'PWD',
|
| + 'PYTHONPATH',
|
| + 'SHELL',
|
| + 'SSH_AGENT_PID',
|
| + 'SSH_AUTH_SOCK',
|
| + 'SSH_CLIENT',
|
| + 'SSH_CONNECTION',
|
| + 'SSH_TTY',
|
| + 'TESTING_MASTER',
|
| + 'TESTING_MASTER_HOST',
|
| + 'TESTING_SLAVENAME',
|
| + 'USER',
|
| + 'USERNAME',
|
| +])
|
| +
|
| +
|
| +def _isolate_environment():
|
| + """Isolate the environment to a known subset set."""
|
| + if sys.platform.startswith('win'):
|
| + whitelist = ENV_WHITELIST_WIN
|
| + elif sys.platform in ('darwin', 'posix', 'linux2'):
|
| + whitelist = ENV_WHITELIST_POSIX
|
| + else:
|
| + print ('WARNING: unknown platform %s, not isolating environment.' %
|
| + sys.platform)
|
| + return
|
| +
|
| + for k in os.environ.keys():
|
| + if k not in whitelist:
|
| + del os.environ[k]
|
| +
|
| +
|
| +class StepPresentation(object):
|
| + STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION'))
|
| +
|
| + def __init__(self):
|
| + self._finalized = False
|
| +
|
| + self._logs = collections.OrderedDict()
|
| + self._links = collections.OrderedDict()
|
| + self._perf_logs = collections.OrderedDict()
|
| + self._status = None
|
| + self._step_summary_text = ''
|
| + self._step_text = ''
|
| + self._properties = {}
|
| +
|
| + # (E0202) pylint bug: http://www.logilab.org/ticket/89092
|
| + @property
|
| + def status(self): # pylint: disable=E0202
|
| + return self._status
|
| +
|
| + @status.setter
|
| + def status(self, val): # pylint: disable=E0202
|
| + assert not self._finalized
|
| + assert val in self.STATUSES
|
| + self._status = val
|
| +
|
| + @property
|
| + def step_text(self):
|
| + return self._step_text
|
| +
|
| + @step_text.setter
|
| + def step_text(self, val):
|
| + assert not self._finalized
|
| + self._step_text = val
|
| +
|
| + @property
|
| + def step_summary_text(self):
|
| + return self._step_summary_text
|
| +
|
| + @step_summary_text.setter
|
| + def step_summary_text(self, val):
|
| + assert not self._finalized
|
| + self._step_summary_text = val
|
| +
|
| + @property
|
| + def logs(self):
|
| + if not self._finalized:
|
| + return self._logs
|
| + else:
|
| + return copy.deepcopy(self._logs)
|
| +
|
| + @property
|
| + def links(self):
|
| + if not self._finalized:
|
| + return self._links
|
| + else:
|
| + return copy.deepcopy(self._links)
|
| +
|
| + @property
|
| + def perf_logs(self):
|
| + if not self._finalized:
|
| + return self._perf_logs
|
| + else:
|
| + return copy.deepcopy(self._perf_logs)
|
| +
|
| + @property
|
| + def properties(self): # pylint: disable=E0202
|
| + if not self._finalized:
|
| + return self._properties
|
| + else:
|
| + return copy.deepcopy(self._properties)
|
| +
|
| + @properties.setter
|
| + def properties(self, val): # pylint: disable=E0202
|
| + assert not self._finalized
|
| + assert isinstance(val, dict)
|
| + self._properties = val
|
| +
|
| + def finalize(self, annotator_step):
|
| + self._finalized = True
|
| + if self.step_text:
|
| + annotator_step.step_text(self.step_text)
|
| + if self.step_summary_text:
|
| + annotator_step.step_summary_text(self.step_summary_text)
|
| + for name, lines in self.logs.iteritems():
|
| + annotator_step.write_log_lines(name, lines)
|
| + for name, lines in self.perf_logs.iteritems():
|
| + annotator_step.write_log_lines(name, lines, perf=True)
|
| + for label, url in self.links.iteritems():
|
| + annotator_step.step_link(label, url)
|
| + status_mapping = {
|
| + 'WARNING': annotator_step.step_warnings,
|
| + 'FAILURE': annotator_step.step_failure,
|
| + 'EXCEPTION': annotator_step.step_exception,
|
| + }
|
| + status_mapping.get(self.status, lambda: None)()
|
| + for key, value in self._properties.iteritems():
|
| + annotator_step.set_build_property(key, json.dumps(value, sort_keys=True))
|
| +
|
| +
|
| +class StepDataAttributeError(AttributeError):
|
| + """Raised when a non-existent attributed is accessed on a StepData object."""
|
| + def __init__(self, step, attr):
|
| + self.step = step
|
| + self.attr = attr
|
| + message = ('The recipe attempted to access missing step data "%s" for step '
|
| + '"%s". Please examine that step for errors.' % (attr, step))
|
| + super(StepDataAttributeError, self).__init__(message)
|
| +
|
| +
|
| +class StepData(object):
|
| + def __init__(self, step, retcode):
|
| + self._retcode = retcode
|
| + self._step = step
|
| +
|
| + self._presentation = StepPresentation()
|
| + self.abort_reason = None
|
| +
|
| + @property
|
| + def step(self):
|
| + return copy.deepcopy(self._step)
|
| +
|
| + @property
|
| + def retcode(self):
|
| + return self._retcode
|
| +
|
| + @property
|
| + def presentation(self):
|
| + return self._presentation
|
| +
|
| + def __getattr__(self, name):
|
| + raise StepDataAttributeError(self._step['name'], name)
|
| +
|
| +
|
| +# TODO(martiniss) update comment
|
| +# Result of 'render_step', fed into 'step_callback'.
|
| +Placeholders = collections.namedtuple(
|
| + 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin'])
|
| +
|
| +
|
| +def render_step(step, step_test):
|
| + """Renders a step so that it can be fed to annotator.py.
|
| +
|
| + Args:
|
| + step_test: The test data json dictionary for this step, if any.
|
| + Passed through unaltered to each placeholder.
|
| +
|
| + Returns any placeholder instances that were found while rendering the step.
|
| + """
|
| + # Process 'cmd', rendering placeholders there.
|
| + placeholders = collections.defaultdict(lambda: collections.defaultdict(list))
|
| + new_cmd = []
|
| + for item in step.get('cmd', []):
|
| + if isinstance(item, util.Placeholder):
|
| + module_name, placeholder_name = item.name_pieces
|
| + tdata = step_test.pop_placeholder(item.name_pieces)
|
| + new_cmd.extend(item.render(tdata))
|
| + placeholders[module_name][placeholder_name].append((item, tdata))
|
| + else:
|
| + new_cmd.append(item)
|
| + step['cmd'] = new_cmd
|
| +
|
| + # Process 'stdout', 'stderr' and 'stdin' placeholders, if given.
|
| + stdio_placeholders = {}
|
| + for key in ('stdout', 'stderr', 'stdin'):
|
| + placeholder = step.get(key)
|
| + tdata = None
|
| + if placeholder:
|
| + assert isinstance(placeholder, util.Placeholder), key
|
| + tdata = getattr(step_test, key)
|
| + placeholder.render(tdata)
|
| + assert placeholder.backing_file
|
| + step[key] = placeholder.backing_file
|
| + stdio_placeholders[key] = (placeholder, tdata)
|
| +
|
| + return Placeholders(cmd=placeholders, **stdio_placeholders)
|
| +
|
| +
|
| +def get_placeholder_results(step_result, placeholders):
|
| + class BlankObject(object):
|
| + pass
|
| +
|
| + # Placeholders inside step |cmd|.
|
| + for module_name, pholders in placeholders.cmd.iteritems():
|
| + assert not hasattr(step_result, module_name)
|
| + o = BlankObject()
|
| + setattr(step_result, module_name, o)
|
| +
|
| + for placeholder_name, items in pholders.iteritems():
|
| + lst = [ph.result(step_result.presentation, td) for ph, td in items]
|
| + setattr(o, placeholder_name+"_all", lst)
|
| + setattr(o, placeholder_name, lst[0])
|
| +
|
| + # Placeholders that are used with IO redirection.
|
| + for key in ('stdout', 'stderr', 'stdin'):
|
| + assert not hasattr(step_result, key)
|
| + ph, td = getattr(placeholders, key)
|
| + result = ph.result(step_result.presentation, td) if ph else None
|
| + setattr(step_result, key, result)
|
| +
|
| +
|
| +def get_callable_name(func):
|
| + """Returns __name__ of a callable, handling functools.partial types."""
|
| + if isinstance(func, functools.partial):
|
| + return get_callable_name(func.func)
|
| + else:
|
| + return func.__name__
|
| +
|
| +
|
| +# Return value of run_steps and RecipeEngine.run.
|
| +RecipeExecutionResult = collections.namedtuple(
|
| + 'RecipeExecutionResult', 'status_code steps_ran')
|
| +
|
| +
|
| +def run_steps(properties,
|
| + stream,
|
| + universe,
|
| + test_data=recipe_test_api.DisabledTestData()):
|
| + """Returns a tuple of (status_code, steps_ran).
|
| +
|
| + Only one of these values will be set at a time. This is mainly to support the
|
| + testing interface used by unittests/recipes_test.py.
|
| + """
|
| + stream.honor_zero_return_code()
|
| +
|
| + # TODO(iannucci): Stop this when blamelist becomes sane data.
|
| + if ('blamelist_real' in properties and
|
| + 'blamelist' in properties):
|
| + properties['blamelist'] = properties['blamelist_real']
|
| + del properties['blamelist_real']
|
| +
|
| + # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by
|
| + # 'patch_project'. 'root' had Rietveld knowing about the implementation of
|
| + # the builders. 'patch_project' lets the builder (recipe) decide its own
|
| + # destiny.
|
| + properties.pop('root', None)
|
| +
|
| + # TODO(iannucci): A much better way to do this would be to dynamically
|
| + # detect if the mirrors are actually available during the execution of the
|
| + # recipe.
|
| + if ('use_mirror' not in properties and (
|
| + 'TESTING_MASTERNAME' in os.environ or
|
| + 'TESTING_SLAVENAME' in os.environ)):
|
| + properties['use_mirror'] = False
|
| +
|
| + engine = RecipeEngine(stream, properties, test_data)
|
| +
|
| + # Create all API modules and top level RunSteps function. It doesn't launch
|
| + # any recipe code yet; RunSteps needs to be called.
|
| + api = None
|
| + with stream.step('setup_build') as s:
|
| + assert 'recipe' in properties # Should be ensured by get_recipe_properties.
|
| + recipe = properties['recipe']
|
| +
|
| + properties_to_print = properties.copy()
|
| + if 'use_mirror' in properties:
|
| + del properties_to_print['use_mirror']
|
| +
|
| + run_recipe_help_lines = [
|
| + 'To repro this locally, run the following line from a build checkout:',
|
| + '',
|
| + './scripts/tools/run_recipe.py %s --properties-file - <<EOF' % recipe,
|
| + repr(properties_to_print),
|
| + 'EOF',
|
| + '',
|
| + 'To run on Windows, you can put the JSON in a file and redirect the',
|
| + 'contents of the file into run_recipe.py, with the < operator.',
|
| + ]
|
| +
|
| + for line in run_recipe_help_lines:
|
| + s.step_log_line('run_recipe', line)
|
| + s.step_log_end('run_recipe')
|
| +
|
| + _isolate_environment()
|
| +
|
| + # Find and load the recipe to run.
|
| + try:
|
| + recipe_module = universe.load_recipe(recipe)
|
| + stream.emit('Running recipe with %s' % (properties,))
|
| + prop_defs = recipe_module.PROPERTIES
|
| +
|
| + api = loader.create_recipe_api(recipe_module.LOADED_DEPS,
|
| + engine,
|
| + test_data)
|
| +
|
| + s.step_text('<br/>running recipe: "%s"' % recipe)
|
| + except loader.NoSuchRecipe as e:
|
| + s.step_text('<br/>recipe not found: %s' % e)
|
| + s.step_failure()
|
| + return RecipeExecutionResult(2, None)
|
| +
|
| + # Run the steps emitted by a recipe via the engine, emitting annotations
|
| + # into |stream| along the way.
|
| + return engine.run(recipe_module.RunSteps, api, prop_defs)
|
| +
|
| +
|
| +def _merge_envs(original, override):
|
| + """Merges two environments.
|
| +
|
| + Returns a new environment dict with entries from |override| overwriting
|
| + corresponding entries in |original|. Keys whose value is None will completely
|
| + remove the environment variable. Values can contain %(KEY)s strings, which
|
| + will be substituted with the values from the original (useful for amending, as
|
| + opposed to overwriting, variables like PATH).
|
| + """
|
| + result = original.copy()
|
| + if not override:
|
| + return result
|
| + for k, v in override.items():
|
| + if v is None:
|
| + if k in result:
|
| + del result[k]
|
| + else:
|
| + result[str(k)] = str(v) % original
|
| + return result
|
| +
|
| +
|
| +def _print_step(step, env, stream):
|
| + """Prints the step command and relevant metadata.
|
| +
|
| + Intended to be similar to the information that Buildbot prints at the
|
| + beginning of each non-annotator step.
|
| + """
|
| + step_info_lines = []
|
| + step_info_lines.append(' '.join(step['cmd']))
|
| + step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd()))
|
| + for key, value in sorted(step.items()):
|
| + if value is not None:
|
| + if callable(value):
|
| + # This prevents functions from showing up as:
|
| + # '<function foo at 0x7f523ec7a410>'
|
| + # which is tricky to test.
|
| + value = value.__name__+'(...)'
|
| + step_info_lines.append(' %s: %s' % (key, value))
|
| + step_info_lines.append('full environment:')
|
| + for key, value in sorted(env.items()):
|
| + step_info_lines.append(' %s: %s' % (key, value))
|
| + step_info_lines.append('')
|
| + stream.emit('\n'.join(step_info_lines))
|
| +
|
| +
|
| +@contextlib.contextmanager
|
| +def _modify_lookup_path(path):
|
| + """Places the specified path into os.environ.
|
| +
|
| + Necessary because subprocess.Popen uses os.environ to perform lookup on the
|
| + supplied command, and only uses the |env| kwarg for modifying the environment
|
| + of the child process.
|
| + """
|
| + saved_path = os.environ['PATH']
|
| + try:
|
| + if path is not None:
|
| + os.environ['PATH'] = path
|
| + yield
|
| + finally:
|
| + os.environ['PATH'] = saved_path
|
| +
|
| +
|
| +def _normalize_change(change):
|
| + assert isinstance(change, dict), 'Change is not a dict'
|
| + change = change.copy()
|
| +
|
| + # Convert when_timestamp to UNIX timestamp.
|
| + when = change.get('when_timestamp')
|
| + if isinstance(when, datetime.datetime):
|
| + when = calendar.timegm(when.utctimetuple())
|
| + change['when_timestamp'] = when
|
| +
|
| + return change
|
| +
|
| +
|
| +def _trigger_builds(step, trigger_specs):
|
| + assert trigger_specs is not None
|
| + for trig in trigger_specs:
|
| + builder_name = trig.get('builder_name')
|
| + if not builder_name:
|
| + raise ValueError('Trigger spec: builder_name is not set')
|
| +
|
| + changes = trig.get('buildbot_changes', [])
|
| + assert isinstance(changes, list), 'buildbot_changes must be a list'
|
| + changes = map(_normalize_change, changes)
|
| +
|
| + step.step_trigger(json.dumps({
|
| + 'builderNames': [builder_name],
|
| + 'bucket': trig.get('bucket'),
|
| + 'changes': changes,
|
| + 'properties': trig.get('properties'),
|
| + }, sort_keys=True))
|
| +
|
| +
|
| +def _run_annotated_step(
|
| + stream, name, cmd, cwd=None, env=None, allow_subannotations=False,
|
| + trigger_specs=None, nest_level=0, **kwargs):
|
| + """Runs a single step.
|
| +
|
| + Context:
|
| + stream: StructuredAnnotationStream to use to emit step
|
| +
|
| + Step parameters:
|
| + name: name of the step, will appear in buildbots waterfall
|
| + cmd: command to run, list of one or more strings
|
| + cwd: absolute path to working directory for the command
|
| + env: dict with overrides for environment variables
|
| + allow_subannotations: if True, lets the step emit its own annotations
|
| + trigger_specs: a list of trigger specifications, which are dict with keys:
|
| + properties: a dict of properties.
|
| + Buildbot requires buildername property.
|
| +
|
| + Known kwargs:
|
| + stdout: Path to a file to put step stdout into. If used, stdout won't appear
|
| + in annotator's stdout (and |allow_subannotations| is ignored).
|
| + stderr: Path to a file to put step stderr into. If used, stderr won't appear
|
| + in annotator's stderr.
|
| + stdin: Path to a file to read step stdin from.
|
| +
|
| + Returns the returncode of the step.
|
| + """
|
| + if isinstance(cmd, basestring):
|
| + cmd = (cmd,)
|
| + cmd = map(str, cmd)
|
| +
|
| + # For error reporting.
|
| + step_dict = kwargs.copy()
|
| + step_dict.update({
|
| + 'name': name,
|
| + 'cmd': cmd,
|
| + 'cwd': cwd,
|
| + 'env': env,
|
| + 'allow_subannotations': allow_subannotations,
|
| + })
|
| + step_env = _merge_envs(os.environ, env)
|
| +
|
| + step_annotation = stream.step(name)
|
| + step_annotation.step_started()
|
| +
|
| + if nest_level:
|
| + step_annotation.step_nest_level(nest_level)
|
| +
|
| + _print_step(step_dict, step_env, stream)
|
| + returncode = 0
|
| + if cmd:
|
| + try:
|
| + # Open file handles for IO redirection based on file names in step_dict.
|
| + fhandles = {
|
| + 'stdout': subprocess.PIPE,
|
| + 'stderr': subprocess.PIPE,
|
| + 'stdin': None,
|
| + }
|
| + for key in fhandles:
|
| + if key in step_dict:
|
| + fhandles[key] = open(step_dict[key],
|
| + 'rb' if key == 'stdin' else 'wb')
|
| +
|
| + if sys.platform.startswith('win'):
|
| + # Windows has a bad habit of opening a dialog when a console program
|
| + # crashes, rather than just letting it crash. Therefore, when a program
|
| + # crashes on Windows, we don't find out until the build step times out.
|
| + # This code prevents the dialog from appearing, so that we find out
|
| + # immediately and don't waste time waiting for a user to close the
|
| + # dialog.
|
| + import ctypes
|
| + # SetErrorMode(SEM_NOGPFAULTERRORBOX). For more information, see:
|
| + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx
|
| + ctypes.windll.kernel32.SetErrorMode(0x0002)
|
| + # CREATE_NO_WINDOW. For more information, see:
|
| + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
|
| + creationflags = 0x8000000
|
| + else:
|
| + creationflags = 0
|
| +
|
| + with _modify_lookup_path(step_env.get('PATH')):
|
| + proc = subprocess.Popen(
|
| + cmd,
|
| + env=step_env,
|
| + cwd=cwd,
|
| + universal_newlines=True,
|
| + creationflags=creationflags,
|
| + **fhandles)
|
| +
|
| + # Safe to close file handles now that subprocess has inherited them.
|
| + for handle in fhandles.itervalues():
|
| + if isinstance(handle, file):
|
| + handle.close()
|
| +
|
| + outlock = threading.Lock()
|
| + def filter_lines(lock, allow_subannotations, inhandle, outhandle):
|
| + while True:
|
| + line = inhandle.readline()
|
| + if not line:
|
| + break
|
| + lock.acquire()
|
| + try:
|
| + if not allow_subannotations and line.startswith('@@@'):
|
| + outhandle.write('!')
|
| + outhandle.write(line)
|
| + outhandle.flush()
|
| + finally:
|
| + lock.release()
|
| +
|
| + # Pump piped stdio through filter_lines. IO going to files on disk is
|
| + # not filtered.
|
| + threads = []
|
| + for key in ('stdout', 'stderr'):
|
| + if fhandles[key] == subprocess.PIPE:
|
| + inhandle = getattr(proc, key)
|
| + outhandle = getattr(sys, key)
|
| + threads.append(threading.Thread(
|
| + target=filter_lines,
|
| + args=(outlock, allow_subannotations, inhandle, outhandle)))
|
| +
|
| + for th in threads:
|
| + th.start()
|
| + proc.wait()
|
| + for th in threads:
|
| + th.join()
|
| + returncode = proc.returncode
|
| + except OSError:
|
| + # File wasn't found, error will be reported to stream when the exception
|
| + # crosses the context manager.
|
| + step_annotation.step_exception_occured(*sys.exc_info())
|
| + raise
|
| +
|
| + # TODO(martiniss) move logic into own module?
|
| + if trigger_specs:
|
| + _trigger_builds(step_annotation, trigger_specs)
|
| +
|
| + return step_annotation, returncode
|
| +
|
| +class RecipeEngine(object):
|
| + """
|
| + Knows how to execute steps emitted by a recipe, holds global state such as
|
| + step history and build properties. Each recipe module API has a reference to
|
| + this object.
|
| +
|
| + Recipe modules that are aware of the engine:
|
| + * properties - uses engine.properties.
|
| + * step_history - uses engine.step_history.
|
| + * step - uses engine.create_step(...).
|
| +
|
| + """
|
| + def __init__(self, stream, properties, test_data):
|
| + self._stream = stream
|
| + self._properties = properties
|
| + self._test_data = test_data
|
| + self._step_history = collections.OrderedDict()
|
| +
|
| + self._previous_step_annotation = None
|
| + self._previous_step_result = None
|
| + self._api = None
|
| +
|
| + @property
|
| + def properties(self):
|
| + return self._properties
|
| +
|
| + @property
|
| + def previous_step_result(self):
|
| + """Allows api.step to get the active result from any context."""
|
| + return self._previous_step_result
|
| +
|
| + def _emit_results(self):
|
| + """Internal helper used to emit results."""
|
| + annotation = self._previous_step_annotation
|
| + step_result = self._previous_step_result
|
| +
|
| + self._previous_step_annotation = None
|
| + self._previous_step_result = None
|
| +
|
| + if not annotation or not step_result:
|
| + return
|
| +
|
| + step_result.presentation.finalize(annotation)
|
| + if self._test_data.enabled:
|
| + val = annotation.stream.getvalue()
|
| + lines = filter(None, val.splitlines())
|
| + if lines:
|
| + # note that '~' sorts after 'z' so that this will be last on each
|
| + # step. also use _step to get access to the mutable step
|
| + # dictionary.
|
| + # pylint: disable=w0212
|
| + step_result._step['~followup_annotations'] = lines
|
| + annotation.step_ended()
|
| +
|
| + def run_step(self, step):
|
| + """
|
| + Runs a step.
|
| +
|
| + Args:
|
| + step: The step to run.
|
| +
|
| + Returns:
|
| + A StepData object containing the result of running the step.
|
| + """
|
| + ok_ret = step.pop('ok_ret')
|
| + infra_step = step.pop('infra_step')
|
| + nest_level = step.pop('step_nest_level')
|
| +
|
| + test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData)
|
| + step_test = self._test_data.pop_step_test_data(step['name'],
|
| + test_data_fn)
|
| + placeholders = render_step(step, step_test)
|
| +
|
| + self._step_history[step['name']] = step
|
| + self._emit_results()
|
| +
|
| + step_result = None
|
| +
|
| + if not self._test_data.enabled:
|
| + self._previous_step_annotation, retcode = _run_annotated_step(
|
| + self._stream, nest_level=nest_level, **step)
|
| +
|
| + step_result = StepData(step, retcode)
|
| + self._stream.step_cursor(step['name'])
|
| + else:
|
| + self._previous_step_annotation = annotation = self._stream.step(
|
| + step['name'])
|
| + annotation.step_started()
|
| + try:
|
| + annotation.stream = cStringIO.StringIO()
|
| + if nest_level:
|
| + annotation.step_nest_level(nest_level)
|
| +
|
| + step_result = StepData(step, step_test.retcode)
|
| + except OSError:
|
| + exc_type, exc_value, exc_tb = sys.exc_info()
|
| + trace = traceback.format_exception(exc_type, exc_value, exc_tb)
|
| + trace_lines = ''.join(trace).split('\n')
|
| + annotation.write_log_lines('exception', filter(None, trace_lines))
|
| + annotation.step_exception()
|
| +
|
| + get_placeholder_results(step_result, placeholders)
|
| + self._previous_step_result = step_result
|
| +
|
| + if step_result.retcode in ok_ret:
|
| + step_result.presentation.status = 'SUCCESS'
|
| + return step_result
|
| + else:
|
| + if not infra_step:
|
| + state = 'FAILURE'
|
| + exc = recipe_api.StepFailure
|
| + else:
|
| + state = 'EXCEPTION'
|
| + exc = recipe_api.InfraFailure
|
| +
|
| + step_result.presentation.status = state
|
| + if step_test.enabled:
|
| + # To avoid cluttering the expectations, don't emit this in testmode.
|
| + self._previous_step_annotation.emit(
|
| + 'step returned non-zero exit code: %d' % step_result.retcode)
|
| +
|
| + raise exc(step['name'], step_result)
|
| +
|
| +
|
| + def run(self, steps_function, api, prop_defs):
|
| + """Run a recipe represented by top level RunSteps function.
|
| +
|
| + This function blocks until recipe finishes.
|
| +
|
| + Args:
|
| + steps_function: function that runs the steps.
|
| + api: The api, with loaded module dependencies.
|
| + Used by the some special modules.
|
| + prop_defs: Property definitions for this recipe.
|
| +
|
| + Returns:
|
| + RecipeExecutionResult with status code and list of steps ran.
|
| + """
|
| + self._api = api
|
| + retcode = None
|
| + final_result = None
|
| +
|
| + try:
|
| + try:
|
| + retcode = loader.invoke_with_properties(
|
| + steps_function, api._engine.properties, prop_defs, api=api)
|
| + assert retcode is None, (
|
| + "Non-None return from RunSteps is not supported yet")
|
| +
|
| + assert not self._test_data.enabled or not self._test_data.step_data, (
|
| + "Unconsumed test data! %s" % (self._test_data.step_data,))
|
| + finally:
|
| + self._emit_results()
|
| + except recipe_api.StepFailure as f:
|
| + retcode = f.retcode or 1
|
| + final_result = {
|
| + "name": "$final_result",
|
| + "reason": f.reason,
|
| + "status_code": retcode
|
| + }
|
| + except StepDataAttributeError as ex:
|
| + unexpected_exception = self._test_data.is_unexpected_exception(ex)
|
| +
|
| + retcode = -1
|
| + final_result = {
|
| + "name": "$final_result",
|
| + "reason": "Invalid Step Data Access: %r" % ex,
|
| + "status_code": retcode
|
| + }
|
| +
|
| + with self._stream.step('Invalid Step Data Access') as s:
|
| + s.step_exception()
|
| + s.write_log_lines('exception', traceback.format_exc().splitlines())
|
| +
|
| + if unexpected_exception:
|
| + raise
|
| +
|
| + except Exception as ex:
|
| + unexpected_exception = self._test_data.is_unexpected_exception(ex)
|
| +
|
| + retcode = -1
|
| + final_result = {
|
| + "name": "$final_result",
|
| + "reason": "Uncaught Exception: %r" % ex,
|
| + "status_code": retcode
|
| + }
|
| +
|
| + with self._stream.step('Uncaught Exception') as s:
|
| + s.step_exception()
|
| + s.write_log_lines('exception', traceback.format_exc().splitlines())
|
| +
|
| + if unexpected_exception:
|
| + raise
|
| +
|
| + if final_result is not None:
|
| + self._step_history[final_result['name']] = final_result
|
| +
|
| + return RecipeExecutionResult(retcode, self._step_history)
|
| +
|
| + def create_step(self, step): # pylint: disable=R0201
|
| + """Called by step module to instantiate a new step.
|
| +
|
| + Args:
|
| + step: ConfigGroup object with information about the step, see
|
| + recipe_modules/step/config.py.
|
| +
|
| + Returns:
|
| + Opaque engine specific object that is understood by 'run_steps' method.
|
| + """
|
| + return step.as_jsonish()
|
| +
|
| +
|
|
|