Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(8)

Unified Diff: third_party/recipe_engine/main.py

Issue 1151423002: Move recipe engine to third_party/recipe_engine. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Moved field_composer_test with its buddies Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « third_party/recipe_engine/loader.py ('k') | third_party/recipe_engine/recipe_api.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/recipe_engine/main.py
diff --git a/scripts/slave/annotated_run.py b/third_party/recipe_engine/main.py
old mode 100755
new mode 100644
similarity index 68%
copy from scripts/slave/annotated_run.py
copy to third_party/recipe_engine/main.py
index 35873dccfca726b7aae33ef975740b44593fc061..f2ed83ae6ce8493ebed2df195cb488ef30dca145
--- a/scripts/slave/annotated_run.py
+++ b/third_party/recipe_engine/main.py
@@ -1,5 +1,4 @@
-#!/usr/bin/env python
-# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# 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.
@@ -64,27 +63,24 @@ iterable_of_things.
'failed' is a boolean representing if the build is in a 'failed' state.
"""
+import collections
+import contextlib
import copy
import functools
import json
-import optparse
import os
import subprocess
import sys
+import threading
import traceback
import cStringIO
-import common.python26_polyfill # pylint: disable=W0611
-import collections # Import after polyfill to get OrderedDict on 2.6
-from common import annotator
-from common import chromium_utils
-
-from slave import recipe_loader
-from slave import recipe_test_api
-from slave import recipe_util
-from slave import recipe_api
+from . import loader
+from . import recipe_api
+from . import recipe_test_api
+from . import util
SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
@@ -228,7 +224,7 @@ def render_step(step, step_test):
placeholders = collections.defaultdict(lambda: collections.defaultdict(list))
new_cmd = []
for item in step.get('cmd', []):
- if isinstance(item, recipe_util.Placeholder):
+ 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))
@@ -243,7 +239,7 @@ def render_step(step, step_test):
placeholder = step.get(key)
tdata = None
if placeholder:
- assert isinstance(placeholder, recipe_util.Placeholder), key
+ assert isinstance(placeholder, util.Placeholder), key
tdata = getattr(step_test, key)
placeholder.render(tdata)
assert placeholder.backing_file
@@ -284,83 +280,15 @@ def get_callable_name(func):
return func.__name__
-def get_args(argv):
- """Process command-line arguments."""
-
- parser = optparse.OptionParser(
- description='Entry point for annotated builds.')
- parser.add_option('--build-properties',
- action='callback', callback=chromium_utils.convert_json,
- type='string', default={},
- help='build properties in JSON format')
- parser.add_option('--factory-properties',
- action='callback', callback=chromium_utils.convert_json,
- type='string', default={},
- help='factory properties in JSON format')
- parser.add_option('--build-properties-gz',
- action='callback', callback=chromium_utils.convert_gz_json,
- type='string', default={}, dest='build_properties',
- help='build properties in b64 gz JSON format')
- parser.add_option('--factory-properties-gz',
- action='callback', callback=chromium_utils.convert_gz_json,
- type='string', default={}, dest='factory_properties',
- help='factory properties in b64 gz JSON format')
- parser.add_option('--keep-stdin', action='store_true', default=False,
- help='don\'t close stdin when running recipe steps')
- return parser.parse_args(argv)
-
-
-def main(argv=None):
- opts, _ = get_args(argv)
-
- stream = annotator.StructuredAnnotationStream()
- universe = recipe_loader.RecipeUniverse()
-
- ret = run_steps(stream, opts.build_properties, opts.factory_properties,
- universe)
- return ret.status_code
-
-
# Return value of run_steps and RecipeEngine.run.
RecipeExecutionResult = collections.namedtuple(
'RecipeExecutionResult', 'status_code steps_ran')
-def get_recipe_properties(factory_properties, build_properties):
- """Constructs the recipe's properties from buildbot's properties.
-
- This merges factory_properties and build_properties. Furthermore, it
- tries to reconstruct the 'recipe' property from builders.pyl if it isn't
- already there, and in that case merges in properties form builders.pyl.
- """
- properties = factory_properties.copy()
- properties.update(build_properties)
-
- # Try to reconstruct the recipe from builders.pyl if not given.
- if 'recipe' not in properties:
- mastername = properties['mastername']
- buildername = properties['buildername']
-
- master_path = chromium_utils.MasterPath(mastername)
- builders_file = os.path.join(master_path, 'builders.pyl')
- if os.path.isfile(builders_file):
- builders = chromium_utils.ReadBuildersFile(builders_file)
- assert buildername in builders['builders'], (
- 'buildername %s is not listed in %s' % (buildername, builders_file))
- builder = builders['builders'][buildername]
-
- # Update properties with builders.pyl data.
- properties['recipe'] = builder['recipe']
- properties.update(builder.get('properties', {}))
- else:
- raise LookupError('Cannot find recipe for %s on %s' %
- (build_properties['buildername'],
- build_properties['mastername']))
- return properties
-
-
-def run_steps(stream, build_properties, factory_properties,
- universe, test_data=recipe_test_api.DisabledTestData()):
+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
@@ -369,20 +297,16 @@ def run_steps(stream, build_properties, factory_properties,
stream.honor_zero_return_code()
# TODO(iannucci): Stop this when blamelist becomes sane data.
- if ('blamelist_real' in build_properties and
- 'blamelist' in build_properties):
- build_properties['blamelist'] = build_properties['blamelist_real']
- del build_properties['blamelist_real']
+ 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.
- build_properties.pop('root', None)
-
- properties = get_recipe_properties(
- factory_properties=factory_properties,
- build_properties=build_properties)
+ 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
@@ -427,12 +351,12 @@ def run_steps(stream, build_properties, factory_properties,
try:
recipe_module = universe.load_recipe(recipe)
stream.emit('Running recipe with %s' % (properties,))
- api = recipe_loader.create_recipe_api(recipe_module.LOADED_DEPS,
+ api = loader.create_recipe_api(recipe_module.LOADED_DEPS,
engine,
test_data)
steps = recipe_module.GenSteps
s.step_text('<br/>running recipe: "%s"' % recipe)
- except recipe_loader.NoSuchRecipe as e:
+ except loader.NoSuchRecipe as e:
s.step_text('<br/>recipe not found: %s' % e)
s.step_failure()
return RecipeExecutionResult(2, None)
@@ -442,6 +366,236 @@ def run_steps(stream, build_properties, factory_properties,
return engine.run(steps, api)
+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, **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()
+
+ _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
@@ -556,7 +710,7 @@ class SequentialRecipeEngine(RecipeEngine):
step_result = None
if not self._test_data.enabled:
- self._previous_step_annotation, retcode = annotator.run_step(
+ self._previous_step_annotation, retcode = _run_annotated_step(
self._stream, **step)
step_result = StepData(step, retcode)
@@ -650,67 +804,3 @@ class SequentialRecipeEngine(RecipeEngine):
return step.as_jsonish()
-class ParallelRecipeEngine(RecipeEngine):
- """New engine that knows how to run steps in parallel.
-
- TODO(vadimsh): Implement it.
- """
-
- def __init__(self, stream, properties, test_data):
- super(ParallelRecipeEngine, self).__init__()
- self._stream = stream
- self._properties = properties
- self._test_data = test_data
-
- @property
- def properties(self):
- return self._properties
-
- def run(self, steps_function, api):
- raise NotImplementedError
-
- def create_step(self, step):
- raise NotImplementedError
-
-
-def update_scripts():
- if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'):
- os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS')
- return False
-
- stream = annotator.StructuredAnnotationStream()
-
- with stream.step('update_scripts') as s:
- build_root = os.path.join(SCRIPT_PATH, '..', '..')
- gclient_name = 'gclient'
- if sys.platform.startswith('win'):
- gclient_name += '.bat'
- gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name)
- gclient_cmd = [gclient_path, 'sync', '--force', '--verbose']
- cmd_dict = {
- 'name': 'update_scripts',
- 'cmd': gclient_cmd,
- 'cwd': build_root,
- }
- annotator.print_step(cmd_dict, os.environ, stream)
- if subprocess.call(gclient_cmd, cwd=build_root) != 0:
- s.step_text('gclient sync failed!')
- s.step_warnings()
- os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1'
-
- # After running update scripts, set PYTHONIOENCODING=UTF-8 for the real
- # annotated_run.
- os.environ['PYTHONIOENCODING'] = 'UTF-8'
-
- return True
-
-
-def shell_main(argv):
- if update_scripts():
- return subprocess.call([sys.executable] + argv)
- else:
- return main(argv)
-
-
-if __name__ == '__main__':
- sys.exit(shell_main(sys.argv))
« no previous file with comments | « third_party/recipe_engine/loader.py ('k') | third_party/recipe_engine/recipe_api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698