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

Side by Side Diff: scripts/slave/annotated_run.py

Issue 15270004: Add step generator protocol, remove annotated_checkout, remove script. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Checkout blobs do not need to be generators Created 7 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
11 AddAnnotatedScript step. That step (found in annotator_commands.py) calls 11 AddAnnotatedScript step. That step (found in annotator_commands.py) calls
12 this script with the build- and factory-properties passed on the command 12 this script with the build- and factory-properties passed on the command
13 line. In general, the factory properties will include one or more other 13 line.
14 scripts for this script to delegate to.
15 14
16 The main mode of operation is for factory_properties to contain a single 15 The main mode of operation is for factory_properties to contain a single
17 property 'recipe' whose value is the basename (without extension) of a python 16 property 'recipe' whose value is the basename (without extension) of a python
18 script in one of the following locations (looked up in this order): 17 script in one of the following locations (looked up in this order):
19 * build_internal/scripts/slave-internal/recipes 18 * build_internal/scripts/slave-internal/recipes
20 * build_internal/scripts/slave/recipes 19 * build_internal/scripts/slave/recipes
21 * build/scripts/slave/recipes 20 * build/scripts/slave/recipes
22 21
23 For example, these factory_properties would run the 'run_presubmit' recipe 22 For example, these factory_properties would run the 'run_presubmit' recipe
24 located in build/scripts/slave/recipes: 23 located in build/scripts/slave/recipes:
25 { 'recipe': 'run_presubmit' } 24 { 'recipe': 'run_presubmit' }
26 25
27 Annotated_run.py will then import the recipe and expect to call a function whose 26 Annotated_run.py will then import the recipe and expect to call a function whose
28 signature is GetFactoryProperties(build_properties) -> factory_properties. The 27 signature is:
29 returned factory_properties will then be used to execute the following actions: 28 GetSteps(api, factory_properties, build_properties) -> iterable_of_things.
30 * optional 'checkout' 29
31 * This checks out a gclient/git/svn spec into the slave build dir. 30 Items in iterable_of_things must be one of:
32 * The value of checkout is expected to be in ('gclient', 'git', 'svn') 31 * A step dictionary (as accepted by annotator.py)
33 * If checkout is specified, annotated_run will also expect to find a value 32 * A sequence of step dictionaries
34 for ('%s_spec' % checkout), e.g. 'gclient_spec'. The value of this spec 33 * A step generator
35 is defined by build/scripts/slave/annotated_checkout.py. 34
36 * 'script' or 'steps' 35 A step generator is called with the following protocol:
37 * 'script' allows you to specify a single script which will be invoked with 36 * The generator is initialized with 'step_history' and 'failed'.
38 build-properties and factory-properties. 37 * Each iteration of the generator is passed the current value of 'failed'.
39 * 'steps' serves as input for build/scripts/common/annotator.py 38
40 * You can have annotated_run pass build/factory properties to a step by 39 On each iteration, a step generator may yield:
41 using the recipe_util.step() function. 40 * A single step dictionary
41 * A sequence of step dictionaries
42 * If a sequence of dictionaries is yielded, and the first step dictionary
43 does not have a 'seed_steps' key, the first step will be augmented with
44 a 'seed_steps' key containing the names of all the steps in the sequence.
45
46 For steps yielded by the generator, if annotated_run enters the failed state,
47 it will only continue to call the generator if the generator sets the
48 'keep_going' key on the steps which it has produced. Otherwise annoated_run will
49 cease calling the generator and move on to the next item in iterable_of_things.
50
51 'step_history' is an OrderedDict of {stepname -> StepData}, always representing
52 the current history of what steps have run, what they returned, and any
53 json data they emitted.
54
55 'failed' is a boolean representing if the build is in a 'failed' state.
42 """ 56 """
43 57
44 import contextlib 58 import contextlib
45 import json 59 import json
46 import optparse 60 import optparse
47 import os 61 import os
48 import subprocess 62 import subprocess
49 import sys 63 import sys
50 import tempfile 64 import tempfile
51 65
52 from collections import namedtuple 66 from collections import namedtuple, OrderedDict
67 from itertools import islice
53 68
54 from common import annotator 69 from common import annotator
55 from common import chromium_utils 70 from common import chromium_utils
56 from slave import recipe_util 71 from slave import recipe_util
57 from slave import annotated_checkout
58 72
59 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) 73 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
60 BUILD_ROOT = os.path.dirname(os.path.dirname(SCRIPT_PATH)) 74 BUILD_ROOT = os.path.dirname(os.path.dirname(SCRIPT_PATH))
61 75
62 76
63 @contextlib.contextmanager 77 @contextlib.contextmanager
64 def temp_purge_path(path): 78 def temp_purge_path(path):
65 saved = sys.path 79 saved = sys.path
66 sys.path = [path] 80 sys.path = [path]
67 try: 81 try:
68 yield 82 yield
69 finally: 83 finally:
70 sys.path = saved 84 sys.path = saved
71 85
72 86
87 class StepData(object):
88 __slots__ = ['step', 'retcode', 'json_data']
89 def __init__(self, step=None, retcode=None, json_data=None):
90 self.step = step
91 self.retcode = retcode
92 self.json_data = json_data
93
94
73 def expand_root_placeholder(root, lst): 95 def expand_root_placeholder(root, lst):
74 """This expands CheckoutRootPlaceholder in paths to a real path. 96 """This expands CheckoutRootPlaceholder in paths to a real path.
75 See recipe_util.checkout_path() for usage.""" 97 See recipe_util.checkout_path() for usage."""
76 ret = [] 98 ret = []
77 replacements = {'CheckoutRootPlaceholder': root} 99 replacements = {'CheckoutRootPlaceholder': root}
78 for item in lst: 100 for item in lst:
79 if isinstance(item, basestring): 101 if isinstance(item, basestring):
80 if '%(CheckoutRootPlaceholder)s' in item: 102 if '%(CheckoutRootPlaceholder)s' in item:
81 assert root, 'Must use "checkout" key to use checkout_path().' 103 assert root, 'Must use "checkout" key to use checkout_path().'
82 ret.append(item % replacements) 104 ret.append(item % replacements)
83 continue 105 continue
84 ret.append(item) 106 ret.append(item)
85 return ret 107 return ret
86 108
87 109
110 def fixup_seed_steps(sequence):
111 """Takes a sequence of step dict's and adds seed_steps to the first entry
112 if appropriate."""
113 if sequence and 'seed_steps' not in sequence[0]:
114 sequence[0]['seed_steps'] = [x['name'] for x in sequence]
115
116
88 def get_args(argv): 117 def get_args(argv):
89 """Process command-line arguments.""" 118 """Process command-line arguments."""
90 119
91 parser = optparse.OptionParser( 120 parser = optparse.OptionParser(
92 description='Entry point for annotated builds.') 121 description='Entry point for annotated builds.')
93 parser.add_option('--build-properties', 122 parser.add_option('--build-properties',
94 action='callback', callback=chromium_utils.convert_json, 123 action='callback', callback=chromium_utils.convert_json,
95 type='string', default={}, 124 type='string', default={},
96 help='build properties in JSON format') 125 help='build properties in JSON format')
97 parser.add_option('--factory-properties', 126 parser.add_option('--factory-properties',
98 action='callback', callback=chromium_utils.convert_json, 127 action='callback', callback=chromium_utils.convert_json,
99 type='string', default={}, 128 type='string', default={},
100 help='factory properties in JSON format') 129 help='factory properties in JSON format')
101 parser.add_option('--keep-stdin', action='store_true', default=False, 130 parser.add_option('--keep-stdin', action='store_true', default=False,
102 help='don\'t close stdin when running recipe steps') 131 help='don\'t close stdin when running recipe steps')
103 return parser.parse_args(argv) 132 return parser.parse_args(argv)
104 133
105 134
106 def main(argv=None): 135 def main(argv=None):
107 opts, _ = get_args(argv) 136 opts, _ = get_args(argv)
108 137
109 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build']) 138 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build'])
110 139
111 ret = make_steps(stream, opts.build_properties, opts.factory_properties) 140 return run_steps(stream, opts.build_properties, opts.factory_properties)[0]
Mike Stip (use stip instead) 2013/05/18 00:37:22 This is a MakeStepsRetval, right? Shouldn't we use
iannucci 2013/05/18 04:00:36 Good catch. Done.
112 assert ret.script is None, "Unexpectedly got script from make_steps?"
113 141
114 if ret.status_code:
115 return ret
116 else:
117 return run_annotator(stream, ret.steps, opts.keep_stdin)
118 142
119 def make_steps(stream, build_properties, factory_properties, 143 def run_steps(stream, build_properties, factory_properties, test_data=None):
120 test_mode=False): 144 """Returns a tuple of (status_code, steps_ran).
121 """Returns a namedtuple of (status_code, script, steps).
122 145
123 Only one of these values will be set at a time. This is mainly to support the 146 Only one of these values will be set at a time. This is mainly to support the
124 testing interface used by unittests/recipes_test.py. In particular, unless 147 testing interface used by unittests/recipes_test.py.
125 test_mode is set, this function should never return a value for script. 148
149 test_data should be a dictionary of step_name -> (retcode, json_data)
126 """ 150 """
127 MakeStepsRetval = namedtuple('MakeStepsRetval', 'status_code script steps') 151 MakeStepsRetval = namedtuple('MakeStepsRetval', 'status_code steps_ran')
128 152
129 # TODO(iannucci): Stop this when blamelist becomes sane data. 153 # TODO(iannucci): Stop this when blamelist becomes sane data.
130 if ('blamelist_real' in build_properties and 154 if ('blamelist_real' in build_properties and
131 'blamelist' in build_properties): 155 'blamelist' in build_properties):
132 build_properties['blamelist'] = build_properties['blamelist_real'] 156 build_properties['blamelist'] = build_properties['blamelist_real']
133 del build_properties['blamelist_real'] 157 del build_properties['blamelist_real']
134 158
135 with stream.step('setup_build') as s: 159 with stream.step('setup_build') as s:
136 assert 'recipe' in factory_properties 160 assert 'recipe' in factory_properties
137 recipe = factory_properties['recipe'] 161 recipe = factory_properties['recipe']
138 recipe_dirs = (os.path.abspath(p) for p in ( 162 recipe_dirs = (os.path.abspath(p) for p in (
139 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts', 163 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts',
140 'slave-internal', 'recipes'), 164 'slave-internal', 'recipes'),
141 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts', 165 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts',
142 'slave', 'recipes'), 166 'slave', 'recipes'),
143 os.path.join(SCRIPT_PATH, 'recipes'), 167 os.path.join(SCRIPT_PATH, 'recipes'),
144 )) 168 ))
145 169
146 for path in recipe_dirs: 170 for path in recipe_dirs:
147 recipe_module = None 171 recipe_module = None
148 with temp_purge_path(path): 172 with temp_purge_path(path):
149 try: 173 try:
150 recipe_module = __import__(recipe, globals(), locals()) 174 recipe_module = __import__(recipe, globals(), locals())
151 except ImportError: 175 except ImportError:
152 continue 176 continue
153 recipe_dict = recipe_module.GetFactoryProperties( 177 steps = recipe_module.GetSteps(
154 recipe_util, 178 recipe_util,
155 factory_properties.copy(), 179 factory_properties.copy(),
156 build_properties.copy()) 180 build_properties.copy())
181 assert isinstance(steps, (list, tuple))
157 break 182 break
158 else: 183 else:
159 s.step_text('recipe not found') 184 s.step_text('recipe not found')
160 s.step_failure() 185 s.step_failure()
161 return MakeStepsRetval(1, None, None) 186 return MakeStepsRetval(1, None)
162
163 factory_properties.update(recipe_dict)
164
165 # If a checkout is specified, get its type and spec and pass them
166 # off to annotated_checkout.py to actually fetch the repo.
167 # annotated_checkout.py handles its own StructuredAnnotationStream.
168 root = None
169 if 'checkout' in factory_properties:
170 checkout_type = factory_properties['checkout']
171 checkout_spec = factory_properties['%s_spec' % checkout_type]
172 ret, root = annotated_checkout.run(checkout_type, checkout_spec,
173 test_mode)
174 if ret != 0:
175 return MakeStepsRetval(ret, None, None)
176 if test_mode:
177 root = '[BUILD_ROOT]'+root[len(BUILD_ROOT):]
178
179 assert ('script' in factory_properties) ^ ('steps' in factory_properties)
180 ret = 0
181
182 # If a script is specified, import it, execute its GetSteps method,
183 # and pass those steps forward so they get executed by annotator.py.
184 # If we're in test_mode mode, just return the script.
185 if 'script' in factory_properties:
186 with stream.step('get_steps') as s:
187 assert isinstance(factory_properties['script'], str)
188 [script] = expand_root_placeholder(root, [factory_properties['script']])
189 if test_mode:
190 return MakeStepsRetval(None, script, None)
191 assert os.path.abspath(script) == script
192
193 with temp_purge_path(os.path.dirname(script)):
194 try:
195 script_name = os.path.splitext(os.path.basename(script))[0]
196 script_module = __import__(script_name, globals(), locals())
197 except ImportError:
198 s.step_text('script not found')
199 s.step_failure()
200 return MakeStepsRetval(1, None, None)
201 steps_dict = script_module.GetSteps(recipe_util,
202 factory_properties.copy(),
203 build_properties.copy())
204 factory_properties['steps'] = steps_dict
205 187
206 # Execute annotator.py with steps if specified. 188 # Execute annotator.py with steps if specified.
207 # annotator.py handles the seeding, execution, and annotation of each step. 189 # annotator.py handles the seeding, execution, and annotation of each step.
208 if 'steps' in factory_properties: 190 factory_properties_str = json.dumps(factory_properties)
209 steps = factory_properties.pop('steps') 191 build_properties_str = json.dumps(build_properties)
210 factory_properties_str = json.dumps(factory_properties) 192 property_placeholder_lst = [
211 build_properties_str = json.dumps(build_properties) 193 '--factory-properties', factory_properties_str,
212 property_placeholder_lst = [ 194 '--build-properties', build_properties_str]
213 '--factory-properties', factory_properties_str, 195
214 '--build-properties', build_properties_str] 196 failed = False
215 for step in steps: 197 step_history = OrderedDict()
216 new_cmd = [] 198 step_history.last_step = lambda: step_history[next(reversed(step_history))]
Mike Stip (use stip instead) 2013/05/18 00:37:22 document these
iannucci 2013/05/18 04:00:36 Done.
217 for item in expand_root_placeholder(root, step['cmd']): 199 step_history.nth_step = (
218 if item == recipe_util.PropertyPlaceholder: 200 lambda n, default: next(islice(step_history.iteritems(), n, None), default))
219 new_cmd.extend(property_placeholder_lst) 201
202 def step_generator_wrapper():
Mike Stip (use stip instead) 2013/05/18 00:37:22 small doc saying this demuxes a single step, a lis
iannucci 2013/05/18 04:00:36 Done.
203 for thing in steps:
204 if isinstance(thing, dict):
205 # single static step
206 yield thing
207 elif isinstance(thing, (list, tuple)):
208 # multiple static steps
209 fixup_seed_steps(thing)
210 for step in thing:
211 yield step
212 else:
213 # step generator
214 step_iter = thing(step_history, failed)
215 first = True
216 try:
217 while True:
218 # Cannot pass non-None to first generator call.
219 step_or_steps = step_iter.send(failed if not first else None)
220 first = False
221
222 if isinstance(step_or_steps, (list, tuple)):
223 gensteps = step_or_steps
Mike Stip (use stip instead) 2013/05/18 00:37:22 is there a way you can unify this code with lines
iannucci 2013/05/18 04:00:36 Done. For bonus points, I refactored these nested
224 fixup_seed_steps(gensteps)
225 else:
226 gensteps = [step_or_steps]
227
228 for step in gensteps:
229 keep_going = step.pop('keep_going', False)
230 yield step
231 if failed and not keep_going:
232 raise StopIteration
233 except StopIteration:
234 pass
235
236 root = None
237 for step in step_generator_wrapper():
238 json_output_fd = json_output_name = None
239 new_cmd = []
240 for item in expand_root_placeholder(root, step['cmd']):
241 if item == recipe_util.PropertyPlaceholder:
242 new_cmd.extend(property_placeholder_lst)
243 elif item == recipe_util.JsonOutputPlaceholder:
244 new_cmd.append('--output-json')
245 if test_data:
246 new_cmd.append('/path/to/tmp/json')
220 else: 247 else:
221 new_cmd.append(item) 248 assert not json_output_name, (
222 step['cmd'] = new_cmd 249 'Can only use json_output_file once per step' % step)
223 if 'cwd' in step: 250 json_output_fd, json_output_name = tempfile.mkstemp()
224 [new_cwd] = expand_root_placeholder(root, [step['cwd']]) 251 new_cmd.append(json_output_name)
225 step['cwd'] = new_cwd 252 else:
253 new_cmd.append(item)
254 step['cmd'] = new_cmd
255 if 'cwd' in step:
256 [new_cwd] = expand_root_placeholder(root, [step['cwd']])
257 step['cwd'] = new_cwd
226 258
227 return MakeStepsRetval(None, None, steps) 259 json_data = step.pop('static_json_data', {})
260 assert not(json_data and json_output_name), (
261 "Cannot have both static_json_data as well as dynamic json_data")
262 if test_data is None:
263 failed, [retcode] = annotator.run_steps([step], failed)
264 if json_output_name:
265 try:
266 json_data = json.load(os.fdopen(json_output_fd, 'r'))
267 except ValueError:
268 pass
269 else:
270 retcode, potential_json_data = test_data.pop(step['name'], (0, {}))
271 json_data = json_data or potential_json_data
272 failed = failed or retcode != 0
228 273
229 def run_annotator(stream, steps, keep_stdin): 274 # Support CheckoutRootPlaceholder.
230 ret = 0 275 root = root or json_data.get('CheckoutRoot', None)
231 annotator_path = os.path.join(
232 os.path.dirname(SCRIPT_PATH), 'common', 'annotator.py')
233 tmpfile, tmpname = tempfile.mkstemp()
234 try:
235 cmd = [sys.executable, annotator_path, tmpname]
236 step_doc = json.dumps(steps)
237 with os.fdopen(tmpfile, 'wb') as f:
238 f.write(step_doc)
239 with stream.step('annotator_preamble'):
240 print 'in %s executing: %s' % (os.getcwd(), ' '.join(cmd))
241 print 'with: %s' % step_doc
242 if keep_stdin:
243 ret = subprocess.call(cmd)
244 else:
245 proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
246 proc.communicate('')
247 ret = proc.returncode
248 finally:
249 os.unlink(tmpname)
250 276
251 return ret 277 assert step['name'] not in step_history
278 step_history[step['name']] = StepData(step, retcode, json_data)
279
280 assert test_data is None or test_data == {}, (
281 "Unconsumed test data! %s" % (test_data,))
282
283 return MakeStepsRetval(retcode, step_history)
252 284
253 285
254 def UpdateScripts(): 286 def UpdateScripts():
255 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): 287 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'):
256 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') 288 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS')
257 return False 289 return False
258 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts']) 290 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts'])
259 with stream.step('update_scripts') as s: 291 with stream.step('update_scripts') as s:
260 build_root = os.path.join(SCRIPT_PATH, '..', '..') 292 build_root = os.path.join(SCRIPT_PATH, '..', '..')
261 gclient_name = 'gclient' 293 gclient_name = 'gclient'
262 if sys.platform.startswith('win'): 294 if sys.platform.startswith('win'):
263 gclient_name += '.bat' 295 gclient_name += '.bat'
264 gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name) 296 gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name)
265 if subprocess.call([gclient_path, 'sync', '--force'], cwd=build_root) != 0: 297 if subprocess.call([gclient_path, 'sync', '--force'], cwd=build_root) != 0:
266 s.step_text('gclient sync failed!') 298 s.step_text('gclient sync failed!')
267 s.step_warnings() 299 s.step_warnings()
268 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1' 300 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1'
269 return True 301 return True
270 302
271 303
272 if __name__ == '__main__': 304 if __name__ == '__main__':
273 if UpdateScripts(): 305 if UpdateScripts():
274 os.execv(sys.executable, [sys.executable] + sys.argv) 306 os.execv(sys.executable, [sys.executable] + sys.argv)
275 sys.exit(main(sys.argv)) 307 sys.exit(main(sys.argv))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698