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 |