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

Side by Side Diff: third_party/recipe_engine/annotated_run.py

Issue 1151423002: Move recipe engine to third_party/recipe_engine. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Copyright notices 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Entry point for fully-annotated builds. 6 """Entry point for fully-annotated builds.
7 7
8 This script is part of the effort to move all builds to annotator-based 8 This script is part of the effort to move all builds to annotator-based
9 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() 9 systems. Any builder configured to use the AnnotatorFactory.BaseFactory()
10 found in scripts/master/factory/annotator_factory.py executes a single 10 found in scripts/master/factory/annotator_factory.py executes a single
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
54 will cease calling the generator and move on to the next item in 54 will cease calling the generator and move on to the next item in
55 iterable_of_things. 55 iterable_of_things.
56 56
57 'step_history' is an OrderedDict of {stepname -> StepData}, always representing 57 'step_history' is an OrderedDict of {stepname -> StepData}, always representing
58 the current history of what steps have run, what they returned, and any 58 the current history of what steps have run, what they returned, and any
59 json data they emitted. Additionally, the OrderedDict has the following 59 json data they emitted. Additionally, the OrderedDict has the following
60 convenience functions defined: 60 convenience functions defined:
61 * last_step - Returns the last step that ran or None 61 * last_step - Returns the last step that ran or None
62 * nth_step(n) - Returns the N'th step that ran or None 62 * nth_step(n) - Returns the N'th step that ran or None
63 63
64 'failed' is a boolean representing if the build is in a 'failed' state. 64 'failed' is a boolean representing if the build is in a 'failed' state.
iannucci 2015/05/27 02:03:27 rename to something more appropriate like 'main.py
luqui 2015/05/28 21:47:38 Done.
65 """ 65 """
66 66
67 import collections
68 import contextlib
67 import copy 69 import copy
68 import functools 70 import functools
69 import json 71 import json
70 import optparse
71 import os 72 import os
72 import subprocess 73 import subprocess
73 import sys 74 import sys
75 import threading
74 import traceback 76 import traceback
75 77
76 import cStringIO 78 import cStringIO
77 79
78 import common.python26_polyfill # pylint: disable=W0611
79 import collections # Import after polyfill to get OrderedDict on 2.6
80 80
81 from common import annotator 81 from . import recipe_loader
82 from common import chromium_utils 82 from . import recipe_test_api
83 83 from . import recipe_util
84 from slave import recipe_loader 84 from . import recipe_api
85 from slave import recipe_test_api
86 from slave import recipe_util
87 from slave import recipe_api
88 85
89 86
90 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) 87 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
91 88
92 89
93 class StepPresentation(object): 90 class StepPresentation(object):
94 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION')) 91 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION'))
95 92
96 def __init__(self): 93 def __init__(self):
97 self._finalized = False 94 self._finalized = False
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after
277 274
278 275
279 def get_callable_name(func): 276 def get_callable_name(func):
280 """Returns __name__ of a callable, handling functools.partial types.""" 277 """Returns __name__ of a callable, handling functools.partial types."""
281 if isinstance(func, functools.partial): 278 if isinstance(func, functools.partial):
282 return get_callable_name(func.func) 279 return get_callable_name(func.func)
283 else: 280 else:
284 return func.__name__ 281 return func.__name__
285 282
286 283
287 def get_args(argv):
288 """Process command-line arguments."""
289
290 parser = optparse.OptionParser(
291 description='Entry point for annotated builds.')
292 parser.add_option('--build-properties',
293 action='callback', callback=chromium_utils.convert_json,
294 type='string', default={},
295 help='build properties in JSON format')
296 parser.add_option('--factory-properties',
297 action='callback', callback=chromium_utils.convert_json,
298 type='string', default={},
299 help='factory properties in JSON format')
300 parser.add_option('--build-properties-gz',
301 action='callback', callback=chromium_utils.convert_gz_json,
302 type='string', default={}, dest='build_properties',
303 help='build properties in b64 gz JSON format')
304 parser.add_option('--factory-properties-gz',
305 action='callback', callback=chromium_utils.convert_gz_json,
306 type='string', default={}, dest='factory_properties',
307 help='factory properties in b64 gz JSON format')
308 parser.add_option('--keep-stdin', action='store_true', default=False,
309 help='don\'t close stdin when running recipe steps')
310 return parser.parse_args(argv)
311
312
313 def main(argv=None):
314 opts, _ = get_args(argv)
315
316 stream = annotator.StructuredAnnotationStream()
317 universe = recipe_loader.RecipeUniverse()
318
319 ret = run_steps(stream, opts.build_properties, opts.factory_properties,
320 universe)
321 return ret.status_code
322
323
324 # Return value of run_steps and RecipeEngine.run. 284 # Return value of run_steps and RecipeEngine.run.
325 RecipeExecutionResult = collections.namedtuple( 285 RecipeExecutionResult = collections.namedtuple(
326 'RecipeExecutionResult', 'status_code steps_ran') 286 'RecipeExecutionResult', 'status_code steps_ran')
327 287
328 288
329 def get_recipe_properties(factory_properties, build_properties): 289 def run_steps(properties,
330 """Constructs the recipe's properties from buildbot's properties. 290 stream,
331 291 universe,
332 This merges factory_properties and build_properties. Furthermore, it 292 test_data=recipe_test_api.DisabledTestData()):
333 tries to reconstruct the 'recipe' property from builders.pyl if it isn't
334 already there, and in that case merges in properties form builders.pyl.
335 """
336 properties = factory_properties.copy()
337 properties.update(build_properties)
338
339 # Try to reconstruct the recipe from builders.pyl if not given.
340 if 'recipe' not in properties:
341 mastername = properties['mastername']
342 buildername = properties['buildername']
343
344 master_path = chromium_utils.MasterPath(mastername)
345 builders_file = os.path.join(master_path, 'builders.pyl')
346 if os.path.isfile(builders_file):
347 builders = chromium_utils.ReadBuildersFile(builders_file)
348 assert buildername in builders['builders'], (
349 'buildername %s is not listed in %s' % (buildername, builders_file))
350 builder = builders['builders'][buildername]
351
352 # Update properties with builders.pyl data.
353 properties['recipe'] = builder['recipe']
354 properties.update(builder.get('properties', {}))
355 else:
356 raise LookupError('Cannot find recipe for %s on %s' %
357 (build_properties['buildername'],
358 build_properties['mastername']))
359 return properties
360
361
362 def run_steps(stream, build_properties, factory_properties,
363 universe, test_data=recipe_test_api.DisabledTestData()):
364 """Returns a tuple of (status_code, steps_ran). 293 """Returns a tuple of (status_code, steps_ran).
365 294
366 Only one of these values will be set at a time. This is mainly to support the 295 Only one of these values will be set at a time. This is mainly to support the
367 testing interface used by unittests/recipes_test.py. 296 testing interface used by unittests/recipes_test.py.
368 """ 297 """
369 stream.honor_zero_return_code() 298 stream.honor_zero_return_code()
370 299
371 # TODO(iannucci): Stop this when blamelist becomes sane data. 300 # TODO(iannucci): Stop this when blamelist becomes sane data.
372 if ('blamelist_real' in build_properties and 301 if ('blamelist_real' in properties and
373 'blamelist' in build_properties): 302 'blamelist' in properties):
374 build_properties['blamelist'] = build_properties['blamelist_real'] 303 properties['blamelist'] = properties['blamelist_real']
375 del build_properties['blamelist_real'] 304 del properties['blamelist_real']
376 305
377 # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by 306 # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by
378 # 'patch_project'. 'root' had Rietveld knowing about the implementation of 307 # 'patch_project'. 'root' had Rietveld knowing about the implementation of
379 # the builders. 'patch_project' lets the builder (recipe) decide its own 308 # the builders. 'patch_project' lets the builder (recipe) decide its own
380 # destiny. 309 # destiny.
381 build_properties.pop('root', None) 310 properties.pop('root', None)
382
383 properties = get_recipe_properties(
384 factory_properties=factory_properties,
385 build_properties=build_properties)
386 311
387 # TODO(iannucci): A much better way to do this would be to dynamically 312 # TODO(iannucci): A much better way to do this would be to dynamically
388 # detect if the mirrors are actually available during the execution of the 313 # detect if the mirrors are actually available during the execution of the
389 # recipe. 314 # recipe.
390 if ('use_mirror' not in properties and ( 315 if ('use_mirror' not in properties and (
391 'TESTING_MASTERNAME' in os.environ or 316 'TESTING_MASTERNAME' in os.environ or
392 'TESTING_SLAVENAME' in os.environ)): 317 'TESTING_SLAVENAME' in os.environ)):
393 properties['use_mirror'] = False 318 properties['use_mirror'] = False
394 319
395 # It's an integration point with a new recipe engine that can run steps 320 # It's an integration point with a new recipe engine that can run steps
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
435 except recipe_loader.NoSuchRecipe as e: 360 except recipe_loader.NoSuchRecipe as e:
436 s.step_text('<br/>recipe not found: %s' % e) 361 s.step_text('<br/>recipe not found: %s' % e)
437 s.step_failure() 362 s.step_failure()
438 return RecipeExecutionResult(2, None) 363 return RecipeExecutionResult(2, None)
439 364
440 # Run the steps emitted by a recipe via the engine, emitting annotations 365 # Run the steps emitted by a recipe via the engine, emitting annotations
441 # into |stream| along the way. 366 # into |stream| along the way.
442 return engine.run(steps, api) 367 return engine.run(steps, api)
443 368
444 369
370 def _merge_envs(original, override):
371 """Merges two environments.
372
373 Returns a new environment dict with entries from |override| overwriting
374 corresponding entries in |original|. Keys whose value is None will completely
375 remove the environment variable. Values can contain %(KEY)s strings, which
376 will be substituted with the values from the original (useful for amending, as
377 opposed to overwriting, variables like PATH).
378 """
379 result = original.copy()
380 if not override:
381 return result
382 for k, v in override.items():
383 if v is None:
384 if k in result:
385 del result[k]
386 else:
387 result[str(k)] = str(v) % original
388 return result
389
390
391 def _print_step(step, env, stream):
392 """Prints the step command and relevant metadata.
393
394 Intended to be similar to the information that Buildbot prints at the
395 beginning of each non-annotator step.
396 """
397 step_info_lines = []
398 step_info_lines.append(' '.join(step['cmd']))
399 step_info_lines.append('in dir %s:' % (step['cwd'] or os.getcwd()))
400 for key, value in sorted(step.items()):
401 if value is not None:
402 if callable(value):
403 # This prevents functions from showing up as:
404 # '<function foo at 0x7f523ec7a410>'
405 # which is tricky to test.
406 value = value.__name__+'(...)'
407 step_info_lines.append(' %s: %s' % (key, value))
408 step_info_lines.append('full environment:')
409 for key, value in sorted(env.items()):
410 step_info_lines.append(' %s: %s' % (key, value))
411 step_info_lines.append('')
412 stream.emit('\n'.join(step_info_lines))
413
414
415 @contextlib.contextmanager
416 def _modify_lookup_path(path):
417 """Places the specified path into os.environ.
418
419 Necessary because subprocess.Popen uses os.environ to perform lookup on the
420 supplied command, and only uses the |env| kwarg for modifying the environment
421 of the child process.
422 """
423 saved_path = os.environ['PATH']
424 try:
425 if path is not None:
426 os.environ['PATH'] = path
427 yield
428 finally:
429 os.environ['PATH'] = saved_path
430
431
432 def _normalize_change(change):
433 assert isinstance(change, dict), 'Change is not a dict'
434 change = change.copy()
435
436 # Convert when_timestamp to UNIX timestamp.
437 when = change.get('when_timestamp')
438 if isinstance(when, datetime.datetime):
439 when = calendar.timegm(when.utctimetuple())
440 change['when_timestamp'] = when
441
442 return change
443
444
445 def _trigger_builds(step, trigger_specs):
446 assert trigger_specs is not None
447 for trig in trigger_specs:
448 builder_name = trig.get('builder_name')
449 if not builder_name:
450 raise ValueError('Trigger spec: builder_name is not set')
451
452 changes = trig.get('buildbot_changes', [])
453 assert isinstance(changes, list), 'buildbot_changes must be a list'
454 changes = map(_normalize_change, changes)
455
456 step.step_trigger(json.dumps({
457 'builderNames': [builder_name],
458 'bucket': trig.get('bucket'),
459 'changes': changes,
460 'properties': trig.get('properties'),
461 }, sort_keys=True))
462
463
464 def _run_annotated_step(
465 stream, name, cmd, cwd=None, env=None, allow_subannotations=False,
466 trigger_specs=None, **kwargs):
467 """Runs a single step.
468
469 Context:
470 stream: StructuredAnnotationStream to use to emit step
471
472 Step parameters:
473 name: name of the step, will appear in buildbots waterfall
474 cmd: command to run, list of one or more strings
475 cwd: absolute path to working directory for the command
476 env: dict with overrides for environment variables
477 allow_subannotations: if True, lets the step emit its own annotations
478 trigger_specs: a list of trigger specifications, which are dict with keys:
479 properties: a dict of properties.
480 Buildbot requires buildername property.
481
482 Known kwargs:
483 stdout: Path to a file to put step stdout into. If used, stdout won't appear
484 in annotator's stdout (and |allow_subannotations| is ignored).
485 stderr: Path to a file to put step stderr into. If used, stderr won't appear
486 in annotator's stderr.
487 stdin: Path to a file to read step stdin from.
488
489 Returns the returncode of the step.
490 """
491 if isinstance(cmd, basestring):
492 cmd = (cmd,)
493 cmd = map(str, cmd)
494
495 # For error reporting.
496 step_dict = kwargs.copy()
497 step_dict.update({
498 'name': name,
499 'cmd': cmd,
500 'cwd': cwd,
501 'env': env,
502 'allow_subannotations': allow_subannotations,
503 })
504 step_env = _merge_envs(os.environ, env)
505
506 step_annotation = stream.step(name)
507 step_annotation.step_started()
508
509 _print_step(step_dict, step_env, stream)
510 returncode = 0
511 if cmd:
512 try:
513 # Open file handles for IO redirection based on file names in step_dict.
514 fhandles = {
515 'stdout': subprocess.PIPE,
516 'stderr': subprocess.PIPE,
517 'stdin': None,
518 }
519 for key in fhandles:
520 if key in step_dict:
521 fhandles[key] = open(step_dict[key],
522 'rb' if key == 'stdin' else 'wb')
523
524 if sys.platform.startswith('win'):
525 # Windows has a bad habit of opening a dialog when a console program
526 # crashes, rather than just letting it crash. Therefore, when a program
527 # crashes on Windows, we don't find out until the build step times out.
528 # This code prevents the dialog from appearing, so that we find out
529 # immediately and don't waste time waiting for a user to close the
530 # dialog.
531 import ctypes
532 # SetErrorMode(SEM_NOGPFAULTERRORBOX). For more information, see:
533 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx
534 ctypes.windll.kernel32.SetErrorMode(0x0002)
535 # CREATE_NO_WINDOW. For more information, see:
536 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
537 creationflags = 0x8000000
538 else:
539 creationflags = 0
540
541 with _modify_lookup_path(step_env.get('PATH')):
542 proc = subprocess.Popen(
543 cmd,
544 env=step_env,
545 cwd=cwd,
546 universal_newlines=True,
547 creationflags=creationflags,
548 **fhandles)
549
550 # Safe to close file handles now that subprocess has inherited them.
551 for handle in fhandles.itervalues():
552 if isinstance(handle, file):
553 handle.close()
554
555 outlock = threading.Lock()
556 def filter_lines(lock, allow_subannotations, inhandle, outhandle):
557 while True:
558 line = inhandle.readline()
559 if not line:
560 break
561 lock.acquire()
562 try:
563 if not allow_subannotations and line.startswith('@@@'):
564 outhandle.write('!')
565 outhandle.write(line)
566 outhandle.flush()
567 finally:
568 lock.release()
569
570 # Pump piped stdio through filter_lines. IO going to files on disk is
571 # not filtered.
572 threads = []
573 for key in ('stdout', 'stderr'):
574 if fhandles[key] == subprocess.PIPE:
575 inhandle = getattr(proc, key)
576 outhandle = getattr(sys, key)
577 threads.append(threading.Thread(
578 target=filter_lines,
579 args=(outlock, allow_subannotations, inhandle, outhandle)))
580
581 for th in threads:
582 th.start()
583 proc.wait()
584 for th in threads:
585 th.join()
586 returncode = proc.returncode
587 except OSError:
588 # File wasn't found, error will be reported to stream when the exception
589 # crosses the context manager.
590 step_annotation.step_exception_occured(*sys.exc_info())
591 raise
592
593 # TODO(martiniss) move logic into own module?
594 if trigger_specs:
595 _trigger_builds(step_annotation, trigger_specs)
596
597 return step_annotation, returncode
598
599
445 class RecipeEngine(object): 600 class RecipeEngine(object):
446 """Knows how to execute steps emitted by a recipe, holds global state such as 601 """Knows how to execute steps emitted by a recipe, holds global state such as
447 step history and build properties. Each recipe module API has a reference to 602 step history and build properties. Each recipe module API has a reference to
448 this object. 603 this object.
449 604
450 Recipe modules that are aware of the engine: 605 Recipe modules that are aware of the engine:
451 * properties - uses engine.properties. 606 * properties - uses engine.properties.
452 * step_history - uses engine.step_history. 607 * step_history - uses engine.step_history.
453 * step - uses engine.create_step(...). 608 * step - uses engine.create_step(...).
454 609
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after
549 step_test = self._test_data.pop_step_test_data(step['name'], 704 step_test = self._test_data.pop_step_test_data(step['name'],
550 test_data_fn) 705 test_data_fn)
551 placeholders = render_step(step, step_test) 706 placeholders = render_step(step, step_test)
552 707
553 self._step_history[step['name']] = step 708 self._step_history[step['name']] = step
554 self._emit_results() 709 self._emit_results()
555 710
556 step_result = None 711 step_result = None
557 712
558 if not self._test_data.enabled: 713 if not self._test_data.enabled:
559 self._previous_step_annotation, retcode = annotator.run_step( 714 self._previous_step_annotation, retcode = _run_annotated_step(
560 self._stream, **step) 715 self._stream, **step)
561 716
562 step_result = StepData(step, retcode) 717 step_result = StepData(step, retcode)
563 self._previous_step_annotation.annotation_stream.step_cursor(step['name']) 718 self._previous_step_annotation.annotation_stream.step_cursor(step['name'])
564 else: 719 else:
565 self._previous_step_annotation = annotation = self._stream.step( 720 self._previous_step_annotation = annotation = self._stream.step(
566 step['name']) 721 step['name'])
567 annotation.step_started() 722 annotation.step_started()
568 try: 723 try:
569 annotation.stream = cStringIO.StringIO() 724 annotation.stream = cStringIO.StringIO()
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after
643 self._step_history[final_result['name']] = final_result 798 self._step_history[final_result['name']] = final_result
644 799
645 return RecipeExecutionResult(retcode, self._step_history) 800 return RecipeExecutionResult(retcode, self._step_history)
646 801
647 def create_step(self, step): # pylint: disable=R0201 802 def create_step(self, step): # pylint: disable=R0201
648 # This version of engine doesn't do anything, just converts step to dict 803 # This version of engine doesn't do anything, just converts step to dict
649 # (that is consumed by annotator engine). 804 # (that is consumed by annotator engine).
650 return step.as_jsonish() 805 return step.as_jsonish()
651 806
652 807
653 class ParallelRecipeEngine(RecipeEngine):
654 """New engine that knows how to run steps in parallel.
655
656 TODO(vadimsh): Implement it.
657 """
658
659 def __init__(self, stream, properties, test_data):
660 super(ParallelRecipeEngine, self).__init__()
661 self._stream = stream
662 self._properties = properties
663 self._test_data = test_data
664
665 @property
666 def properties(self):
667 return self._properties
668
669 def run(self, steps_function, api):
670 raise NotImplementedError
671
672 def create_step(self, step):
673 raise NotImplementedError
674
675
676 def update_scripts():
677 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'):
678 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS')
679 return False
680
681 stream = annotator.StructuredAnnotationStream()
682
683 with stream.step('update_scripts') as s:
684 build_root = os.path.join(SCRIPT_PATH, '..', '..')
685 gclient_name = 'gclient'
686 if sys.platform.startswith('win'):
687 gclient_name += '.bat'
688 gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name)
689 gclient_cmd = [gclient_path, 'sync', '--force', '--verbose']
690 cmd_dict = {
691 'name': 'update_scripts',
692 'cmd': gclient_cmd,
693 'cwd': build_root,
694 }
695 annotator.print_step(cmd_dict, os.environ, stream)
696 if subprocess.call(gclient_cmd, cwd=build_root) != 0:
697 s.step_text('gclient sync failed!')
698 s.step_warnings()
699 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1'
700
701 # After running update scripts, set PYTHONIOENCODING=UTF-8 for the real
702 # annotated_run.
703 os.environ['PYTHONIOENCODING'] = 'UTF-8'
704
705 return True
706
707
708 def shell_main(argv):
709 if update_scripts():
710 return subprocess.call([sys.executable] + argv)
711 else:
712 return main(argv)
713
714
715 if __name__ == '__main__':
716 sys.exit(shell_main(sys.argv))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698