| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 |
| OLD | NEW |