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

Side by Side Diff: recipe_engine/run.py

Issue 2415793003: Setup basic Runtime with properties and platform.
Patch Set: Split out, more immutables, better utilization. Created 4 years, 2 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 unified diff | Download patch
OLDNEW
1 # Copyright 2016 The LUCI Authors. All rights reserved. 1 # Copyright 2016 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 """Entry point for fully-annotated builds. 5 """Entry point for fully-annotated builds.
6 6
7 This script is part of the effort to move all builds to annotator-based 7 This script is part of the effort to move all builds to annotator-based
8 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() 8 systems. Any builder configured to use the AnnotatorFactory.BaseFactory()
9 found in scripts/master/factory/annotator_factory.py executes a single 9 found in scripts/master/factory/annotator_factory.py executes a single
10 AddAnnotatedScript step. That step (found in annotator_commands.py) calls 10 AddAnnotatedScript step. That step (found in annotator_commands.py) calls
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
57 the current history of what steps have run, what they returned, and any 57 the current history of what steps have run, what they returned, and any
58 json data they emitted. Additionally, the OrderedDict has the following 58 json data they emitted. Additionally, the OrderedDict has the following
59 convenience functions defined: 59 convenience functions defined:
60 * last_step - Returns the last step that ran or None 60 * last_step - Returns the last step that ran or None
61 * nth_step(n) - Returns the N'th step that ran or None 61 * nth_step(n) - Returns the N'th step that ran or None
62 62
63 'failed' is a boolean representing if the build is in a 'failed' state. 63 'failed' is a boolean representing if the build is in a 'failed' state.
64 """ 64 """
65 65
66 import collections 66 import collections
67 import copy
67 import json 68 import json
68 import os 69 import os
69 import sys
70 import traceback 70 import traceback
71 71
72 from . import env 72 from . import env
73 73
74 from . import loader 74 from . import loader
75 from . import recipe_api 75 from . import recipe_api
76 from . import recipe_test_api 76 from . import recipe_test_api
77 from . import step_runner as step_runner_module 77 from . import step_runner as step_runner_module
78 from . import types 78 from . import types
79 from . import util 79 from . import util
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after
181 181
182 182
183 # Return value of run_steps and RecipeEngine.run. Just a container for the 183 # Return value of run_steps and RecipeEngine.run. Just a container for the
184 # literal return value of the recipe. 184 # literal return value of the recipe.
185 RecipeResult = collections.namedtuple('RecipeResult', 'result') 185 RecipeResult = collections.namedtuple('RecipeResult', 'result')
186 186
187 187
188 # TODO(dnj): Replace "properties" with a generic runtime instance. This instance 188 # TODO(dnj): Replace "properties" with a generic runtime instance. This instance
189 # will be used to seed recipe clients and expanded to include managed runtime 189 # will be used to seed recipe clients and expanded to include managed runtime
190 # entities. 190 # entities.
191 def run_steps(properties, stream_engine, step_runner, universe_view): 191 def run_steps(rt, stream_engine, step_runner, universe_view):
192 """Runs a recipe (given by the 'recipe' property). 192 """Runs a recipe (given by the 'recipe' property).
193 193
194 Args: 194 Args:
195 properties: a dictionary of properties to pass to the recipe. The 195 rt (Runtime): The runtime instance.
196 'recipe' property defines which recipe to actually run.
197 stream_engine: the StreamEngine to use to create individual step streams. 196 stream_engine: the StreamEngine to use to create individual step streams.
198 step_runner: The StepRunner to use to 'actually run' the steps. 197 step_runner: The StepRunner to use to 'actually run' the steps.
199 universe_view: The RecipeUniverse to use to load the recipes & modules. 198 universe_view: The RecipeUniverse to use to load the recipes & modules.
200 199
201 Returns: RecipeResult 200 Returns: RecipeResult
202 """ 201 """
203 with stream_engine.make_step_stream('setup_build') as s: 202 with stream_engine.make_step_stream('setup_build') as s:
204 engine = RecipeEngine(step_runner, properties, universe_view) 203 engine = RecipeEngine(step_runner, rt, universe_view)
205 204
206 # Create all API modules and top level RunSteps function. It doesn't launch 205 # Create all API modules and top level RunSteps function. It doesn't launch
207 # any recipe code yet; RunSteps needs to be called. 206 # any recipe code yet; RunSteps needs to be called.
208 api = None 207 api = None
209 208
210 assert 'recipe' in properties 209 assert 'recipe' in rt.properties
211 recipe = properties['recipe'] 210 recipe = rt.properties['recipe']
212 211
213 root_package = universe_view.universe.package_deps.root_package 212 root_package = universe_view.universe.package_deps.root_package
214 run_recipe_help_lines = [ 213 run_recipe_help_lines = [
215 'To repro this locally, run the following line from the root of a %r' 214 'To repro this locally, run the following line from the root of a %r'
216 ' checkout:' % (root_package.name), 215 ' checkout:' % (root_package.name),
217 '', 216 '',
218 '%s run --properties-file - %s <<EOF' % ( 217 '%s run --properties-file - %s <<EOF' % (
219 os.path.join( '.', root_package.relative_recipes_dir, 'recipes.py'), 218 os.path.join( '.', root_package.relative_recipes_dir, 'recipes.py'),
220 recipe), 219 recipe),
221 '%s' % json.dumps(properties), 220 '%s' % rt.properties.to_json(sort_keys=True),
222 'EOF', 221 'EOF',
223 '', 222 '',
224 'To run on Windows, you can put the JSON in a file and redirect the', 223 'To run on Windows, you can put the JSON in a file and redirect the',
225 'contents of the file into run_recipe.py, with the < operator.', 224 'contents of the file into run_recipe.py, with the < operator.',
226 ] 225 ]
227 226
228 with s.new_log_stream('run_recipe') as l: 227 with s.new_log_stream('run_recipe') as l:
229 for line in run_recipe_help_lines: 228 for line in run_recipe_help_lines:
230 l.write_line(line) 229 l.write_line(line)
231 230
232 _isolate_environment() 231 os.environ = _isolate_environment(rt, os.environ)
233 232
234 # Find and load the recipe to run. 233 # Find and load the recipe to run.
235 try: 234 try:
236 recipe_script = universe_view.load_recipe(recipe, engine=engine) 235 recipe_script = universe_view.load_recipe(recipe, engine=engine)
237 s.write_line('Running recipe with %s' % (properties,)) 236 s.write_line('Running recipe with %s' % (rt.properties,))
238 237
239 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, 238 api = loader.create_recipe_api(recipe_script.LOADED_DEPS,
240 engine, 239 engine,
241 recipe_test_api.DisabledTestData()) 240 recipe_test_api.DisabledTestData())
242 241
243 s.add_step_text('running recipe: "%s"' % recipe) 242 s.add_step_text('running recipe: "%s"' % recipe)
244 except (loader.LoaderError, ImportError, AssertionError) as e: 243 except (loader.LoaderError, ImportError, AssertionError) as e:
245 for line in str(e).splitlines(): 244 for line in str(e).splitlines():
246 s.add_step_text(line) 245 s.add_step_text(line)
247 s.set_step_status('EXCEPTION') 246 s.set_step_status('EXCEPTION')
248 return RecipeResult({ 247 return RecipeResult({
249 'status_code': 2, 248 'status_code': 2,
250 'reason': str(e), 249 'reason': str(e),
251 }) 250 })
252 251
253 # Run the steps emitted by a recipe via the engine, emitting annotations 252 # Run the steps emitted by a recipe via the engine, emitting annotations
254 # into |stream| along the way. 253 # into |stream| along the way.
255 return engine.run(recipe_script, api, properties) 254 return engine.run(recipe_script, api)
256 255
257 256
258 def _isolate_environment(): 257 def _isolate_environment(rt, env):
259 """Isolate the environment to a known subset set.""" 258 """Isolate the environment to a known subset set."""
260 if sys.platform.startswith('win'): 259 if rt.platform.is_win():
261 whitelist = ENV_WHITELIST_WIN 260 whitelist = ENV_WHITELIST_WIN
262 elif sys.platform in ('darwin', 'posix', 'linux2'): 261 elif rt.platform.is_posix():
263 whitelist = ENV_WHITELIST_POSIX 262 whitelist = ENV_WHITELIST_POSIX
264 else: 263 else:
265 print ('WARNING: unknown platform %s, not isolating environment.' % 264 print 'WARNING: unknown platform %s, not isolating environment.' % (
266 sys.platform) 265 rt.platform,)
267 return 266 return env
267 return {k: v for k, v in env.iteritems() if k in whitelist}
268 268
269 for k in os.environ.keys(): 269
270 if k not in whitelist: 270 class Runtime(object):
271 del os.environ[k] 271 """Container for instance-global state."""
272
273 def __init__(self, properties, platform=None):
274 # Store both the frozen and mutable properties. We will use the frozen ones
275 # internally to ensure recipe engine code doesn't modify them. The
276 # properties client may serve the original properties dict.
277 self._properties = types.freeze(properties)
278 self._properties_dict = copy.deepcopy(properties)
279
280 self._platform = platform if platform else util.Platform.probe()
281
282 @property
283 def properties(self):
284 """Returns (types.FrozenDict): The input properties."""
285 return self._properties
286
287 def mutable_properties(self):
288 """Returns (dict): A copy of the input properties."""
289 return copy.deepcopy(self._properties_dict)
290
291 @property
292 def platform(self):
293 """Returns (util.Platform): The current running Platform."""
294 return self._platform
272 295
273 296
274 class RecipeEngine(object): 297 class RecipeEngine(object):
275 """ 298 """
276 Knows how to execute steps emitted by a recipe, holds global state such as 299 Knows how to execute steps emitted by a recipe, holds global state such as
277 step history and build properties. Each recipe module API has a reference to 300 step history and build properties. Each recipe module API has a reference to
278 this object. 301 this object.
279
280 Recipe modules that are aware of the engine:
281 * properties - uses engine.properties.
282 * step - uses engine.create_step(...), and previous_step_result.
283 """ 302 """
284 303
285 ActiveStep = collections.namedtuple('ActiveStep', ( 304 ActiveStep = collections.namedtuple('ActiveStep', (
286 'config', 'step_result', 'open_step')) 305 'config', 'step_result', 'open_step'))
287 306
288 def __init__(self, step_runner, properties, universe_view): 307 def __init__(self, step_runner, rt, universe_view):
289 """See run_steps() for parameter meanings.""" 308 """See run_steps() for parameter meanings."""
290 self._step_runner = step_runner 309 self._step_runner = step_runner
291 self._properties = properties 310 self._rt = rt
292 self._universe_view = universe_view 311 self._universe_view = universe_view
293 self._clients = {client.IDENT: client for client in ( 312 self._clients = {client.IDENT: client for client in (
294 recipe_api.StepClient(self), 313 recipe_api.StepClient(self),
295 recipe_api.PropertiesClient(self), 314 recipe_api.PropertiesClient(self._rt),
315 recipe_api.PlatformClient(self._rt.platform),
296 recipe_api.DependencyManagerClient(self), 316 recipe_api.DependencyManagerClient(self),
297 )} 317 )}
298 318
299 # A stack of ActiveStep objects, holding the most recently executed step at 319 # A stack of ActiveStep objects, holding the most recently executed step at
300 # each nest level (objects deeper in the stack have lower nest levels). 320 # each nest level (objects deeper in the stack have lower nest levels).
301 # When we pop from this stack, we close the corresponding step stream. 321 # When we pop from this stack, we close the corresponding step stream.
302 self._step_stack = [] 322 self._step_stack = []
303 323
304 # TODO(iannucci): come up with a more structured way to advertise/set mode
305 # flags/options for the engine.
306 if '$recipe_engine' in properties:
307 options = properties['$recipe_engine']
308 try:
309 mode_flags = options.get('mode_flags')
310 if mode_flags:
311 if mode_flags.get('use_subprocess42'):
312 print "IGNORING MODE_SUBPROCESS42"
313 except Exception as e:
314 print "Failed to set recipe_engine options, got: %r: %s" % (options, e)
315
316 @property 324 @property
317 def properties(self): 325 def properties(self):
318 return self._properties 326 return self._rt.properties
319 327
320 @property 328 @property
321 def universe(self): 329 def universe(self):
322 return self._universe_view.universe 330 return self._universe_view.universe
323 331
324 def _close_through_level(self, level): 332 def _close_through_level(self, level):
325 """Close all open steps whose nest level is >= the supplied level. 333 """Close all open steps whose nest level is >= the supplied level.
326 334
327 Args: 335 Args:
328 level (int): the nest level to close through. 336 level (int): the nest level to close through.
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after
378 state = 'EXCEPTION' 386 state = 'EXCEPTION'
379 exc = recipe_api.InfraFailure 387 exc = recipe_api.InfraFailure
380 388
381 step_result.presentation.status = state 389 step_result.presentation.status = state
382 390
383 self._step_stack[-1].open_step.stream.write_line( 391 self._step_stack[-1].open_step.stream.write_line(
384 'step returned non-zero exit code: %d' % step_result.retcode) 392 'step returned non-zero exit code: %d' % step_result.retcode)
385 393
386 raise exc(step_config.name, step_result) 394 raise exc(step_config.name, step_result)
387 395
388 def run(self, recipe_script, api, properties): 396 def run(self, recipe_script, api):
389 """Run a recipe represented by a recipe_script object. 397 """Run a recipe represented by a recipe_script object.
390 398
391 This function blocks until recipe finishes. 399 This function blocks until recipe finishes.
392 It mainly executes the recipe, and has some exception handling logic, and 400 It mainly executes the recipe, and has some exception handling logic, and
393 adds the step history to the result. 401 adds the step history to the result.
394 402
395 Args: 403 Args:
396 recipe_script: The recipe to run, as represented by a RecipeScript object. 404 recipe_script: The recipe to run, as represented by a RecipeScript object.
397 api: The api, with loaded module dependencies. 405 api: The api, with loaded module dependencies.
398 Used by the some special modules. 406 Used by the some special modules.
399 properties: a dictionary of properties to pass to the recipe.
400 407
401 Returns: 408 Returns:
402 RecipeResult which has return value or status code and exception. 409 RecipeResult which has return value or status code and exception.
403 """ 410 """
404 result = None 411 result = None
405 412
406 with self._step_runner.run_context(): 413 with self._step_runner.run_context():
407 try: 414 try:
408 try: 415 try:
409 recipe_result = recipe_script.run(api, properties) 416 recipe_result = recipe_script.run(api, self._rt)
410 result = { 417 result = {
411 "recipe_result": recipe_result, 418 "recipe_result": recipe_result,
412 "status_code": 0 419 "status_code": 0
413 } 420 }
414 finally: 421 finally:
415 self._close_through_level(0) 422 self._close_through_level(0)
416 except recipe_api.StepFailure as f: 423 except recipe_api.StepFailure as f:
417 result = { 424 result = {
418 # Include "recipe_result" so it doesn't get marked as infra failure. 425 # Include "recipe_result" so it doesn't get marked as infra failure.
419 "recipe_result": None, 426 "recipe_result": None,
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
472 results.append( 479 results.append(
473 loader._invoke_with_properties( 480 loader._invoke_with_properties(
474 run_recipe, properties, recipe_script.PROPERTIES, 481 run_recipe, properties, recipe_script.PROPERTIES,
475 properties.keys())) 482 properties.keys()))
476 except TypeError as e: 483 except TypeError as e:
477 raise TypeError( 484 raise TypeError(
478 "Got %r while trying to call recipe %s with properties %r" % ( 485 "Got %r while trying to call recipe %s with properties %r" % (
479 e, recipe, properties)) 486 e, recipe, properties))
480 487
481 return results 488 return results
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698