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

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: Address comments and change expected json output format 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
« no previous file with comments | « scripts/slave/annotated_checkout.py ('k') | scripts/slave/git_setup.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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. Additionally, the OrderedDict has the following
54 convenience functions defined:
55 * last_step - Returns the last step that ran or None
56 * nth_step(n) - Returns the N'th step that ran or None
57
58 'failed' is a boolean representing if the build is in a 'failed' state.
42 """ 59 """
43 60
61 import collections
44 import contextlib 62 import contextlib
45 import json 63 import json
46 import optparse 64 import optparse
47 import os 65 import os
48 import subprocess 66 import subprocess
49 import sys 67 import sys
50 import tempfile 68 import tempfile
51 69
52 from collections import namedtuple 70 from collections import namedtuple, OrderedDict
71 from itertools import islice
53 72
54 from common import annotator 73 from common import annotator
55 from common import chromium_utils 74 from common import chromium_utils
56 from slave import recipe_util 75 from slave import recipe_util
57 from slave import annotated_checkout
58 76
59 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) 77 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
60 BUILD_ROOT = os.path.dirname(os.path.dirname(SCRIPT_PATH)) 78 BUILD_ROOT = os.path.dirname(os.path.dirname(SCRIPT_PATH))
61 79
62 80
63 @contextlib.contextmanager 81 @contextlib.contextmanager
64 def temp_purge_path(path): 82 def temp_purge_path(path):
65 saved = sys.path 83 saved = sys.path
66 sys.path = [path] 84 sys.path = [path]
67 try: 85 try:
68 yield 86 yield
69 finally: 87 finally:
70 sys.path = saved 88 sys.path = saved
71 89
72 90
91 class StepData(object):
92 __slots__ = ['step', 'retcode', 'json_data']
93 def __init__(self, step=None, retcode=None, json_data=None):
94 self.step = step
95 self.retcode = retcode
96 self.json_data = json_data
97
98
73 def expand_root_placeholder(root, lst): 99 def expand_root_placeholder(root, lst):
74 """This expands CheckoutRootPlaceholder in paths to a real path. 100 """This expands CheckoutRootPlaceholder in paths to a real path.
75 See recipe_util.checkout_path() for usage.""" 101 See recipe_util.checkout_path() for usage."""
76 ret = [] 102 ret = []
77 replacements = {'CheckoutRootPlaceholder': root} 103 replacements = {'CheckoutRootPlaceholder': root}
78 for item in lst: 104 for item in lst:
79 if isinstance(item, basestring): 105 if isinstance(item, basestring):
80 if '%(CheckoutRootPlaceholder)s' in item: 106 if '%(CheckoutRootPlaceholder)s' in item:
81 assert root, 'Must use "checkout" key to use checkout_path().' 107 assert root, 'Must use "checkout" key to use checkout_path().'
82 ret.append(item % replacements) 108 ret.append(item % replacements)
83 continue 109 continue
84 ret.append(item) 110 ret.append(item)
85 return ret 111 return ret
86 112
87 113
114 def fixup_seed_steps(sequence):
115 """Takes a sequence of step dict's and adds seed_steps to the first entry
116 if appropriate."""
117 if sequence and 'seed_steps' not in sequence[0]:
118 sequence[0]['seed_steps'] = [x['name'] for x in sequence]
119
120
121 def ensure_sequence_of_steps(step_or_steps):
122 """Generates one or more fixed steps, given a step or a sequence of steps."""
123 if isinstance(step_or_steps, collections.Sequence):
124 step_seq = step_or_steps
125 fixup_seed_steps(step_seq)
126 for s in step_seq:
127 yield s
128 else:
129 yield step_or_steps
130
131
132 def step_generator_wrapper(steps, step_history, is_failed):
133 """Generates single steps from the non-homogeneous sequence 'steps'.
134
135 Each item in steps may be:
136 * A step (dict)
137 * A sequence of steps
138 * A generator of:
139 * A step
140 * A sequence of steps
141 """
142 for thing in steps:
143 if isinstance(thing, (collections.Sequence, dict)):
144 for step in ensure_sequence_of_steps(thing):
145 yield step
146 else:
147 # step generator
148 step_iter = thing(step_history, is_failed())
149 first = True
150 try:
151 while True:
152 # Cannot pass non-None to first generator call.
153 step_or_steps = step_iter.send(is_failed() if not first else None)
154 first = False
155
156 for step in ensure_sequence_of_steps(step_or_steps):
157 keep_going = step.pop('keep_going', False)
158 yield step
159 if is_failed() and not keep_going:
160 raise StopIteration
161 except StopIteration:
162 pass
163
164
165 def create_state_history():
166 """Returns an OrderedDict with some helper functions attached."""
167 step_history = OrderedDict()
168
169 # Add in some helpers.
170 def last_step():
171 """Returns the last item from step_history, or None."""
172 key = next(reversed(step_history), None)
173 return step_history[key] if key else None
174 step_history.last_step = last_step
175
176 def nth_step(n):
177 """Returns the N'th step from step_history, or None."""
178 return next(islice(step_history.iteritems(), n, None), None)
179 step_history.nth_step = nth_step
180 return step_history
181
182
88 def get_args(argv): 183 def get_args(argv):
89 """Process command-line arguments.""" 184 """Process command-line arguments."""
90 185
91 parser = optparse.OptionParser( 186 parser = optparse.OptionParser(
92 description='Entry point for annotated builds.') 187 description='Entry point for annotated builds.')
93 parser.add_option('--build-properties', 188 parser.add_option('--build-properties',
94 action='callback', callback=chromium_utils.convert_json, 189 action='callback', callback=chromium_utils.convert_json,
95 type='string', default={}, 190 type='string', default={},
96 help='build properties in JSON format') 191 help='build properties in JSON format')
97 parser.add_option('--factory-properties', 192 parser.add_option('--factory-properties',
98 action='callback', callback=chromium_utils.convert_json, 193 action='callback', callback=chromium_utils.convert_json,
99 type='string', default={}, 194 type='string', default={},
100 help='factory properties in JSON format') 195 help='factory properties in JSON format')
101 parser.add_option('--keep-stdin', action='store_true', default=False, 196 parser.add_option('--keep-stdin', action='store_true', default=False,
102 help='don\'t close stdin when running recipe steps') 197 help='don\'t close stdin when running recipe steps')
103 return parser.parse_args(argv) 198 return parser.parse_args(argv)
104 199
105 200
106 def main(argv=None): 201 def main(argv=None):
107 opts, _ = get_args(argv) 202 opts, _ = get_args(argv)
108 203
109 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build']) 204 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build'])
110 205
111 ret = make_steps(stream, opts.build_properties, opts.factory_properties) 206 ret = run_steps(stream, opts.build_properties, opts.factory_properties)
112 assert ret.script is None, "Unexpectedly got script from make_steps?" 207 return ret.status_code
113 208
114 if ret.status_code:
115 return ret
116 else:
117 return run_annotator(stream, ret.steps, opts.keep_stdin)
118 209
119 def make_steps(stream, build_properties, factory_properties, 210 def run_steps(stream, build_properties, factory_properties, test_data=None):
120 test_mode=False): 211 """Returns a tuple of (status_code, steps_ran).
121 """Returns a namedtuple of (status_code, script, steps).
122 212
123 Only one of these values will be set at a time. This is mainly to support the 213 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 214 testing interface used by unittests/recipes_test.py.
125 test_mode is set, this function should never return a value for script. 215
216 test_data should be a dictionary of step_name -> (retcode, json_data)
126 """ 217 """
127 MakeStepsRetval = namedtuple('MakeStepsRetval', 'status_code script steps') 218 MakeStepsRetval = namedtuple('MakeStepsRetval', 'status_code steps_ran')
128 219
129 # TODO(iannucci): Stop this when blamelist becomes sane data. 220 # TODO(iannucci): Stop this when blamelist becomes sane data.
130 if ('blamelist_real' in build_properties and 221 if ('blamelist_real' in build_properties and
131 'blamelist' in build_properties): 222 'blamelist' in build_properties):
132 build_properties['blamelist'] = build_properties['blamelist_real'] 223 build_properties['blamelist'] = build_properties['blamelist_real']
133 del build_properties['blamelist_real'] 224 del build_properties['blamelist_real']
134 225
135 with stream.step('setup_build') as s: 226 with stream.step('setup_build') as s:
136 assert 'recipe' in factory_properties 227 assert 'recipe' in factory_properties
137 recipe = factory_properties['recipe'] 228 recipe = factory_properties['recipe']
138 recipe_dirs = (os.path.abspath(p) for p in ( 229 recipe_dirs = (os.path.abspath(p) for p in (
139 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts', 230 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts',
140 'slave-internal', 'recipes'), 231 'slave-internal', 'recipes'),
141 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts', 232 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', 'scripts',
142 'slave', 'recipes'), 233 'slave', 'recipes'),
143 os.path.join(SCRIPT_PATH, 'recipes'), 234 os.path.join(SCRIPT_PATH, 'recipes'),
144 )) 235 ))
145 236
146 for path in recipe_dirs: 237 for path in recipe_dirs:
147 recipe_module = None 238 recipe_module = None
148 with temp_purge_path(path): 239 with temp_purge_path(path):
149 try: 240 try:
150 recipe_module = __import__(recipe, globals(), locals()) 241 recipe_module = __import__(recipe, globals(), locals())
151 except ImportError: 242 except ImportError:
152 continue 243 continue
153 recipe_dict = recipe_module.GetFactoryProperties( 244 steps = recipe_module.GetSteps(
154 recipe_util, 245 recipe_util,
155 factory_properties.copy(), 246 factory_properties.copy(),
156 build_properties.copy()) 247 build_properties.copy())
248 assert isinstance(steps, (list, tuple))
157 break 249 break
158 else: 250 else:
159 s.step_text('recipe not found') 251 s.step_text('recipe not found')
160 s.step_failure() 252 s.step_failure()
161 return MakeStepsRetval(1, None, None) 253 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 254
206 # Execute annotator.py with steps if specified. 255 # Execute annotator.py with steps if specified.
207 # annotator.py handles the seeding, execution, and annotation of each step. 256 # annotator.py handles the seeding, execution, and annotation of each step.
208 if 'steps' in factory_properties: 257 factory_properties_str = json.dumps(factory_properties)
209 steps = factory_properties.pop('steps') 258 build_properties_str = json.dumps(build_properties)
210 factory_properties_str = json.dumps(factory_properties) 259 property_placeholder_lst = [
211 build_properties_str = json.dumps(build_properties) 260 '--factory-properties', factory_properties_str,
212 property_placeholder_lst = [ 261 '--build-properties', build_properties_str]
213 '--factory-properties', factory_properties_str, 262
214 '--build-properties', build_properties_str] 263 failed = False
215 for step in steps: 264 step_history = create_state_history()
216 new_cmd = [] 265
217 for item in expand_root_placeholder(root, step['cmd']): 266 root = None
218 if item == recipe_util.PropertyPlaceholder: 267 for step in step_generator_wrapper(steps, step_history, lambda: failed):
219 new_cmd.extend(property_placeholder_lst) 268 json_output_fd = json_output_name = None
269 new_cmd = []
270 for item in expand_root_placeholder(root, step['cmd']):
271 if item == recipe_util.PropertyPlaceholder:
272 new_cmd.extend(property_placeholder_lst)
273 elif item == recipe_util.JsonOutputPlaceholder:
274 new_cmd.append('--output-json')
275 if test_data:
276 new_cmd.append('/path/to/tmp/json')
220 else: 277 else:
221 new_cmd.append(item) 278 assert not json_output_name, (
222 step['cmd'] = new_cmd 279 'Can only use json_output_file once per step' % step)
223 if 'cwd' in step: 280 json_output_fd, json_output_name = tempfile.mkstemp()
224 [new_cwd] = expand_root_placeholder(root, [step['cwd']]) 281 new_cmd.append(json_output_name)
225 step['cwd'] = new_cwd 282 else:
283 new_cmd.append(item)
284 step['cmd'] = new_cmd
285 if 'cwd' in step:
286 [new_cwd] = expand_root_placeholder(root, [step['cwd']])
287 step['cwd'] = new_cwd
226 288
227 return MakeStepsRetval(None, None, steps) 289 json_data = step.pop('static_json_data', {})
290 assert not(json_data and json_output_name), (
291 "Cannot have both static_json_data as well as dynamic json_data")
292 if test_data is None:
293 failed, [retcode] = annotator.run_steps([step], failed)
294 if json_output_name:
295 try:
296 json_data = json.load(os.fdopen(json_output_fd, 'r'))
297 except ValueError:
298 pass
299 else:
300 retcode, potential_json_data = test_data.pop(step['name'], (0, {}))
301 json_data = json_data or potential_json_data
302 failed = failed or retcode != 0
228 303
229 def run_annotator(stream, steps, keep_stdin): 304 # Support CheckoutRootPlaceholder.
230 ret = 0 305 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 306
251 return ret 307 assert step['name'] not in step_history
308 step_history[step['name']] = StepData(step, retcode, json_data)
309
310 assert test_data is None or test_data == {}, (
311 "Unconsumed test data! %s" % (test_data,))
312
313 return MakeStepsRetval(retcode, step_history)
252 314
253 315
254 def UpdateScripts(): 316 def UpdateScripts():
255 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): 317 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'):
256 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') 318 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS')
257 return False 319 return False
258 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts']) 320 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts'])
259 with stream.step('update_scripts') as s: 321 with stream.step('update_scripts') as s:
260 build_root = os.path.join(SCRIPT_PATH, '..', '..') 322 build_root = os.path.join(SCRIPT_PATH, '..', '..')
261 gclient_name = 'gclient' 323 gclient_name = 'gclient'
262 if sys.platform.startswith('win'): 324 if sys.platform.startswith('win'):
263 gclient_name += '.bat' 325 gclient_name += '.bat'
264 gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name) 326 gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name)
265 if subprocess.call([gclient_path, 'sync', '--force'], cwd=build_root) != 0: 327 if subprocess.call([gclient_path, 'sync', '--force'], cwd=build_root) != 0:
266 s.step_text('gclient sync failed!') 328 s.step_text('gclient sync failed!')
267 s.step_warnings() 329 s.step_warnings()
268 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1' 330 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1'
269 return True 331 return True
270 332
271 333
272 if __name__ == '__main__': 334 if __name__ == '__main__':
273 if UpdateScripts(): 335 if UpdateScripts():
274 os.execv(sys.executable, [sys.executable] + sys.argv) 336 os.execv(sys.executable, [sys.executable] + sys.argv)
275 sys.exit(main(sys.argv)) 337 sys.exit(main(sys.argv))
OLDNEW
« no previous file with comments | « scripts/slave/annotated_checkout.py ('k') | scripts/slave/git_setup.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698