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 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
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 json | 67 import json |
68 import os | 68 import os |
69 import sys | |
70 import traceback | 69 import traceback |
71 | 70 |
72 from . import env | 71 from . import env |
73 | 72 |
74 from . import loader | 73 from . import loader |
75 from . import recipe_api | 74 from . import recipe_api |
76 from . import recipe_test_api | 75 from . import recipe_test_api |
77 from . import step_runner as step_runner_module | 76 from . import step_runner as step_runner_module |
78 from . import types | 77 from . import types |
79 from . import util | 78 from . import util |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
181 | 180 |
182 | 181 |
183 # Return value of run_steps and RecipeEngine.run. Just a container for the | 182 # Return value of run_steps and RecipeEngine.run. Just a container for the |
184 # literal return value of the recipe. | 183 # literal return value of the recipe. |
185 RecipeResult = collections.namedtuple('RecipeResult', 'result') | 184 RecipeResult = collections.namedtuple('RecipeResult', 'result') |
186 | 185 |
187 | 186 |
188 # TODO(dnj): Replace "properties" with a generic runtime instance. This instance | 187 # 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 | 188 # will be used to seed recipe clients and expanded to include managed runtime |
190 # entities. | 189 # entities. |
191 def run_steps(properties, stream_engine, step_runner, universe_view): | 190 def run_steps(rt, stream_engine, step_runner, universe_view): |
192 """Runs a recipe (given by the 'recipe' property). | 191 """Runs a recipe (given by the 'recipe' property). |
193 | 192 |
194 Args: | 193 Args: |
195 properties: a dictionary of properties to pass to the recipe. The | 194 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. | 195 stream_engine: the StreamEngine to use to create individual step streams. |
198 step_runner: The StepRunner to use to 'actually run' the steps. | 196 step_runner: The StepRunner to use to 'actually run' the steps. |
199 universe_view: The RecipeUniverse to use to load the recipes & modules. | 197 universe_view: The RecipeUniverse to use to load the recipes & modules. |
200 | 198 |
201 Returns: RecipeResult | 199 Returns: RecipeResult |
202 """ | 200 """ |
203 # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by | |
dnj
2016/10/13 01:08:23
Talked with iannucci@, probably don't need this.
| |
204 # 'patch_project'. 'root' had Rietveld knowing about the implementation of | |
205 # the builders. 'patch_project' lets the builder (recipe) decide its own | |
206 # destiny. | |
207 properties.pop('root', None) | |
208 | |
209 # TODO(iannucci): A much better way to do this would be to dynamically | |
dnj
2016/10/13 01:08:23
Transplanted into "recipes.py", closer to other pr
| |
210 # detect if the mirrors are actually available during the execution of the | |
211 # recipe. | |
212 if ('use_mirror' not in properties and ( | |
213 'TESTING_MASTERNAME' in os.environ or | |
214 'TESTING_SLAVENAME' in os.environ)): | |
215 properties['use_mirror'] = False | |
216 | |
217 with stream_engine.make_step_stream('setup_build') as s: | 201 with stream_engine.make_step_stream('setup_build') as s: |
218 engine = RecipeEngine(step_runner, properties, universe_view) | 202 engine = RecipeEngine(step_runner, rt, universe_view) |
219 | 203 |
220 # Create all API modules and top level RunSteps function. It doesn't launch | 204 # Create all API modules and top level RunSteps function. It doesn't launch |
221 # any recipe code yet; RunSteps needs to be called. | 205 # any recipe code yet; RunSteps needs to be called. |
222 api = None | 206 api = None |
223 | 207 |
224 assert 'recipe' in properties | 208 assert 'recipe' in rt.properties |
225 recipe = properties['recipe'] | 209 recipe = rt.properties['recipe'] |
226 | 210 |
227 properties_to_print = properties.copy() | 211 # Remove domain-specific properties. |
228 if 'use_mirror' in properties: | 212 properties_to_print = rt.properties.copy() |
229 del properties_to_print['use_mirror'] | 213 properties_to_print.pop('use_mirror', None) |
iannucci
2016/10/15 00:39:45
I would ditch this
dnj
2016/10/15 00:59:27
altogether, or this specific change?
iannucci
2016/10/15 01:04:09
Well you're moving the other property stuff in thi
dnj
2016/10/15 15:42:05
Done.
| |
230 | 214 |
231 root_package = universe_view.universe.package_deps.root_package | 215 root_package = universe_view.universe.package_deps.root_package |
232 run_recipe_help_lines = [ | 216 run_recipe_help_lines = [ |
233 'To repro this locally, run the following line from the root of a %r' | 217 'To repro this locally, run the following line from the root of a %r' |
234 ' checkout:' % (root_package.name), | 218 ' checkout:' % (root_package.name), |
235 '', | 219 '', |
236 '%s run --properties-file - %s <<EOF' % ( | 220 '%s run --properties-file - %s <<EOF' % ( |
237 os.path.join( '.', root_package.relative_recipes_dir, 'recipes.py'), | 221 os.path.join( '.', root_package.relative_recipes_dir, 'recipes.py'), |
238 recipe), | 222 recipe), |
239 '%s' % json.dumps(properties_to_print), | 223 '%s' % json.dumps(properties_to_print), |
240 'EOF', | 224 'EOF', |
241 '', | 225 '', |
242 'To run on Windows, you can put the JSON in a file and redirect the', | 226 'To run on Windows, you can put the JSON in a file and redirect the', |
243 'contents of the file into run_recipe.py, with the < operator.', | 227 'contents of the file into run_recipe.py, with the < operator.', |
244 ] | 228 ] |
245 | 229 |
246 with s.new_log_stream('run_recipe') as l: | 230 with s.new_log_stream('run_recipe') as l: |
247 for line in run_recipe_help_lines: | 231 for line in run_recipe_help_lines: |
248 l.write_line(line) | 232 l.write_line(line) |
249 | 233 |
250 _isolate_environment() | 234 os.environ = _isolate_environment(rt, os.environ) |
251 | 235 |
252 # Find and load the recipe to run. | 236 # Find and load the recipe to run. |
253 try: | 237 try: |
254 recipe_script = universe_view.load_recipe(recipe, engine=engine) | 238 recipe_script = universe_view.load_recipe(recipe, engine=engine) |
255 s.write_line('Running recipe with %s' % (properties,)) | 239 s.write_line('Running recipe with %s' % (rt.properties,)) |
256 | 240 |
257 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, | 241 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, |
258 engine, | 242 engine, |
259 recipe_test_api.DisabledTestData()) | 243 recipe_test_api.DisabledTestData()) |
260 | 244 |
261 s.add_step_text('running recipe: "%s"' % recipe) | 245 s.add_step_text('running recipe: "%s"' % recipe) |
262 except (loader.LoaderError, ImportError, AssertionError) as e: | 246 except (loader.LoaderError, ImportError, AssertionError) as e: |
263 for line in str(e).splitlines(): | 247 for line in str(e).splitlines(): |
264 s.add_step_text(line) | 248 s.add_step_text(line) |
265 s.set_step_status('EXCEPTION') | 249 s.set_step_status('EXCEPTION') |
266 return RecipeResult({ | 250 return RecipeResult({ |
267 'status_code': 2, | 251 'status_code': 2, |
268 'reason': str(e), | 252 'reason': str(e), |
269 }) | 253 }) |
270 | 254 |
271 # Run the steps emitted by a recipe via the engine, emitting annotations | 255 # Run the steps emitted by a recipe via the engine, emitting annotations |
272 # into |stream| along the way. | 256 # into |stream| along the way. |
273 return engine.run(recipe_script, api, properties) | 257 return engine.run(recipe_script, api, rt.properties) |
274 | 258 |
275 | 259 |
276 def _isolate_environment(): | 260 def _isolate_environment(rt, env): |
277 """Isolate the environment to a known subset set.""" | 261 """Isolate the environment to a known subset set.""" |
278 if sys.platform.startswith('win'): | 262 if rt.platform.is_win(): |
279 whitelist = ENV_WHITELIST_WIN | 263 whitelist = ENV_WHITELIST_WIN |
280 elif sys.platform in ('darwin', 'posix', 'linux2'): | 264 elif rt.platform.is_posix(): |
281 whitelist = ENV_WHITELIST_POSIX | 265 whitelist = ENV_WHITELIST_POSIX |
282 else: | 266 else: |
283 print ('WARNING: unknown platform %s, not isolating environment.' % | 267 print 'WARNING: unknown platform %s, not isolating environment.' % ( |
284 sys.platform) | 268 rt.platform,) |
285 return | 269 return env |
270 return {k: v for k, v in env.iteritems() if k in whitelist} | |
286 | 271 |
287 for k in os.environ.keys(): | 272 |
288 if k not in whitelist: | 273 class Runtime(object): |
289 del os.environ[k] | 274 """Container for instance-global state.""" |
275 | |
276 def __init__(self, properties, platform=None): | |
277 self._properties = properties | |
278 self._platform = platform if platform else util.Platform.probe() | |
279 | |
280 @property | |
281 def properties(self): | |
282 """Returns (dict): The input properties.""" | |
283 return self._properties | |
284 | |
285 @property | |
286 def platform(self): | |
287 """Returns (util.Platform): The current running Platform.""" | |
288 return self._platform | |
290 | 289 |
291 | 290 |
292 class RecipeEngine(object): | 291 class RecipeEngine(object): |
293 """ | 292 """ |
294 Knows how to execute steps emitted by a recipe, holds global state such as | 293 Knows how to execute steps emitted by a recipe, holds global state such as |
295 step history and build properties. Each recipe module API has a reference to | 294 step history and build properties. Each recipe module API has a reference to |
296 this object. | 295 this object. |
297 | |
298 Recipe modules that are aware of the engine: | |
299 * properties - uses engine.properties. | |
300 * step - uses engine.create_step(...), and previous_step_result. | |
301 """ | 296 """ |
302 | 297 |
303 ActiveStep = collections.namedtuple('ActiveStep', ( | 298 ActiveStep = collections.namedtuple('ActiveStep', ( |
304 'config', 'step_result', 'open_step')) | 299 'config', 'step_result', 'open_step')) |
305 | 300 |
306 def __init__(self, step_runner, properties, universe_view): | 301 def __init__(self, step_runner, rt, universe_view): |
307 """See run_steps() for parameter meanings.""" | 302 """See run_steps() for parameter meanings.""" |
308 self._step_runner = step_runner | 303 self._step_runner = step_runner |
309 self._properties = properties | 304 self._rt = rt |
310 self._universe_view = universe_view | 305 self._universe_view = universe_view |
311 self._clients = {client.IDENT: client for client in ( | 306 self._clients = {client.IDENT: client for client in ( |
312 recipe_api.StepClient(self), | 307 recipe_api.StepClient(self), |
313 recipe_api.PropertiesClient(self), | 308 recipe_api.PropertiesClient(self._rt.properties), |
309 recipe_api.PlatformClient(self._rt.platform), | |
314 recipe_api.DependencyManagerClient(self), | 310 recipe_api.DependencyManagerClient(self), |
315 )} | 311 )} |
316 | 312 |
317 # A stack of ActiveStep objects, holding the most recently executed step at | 313 # A stack of ActiveStep objects, holding the most recently executed step at |
318 # each nest level (objects deeper in the stack have lower nest levels). | 314 # each nest level (objects deeper in the stack have lower nest levels). |
319 # When we pop from this stack, we close the corresponding step stream. | 315 # When we pop from this stack, we close the corresponding step stream. |
320 self._step_stack = [] | 316 self._step_stack = [] |
321 | 317 |
322 # TODO(iannucci): come up with a more structured way to advertise/set mode | 318 # TODO(iannucci): come up with a more structured way to advertise/set mode |
iannucci
2016/10/15 00:39:45
I think this isn't used any more. you can delete t
dnj
2016/10/15 00:59:27
Done.
| |
323 # flags/options for the engine. | 319 # flags/options for the engine. |
324 if '$recipe_engine' in properties: | 320 options = self._rt.properties.get('$recipe_engine') |
325 options = properties['$recipe_engine'] | 321 if options is not None: |
326 try: | 322 try: |
327 mode_flags = options.get('mode_flags') | 323 mode_flags = options.get('mode_flags') |
328 if mode_flags: | 324 if mode_flags: |
329 if mode_flags.get('use_subprocess42'): | 325 if mode_flags.get('use_subprocess42'): |
330 print "IGNORING MODE_SUBPROCESS42" | 326 print "IGNORING MODE_SUBPROCESS42" |
331 except Exception as e: | 327 except Exception as e: |
332 print "Failed to set recipe_engine options, got: %r: %s" % (options, e) | 328 print "Failed to set recipe_engine options, got: %r: %s" % (options, e) |
333 | 329 |
334 @property | 330 @property |
335 def properties(self): | 331 def properties(self): |
336 return self._properties | 332 return self._rt.properties |
337 | 333 |
338 @property | 334 @property |
339 def universe(self): | 335 def universe(self): |
340 return self._universe_view.universe | 336 return self._universe_view.universe |
341 | 337 |
342 def _close_through_level(self, level): | 338 def _close_through_level(self, level): |
343 """Close all open steps whose nest level is >= the supplied level. | 339 """Close all open steps whose nest level is >= the supplied level. |
344 | 340 |
345 Args: | 341 Args: |
346 level (int): the nest level to close through. | 342 level (int): the nest level to close through. |
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
490 results.append( | 486 results.append( |
491 loader._invoke_with_properties( | 487 loader._invoke_with_properties( |
492 run_recipe, properties, recipe_script.PROPERTIES, | 488 run_recipe, properties, recipe_script.PROPERTIES, |
493 properties.keys())) | 489 properties.keys())) |
494 except TypeError as e: | 490 except TypeError as e: |
495 raise TypeError( | 491 raise TypeError( |
496 "Got %r while trying to call recipe %s with properties %r" % ( | 492 "Got %r while trying to call recipe %s with properties %r" % ( |
497 e, recipe, properties)) | 493 e, recipe, properties)) |
498 | 494 |
499 return results | 495 return results |
OLD | NEW |