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

Side by Side Diff: third_party/recipe_engine/doc/user_guide.md

Issue 1347263002: Revert of Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 5 years, 3 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
(Empty)
1 # Recipes
2
3 Recipes are a domain-specific language (embedded in python) for specifying
4 sequences of subprocess calls in a cross-platform and testable way.
5
6 [TOC]
7
8 ## Background
9
10 Chromium uses BuildBot for its builds. It requires master restarts to change
11 bot configs, which slows bot changes down.
12
13 With Recipes, most build-related things happen in scripts that run on the
14 slave, which means that the master does not need to be restarted in order
15 to change something about a build configuration.
16
17 Recipes also provide a way to unit test build scripts, by mocking commands and
18 recording "expectations" of what will happen when the script runs under various
19 conditions. This makes it easy to verify that the scope of a change is limited.
20
21 ## Intro
22
23 This README will seek to teach the ways of Recipes, so that you may do one or
24 more of the following:
25
26 * Read them
27 * Make new recipes
28 * Fix bugs in recipes
29 * Create libraries (api modules) for others to use in their recipes.
30
31 The document will build knowledge up in small steps using examples, and so it's
32 probably best to read the whole doc through from top to bottom once before using
33 it as a reference.
34
35 ## Small Beginnings
36
37 **Recipes are a means to cause a series of commands to run on a machine.**
38
39 All recipes take the form of a python file whose body looks like this:
40
41 ```python
42 DEPS = ['step']
43
44 def RunSteps(api):
45 api.step('Print Hello World', ['echo', 'hello', 'world'])
46 ```
47
48 The `RunSteps` function is expected to take at least a single argument `api`
49 (we'll get to that in more detail later), and run a series of steps by calling
50 api functions. All of these functions will eventually make calls to
51 `api.step()`, which is the only way to actually get anything done on the
52 machine. Using python libraries with OS side-effects is prohibited to enable
53 testing.
54
55 For these examples we will work out of the
56 [tools/build](https://chromium.googlesource.com/chromium/tools/build/)
57 repository.
58
59 Put this in a file under `scripts/slave/recipes/hello.py`. You can then
60 run this recipe by calling
61
62 $ scripts/tools/run_recipe.py hello
63
64 *** promo
65 Note: every recipe execution (e.g. build on buildbot) emits
66 a step log called `run_recipe` on the `setup_build` step which provides
67 a precise invocation for `run_recipe.py` correlating exactly with the current
68 recipe invocation. This is useful to locally repro a failing build without
69 having to guess at the parameters to `run_recipe.py`.
70 ***
71
72 ## We should probably test as we go...
73
74 **All recipes MUST have corresponding tests, which achieve 100% code coverage.**
75
76 So, we have our recipe. Let's add a test to it.
77
78 ```python
79 DEPS = ['step']
80
81 def RunSteps(api):
82 api.step('Print Hello World', ['echo', 'hello', 'world'])
83
84 def GenTests(api):
85 yield api.test('basic')
86 ```
87
88 This causes a single test case to be generated, called 'basic', which has no
89 input parameters. As your recipe becomes more complex, you'll need to add more
90 tests to make sure that you maintain 100% code coverage.
91
92 In order to run the tests, run
93
94 $ scripts/slave/unittests/recipe_simulation_test.py train hello
95
96 This will write the file `build/scripts/slave/recipes/hello.expected/basic.json`
97 summarizing the actions of the recipe under the boring conditions
98 specified by `api.test('basic')`.
99
100 [
101 {
102 "cmd": [
103 "echo",
104 "hello",
105 "world"
106 ],
107 "cwd": "[SLAVE_BUILD]",
108 "name": "Print Hello World"
109 }
110 ]
111
112 ## Let's do something useful
113
114 ### Properties are the primary input for your recipes
115
116 In order to do something useful, we need to pull in parameters from the outside
117 world. There's one primary source of input for recipes, which is `properties`.
118
119 Properties are a relic from the days of BuildBot, though they have been
120 dressed up a bit to be more like we'll want them in the future. If you're
121 familiar with BuildBot, you'll probably know them as `factory_properties` and
122 `build_properties`. The new `properties` object is a merging of these two, and
123 is provided by the `properties` api module.
124
125 ```python
126 from recipe_engine.recipe_api import Property
127
128 DEPS = [
129 'step',
130 ]
131
132 PROPERTIES = {
133 'target_of_admiration': Property(
134 kind=str, help="Who you love and adore.", default="Chrome Infra"),
135 }
136
137 def RunSteps(api, target_of_admiration):
138 verb = 'Hello, %s'
139 if target_of_admiration == 'DarthVader':
140 verb = 'Die in a fire, %s!'
141 api.step('Greet Admired Individual', ['echo', verb % target_of_admiration])
142
143 def GenTests(api):
144 yield api.test('basic') + api.properties(target_of_admiration='Bob')
145 yield api.test('vader') + api.properties(target_of_admiration='DarthVader')
146 yield api.test('infra rocks')
147 ```
148
149 Yes, elements of a test specification are combined with `+` and it's weird.
150
151 To specify property values in a local run:
152
153 build/scripts/tools/run_recipe.py <recipe-name> opt=bob other=sally
154
155 Or, more explicitly::
156
157 build/scripts/tools/run_recipe.py --properties-file <path/to/json>
158
159 Where `<path/to/json>` is a file containing a valid json `object` (i.e.
160 key:value pairs).
161
162 ### Modules
163
164 There are all sorts of helper modules. They are found in the `recipe_modules`
165 directory alongside the `recipes` directory where the recipes go.
166
167 Notice the `DEPS` line in the recipe. Any modules named by string in DEPS are
168 'injected' into the `api` parameter that your recipe gets. If you leave them out
169 of DEPS, you'll get an AttributeError when you try to access them. The modules
170 are located primarily in `recipe_modules/`, and their name is their folder name.
171
172 There are a whole bunch of modules which provide really helpful tools. You
173 should go take a look at them. `scripts/tools/show_me_the_modules.py` is a
174 pretty helpful tool. If you want to know more about properties, step and path, I
175 would suggest starting with `show_me_the_modules.py`, and then delving into the
176 helpful docstrings in those helpful modules.
177
178 ## Making Modules
179
180 **Modules are for grouping functionality together and exposing it across
181 recipes.**
182
183 So now you feel like you're pretty good at recipes, but you want to share your
184 echo functionality across a couple recipes which all start the same way. To do
185 this, you need to add a module directory.
186
187 ```
188 recipe_modules/
189 step/
190 properties/
191 path/
192 hello/
193 __init__.py # (Required) Contains optional `DEPS = list([other modules])`
194 api.py # (Required) Contains single required RecipeApi-derived class
195 config.py # (Optional) Contains configuration for your api
196 *_config.py # (Optional) These contain extensions to the configurations of
197 # your dependency APIs
198 ```
199
200 First add an `__init__.py` with DEPS:
201
202 ```python
203 # recipe_modules/hello/__init__.py
204 from recipe_api import Property
205
206 DEPS = ['properties', 'step']
207 PROPERTIES = {
208 'target_of_admiration': Property(default=None),
209 }
210 ```
211
212 And your api.py should look something like:
213
214 ```python
215 from slave import recipe_api
216
217 class HelloApi(recipe_api.RecipeApi):
218 def __init__(self, target_of_admiration):
219 self._target = target_of_admiration
220
221 def greet(self, default_verb=None):
222 verb = default_verb or 'Hello %s'
223 if self._target == 'DarthVader':
224 verb = 'Die in a fire %s!'
225 self.m.step('Hello World',
226 ['echo', verb % self._target])
227 ```
228
229 Note that all the DEPS get injected into `self.m`. This logic is handled outside
230 of the object (i.e. not in `__init__`).
231
232 > Because dependencies are injected after module initialization, *you do not
233 > have access to injected modules in your APIs `__init__` method*!
234
235 And now, our refactored recipe:
236
237 ```python
238 DEPS = ['hello']
239
240 def RunSteps(api):
241 api.hello.greet()
242
243 def GenTests(api):
244 yield api.test('basic') + api.properties(target_of_admiration='Bob')
245 yield api.test('vader') + api.properties(target_of_admiration='DarthVader')
246 ```
247
248 > NOTE: all of the modules are also require 100% code coverage, but you only
249 > need coverage from SOME recipe.
250
251 ## So how do I really write those tests?
252
253 The basic form of tests is:
254
255 ```python
256 def GenTests(api):
257 yield api.test('testname') + # other stuff
258 ```
259
260 Some modules define interfaces for specifying necessary step data; these are
261 injected into `api` from `DEPS` similarly to how it works for `RunSteps`. There
262 are a few other methods available to `GenTests`'s `api`. Common ones include:
263
264 * `api.properties(buildername='foo_builder')` sets properties as we have seen.
265 * `api.platform('linux', 32)` sets the mock platform to 32-bit linux.
266 * `api.step_data('Hello World', retcode=1)` mocks the `'Hello World'` step
267 to have failed with exit code 1.
268
269 By default all simulated steps succeed, the platform is 64-bit linux, and
270 there are no properties. The `api.properties.generic()` method populates some
271 common properties for Chromium recipes.
272
273 The `api` passed to GenTests is confusingly **NOT** the same as the recipe api.
274 It's actually an instance of `recipe_test_api.py:RecipeTestApi()`. This is
275 admittedly pretty weak, and it would be great to have the test api
276 automatically created via modules. On the flip side, the test api is much less
277 necessary than the recipe api, so this transformation has not been designed yet.
278
279 ## What is that config business?
280
281 **Configs are a way for a module to expose it's "global" state in a reusable
282 way.**
283
284 A common problem in Building Things is that you end up with an inordinantly
285 large matrix of configurations. Let's take chromium, for example. Here is a
286 sample list of axes of configuration which chromium needs to build and test:
287
288 * BUILD_CONFIG
289 * HOST_PLATFORM
290 * HOST_ARCH
291 * HOST_BITS
292 * TARGET_PLATFORM
293 * TARGET_ARCH
294 * TARGET_BITS
295 * builder type (ninja? msvs? xcodebuild?)
296 * compiler
297 * ...
298
299 Obviously there are a lot of combinations of those things, but only a relatively
300 small number of *valid* combinations of those things. How can we represent all
301 the valid states while still retaining our sanity?
302
303 We begin by specifying a schema that configurations of the `hello` module
304 will follow, and the config context based on it that we will add configuration
305 items to.
306
307 ```python
308 # recipe_modules/hello/config.py
309 from slave.recipe_config import config_item_context, ConfigGroup
310 from slave.recipe_config import SimpleConfig, StaticConfig, BadConf
311
312 def BaseConfig(TARGET='Bob'):
313 # This is a schema for the 'config blobs' that the hello module deals with.
314 return ConfigGroup(
315 verb = SimpleConfig(str),
316 # A config blob is not complete() until all required entries have a value.
317 tool = SimpleConfig(str, required=True),
318 # Generally, your schema should take a series of CAPITAL args which will be
319 # set as StaticConfig data in the config blob.
320 TARGET = StaticConfig(str(TARGET)),
321 )
322
323 config_ctx = config_item_context(BaseConfig)
324 ```
325
326 The `BaseConfig` schema is expected to return a `ConfigGroup` instance of some
327 sort. All the configs that you get out of this file will be a modified version
328 of something returned by the schema method. The arguments should have sane
329 defaults, and should be named in `ALL_CAPS` (this is to avoid argument name
330 conflicts as we'll see later).
331
332 `config_ctx` is the 'context' for all the config items in this file, and will
333 magically become the `CONFIG_CTX` for the entire module. Other modules may
334 extend this context, which we will get to later.
335
336 Finally let's define some config items themselves. A config item is a function
337 decorated with the `config_ctx`, and takes a config blob as 'c'. The config item
338 updates the config blob, perhaps conditionally. There are many features to
339 `slave/recipe_config.py`. I would recommend reading the docstrings there
340 for all the details.
341
342 ```python
343 # Each of these functions is a 'config item' in the context of config_ctx.
344
345 # is_root means that every config item will apply this item first.
346 @config_ctx(is_root=True)
347 def BASE(c):
348 if c.TARGET == 'DarthVader':
349 c.verb = 'Die in a fire, %s!'
350 else:
351 c.verb = 'Hello, %s'
352
353 @config_ctx(group='tool'): # items with the same group are mutually exclusive.
354 def super_tool(c):
355 if c.TARGET != 'Charlie':
356 raise BadConf('Can only use super tool for Charlie!')
357 c.tool = 'unicorn.py'
358
359 @config_ctx(group='tool'):
360 def default_tool(c):
361 c.tool = 'echo'
362 ```
363
364 Now that we have our config, let's use it.
365
366 ```python
367 # recipe_modules/hello/api.py
368 from slave import recipe_api
369
370 class HelloApi(recipe_api.RecipeApi):
371 def __init__(self, target_of_admiration):
372 self._target = target_of_admiration
373
374 def get_config_defaults(self, _config_name):
375 return {'TARGET': self._target}
376
377 def greet(self):
378 self.m.step('Hello World', [
379 self.m.path.build(self.c.tool), self.c.verb % self.c.TARGET])
380 ```
381
382 Note that `recipe_api.RecipeApi` contains all the plumbing for dealing with
383 configs. If your module has a config, you can access its current value via
384 `self.c`. The users of your module (read: recipes) will need to set this value
385 in one way or another. Also note that c is a 'public' variable, which means that
386 recipes have direct access to the configuration state by `api.<modname>.c`.
387
388 ```python
389 # recipes/hello.py
390 DEPS = ['hello']
391 def RunSteps(api):
392 api.hello.set_config('default_tool')
393 api.hello.greet() # Greets 'target_of_admiration' or 'Bob' with echo.
394
395 def GenTests(api):
396 yield api.test('bob')
397 yield api.test('anya') + api.properties(target_of_admiration='anya')
398 ```
399
400 Note the call to `set_config`. This method takes the configuration name
401 specifed, finds it in the given module (`'hello'` in this case), and sets
402 `api.hello.c` equal to the result of invoking the named config item
403 (`'default_tool'`) with the default configuration (the result of calling
404 `get_config_defaults`), merged over the static defaults specified by the schema.
405
406 We can also call `set_config` differently to get different results:
407
408 ```python
409 # recipes/rainbow_hello.py
410 DEPS = ['hello']
411 def RunSteps(api):
412 api.hello.set_config('super_tool', TARGET='Charlie')
413 api.hello.greet() # Greets 'Charlie' with unicorn.py.
414
415 def GenTests(api):
416 yield api.test('charlie')
417 ```
418
419 ```python
420 # recipes/evil_hello.py
421 DEPS = ['hello']
422 def RunSteps(api):
423 api.hello.set_config('default_tool', TARGET='DarthVader')
424 api.hello.greet() # Causes 'DarthVader' to despair with echo
425
426 def GenTests(api):
427 yield api.test('darth')
428 ```
429
430 `set_config()` also has one additional bit of magic. If a module (say,
431 `chromium`), depends on some other modules (say, `gclient`), if you do
432 `api.chromium.set_config('blink')`, it will apply the `'blink'` config item from
433 the chromium module, but it will also attempt to apply the `'blink'` config for
434 all the dependencies, too. This way, you can have the chromium module extend the
435 gclient config context with a 'blink' config item, and then `set_configs` will
436 stack across all the relevent contexts. (This has since been recognized as a
437 design mistake)
438
439 `recipe_api.RecipeApi` also provides `make_config` and `apply_config`, which
440 allow recipes more-direct access to the config items. However, `set_config()` is
441 the most-preferred way to apply configurations.
442
443 ## What about getting data back from a step?
444
445 Consider this recipe:
446
447 ```python
448 DEPS = ['step', 'path']
449
450 def RunSteps(api):
451 step_result = api.step('Determine blue moon',
452 [api.path['build'].join('is_blue_moon.sh')])
453
454 if step_result.retcode == 0:
455 api.step('HARLEM SHAKE!', [api.path['build'].join('do_the_harlem_shake.sh')] )
456 else:
457 api.step('Boring', [api.path['build'].join('its_a_small_world.sh')])
458
459 def GenTests(api):
460 yield api.test('harlem') + api.step_data('Determine blue moon', retcode=0)
461 yield api.test('boring') + api.step_data('Determine blue moon', retcode=1)
462 ```
463
464 See how we use `step_result` to get the result of the last step? The item we get
465 back is a `recipe_engine.main.StepData` instance (really, just a basic object
466 with member data). The members of this object which are guaranteed to exist are:
467 * `retcode`: Pretty much what you think
468 * `step`: The actual step json which was sent to `annotator.py`. Not usually
469 useful for recipes, but it is used internally for the recipe tests
470 framework.
471 * `presentation`: An object representing how the step will show up on the
472 build page, including its exit status, links, and extra log text. This is a
473 `recipe_engine.main.StepPresentation` object.
474 See also
475 [How to change step presentation](#how-to-change-step-presentation).
476
477 This is pretty neat... However, it turns out that returncodes suck bigtime for
478 communicating actual information. `api.json.output()` to the rescue!
479
480 ```python
481 DEPS = ['step', 'path', 'step_history', 'json']
482
483 def RunSteps(api):
484 step_result = api.step(
485 'run tests',
486 [api.path['build'].join('do_test_things.sh'), api.json.output()])
487 num_passed = step_result.json.output['num_passed']
488 if num_passed > 500:
489 api.step('victory', [api.path['build'].join('do_a_dance.sh')])
490 elif num_passed > 200:
491 api.step('not defeated', [api.path['build'].join('woohoo.sh')])
492 else:
493 api.step('deads!', [api.path['build'].join('you_r_deads.sh')])
494
495 def GenTests(api):
496 yield (api.test('winning') +
497 api.step_data('run tests', api.json.output({'num_passed': 791}))
498 yield (api.test('not_dead_yet') +
499 api.step_data('run tests', api.json.output({'num_passed': 302}))
500 yield (api.test('noooooo') +
501 api.step_data('run tests', api.json.output({'num_passed': 10})))
502 ```
503
504 ### How does THAT work!?
505
506 `api.json.output()` returns a `recipe_api.Placeholder` which is meant to be
507 added into a step command list. When the step runs, the placeholder gets
508 rendered into some strings (in this case, like '/tmp/some392ra8'). When the step
509 finishes, the Placeholder adds data to the `StepData` object for the step which
510 just ran, namespaced by the module name (in this case, the 'json' module decided
511 to add an 'output' attribute to the `step_history` item). I'd encourage you to
512 take a peek at the implementation of the json module to see how this is
513 implemented.
514
515 ### Example: write to standard input of a step
516
517 ```python
518 api.step(..., stdin=api.raw_io.input('test input'))
519 ```
520
521 Also see [raw_io's
522 example.py](https://chromium.googlesource.com/chromium/tools/build.git/+/master/ scripts/slave/recipe_modules/raw_io/example.py).
523
524 ### Example: read standard output of a step as json
525
526 ```python
527 step_result = api.step(..., stdout=api.json.output())
528 data = step_result.stdout
529 # data is a parsed JSON value, such as dict
530 ```
531
532 Also see [json's
533 example.py](https://chromium.googlesource.com/chromium/tools/build.git/+/master/ scripts/slave/recipe_modules/json/example.py).
534
535 ### Example: write to standard input of a step as json
536
537 ```python
538 data = {'value': 1}
539 api.step(..., stdin=api.json.input(data))
540 ```
541
542 Also see [json's
543 example.py](https://chromium.googlesource.com/chromium/tools/build.git/+/master/ scripts/slave/recipe_modules/json/example.py).
544
545 ### Example: simulated step output
546
547 This example specifies the standard output that should be returned when
548 a step is executed in simulation mode. This is typically used for
549 specifying default test data in the recipe or recipe module and removes
550 the need to specify too much test data for each test in GenTests:
551
552 ```python
553 api.step(..., step_test_data=api.raw_io.output('test data'))
554 ```
555
556 ### Example: simulated step output for a test case
557
558 ```python
559 yield (
560 api.test('my_test') +
561 api.step_data(
562 'step_name',
563 output=api.raw_io.output('test data')))
564 ```
565
566 ## How to change step presentation?
567
568 `step_result.presentation` allows modifying the appearance of a step:
569
570 ### Logging
571
572 ```python
573 step_result.presentation.logs['mylog'] = ['line1', 'line2']
574 ```
575
576 Creates an extra log "mylog" under the step.
577
578 ### Setting properties
579
580 `api.properties` are immutable, but you can change and add new
581 properties at the buildbot level.
582
583 ```python
584 step_result.presentation.properties['newprop'] = 1
585 ```
586
587 ### Example: step text
588
589 This modifies the text displayed next to a step name:
590
591 ```python
592 step_result = api.step(...)
593 step_result.presentation.step_text = 'Dynamic step result text'
594 ```
595
596 * `presentaton.logs` allows creating extra logs of a step run. Example:
597 ```python
598 step_result.presentation.logs['mylog'] = ['line1', 'line2']
599 ```
600 * presentation.properties allows changing and adding new properties at the
601 buildbot level. Example:
602 ```python
603 step_result.presentation.properties['newprop'] = 1
604 ```
605
606 ## How do I know what modules to use?
607
608 Use `scripts/tools/show_me_the_modules.py`. It's super effective!
609
610 ## How do I run those tests you were talking about?
611
612 To test all the recipes/apis, use
613 `scripts/slave/unittests/recipe_simulation_test.py`. To set new expectations
614 `scripts/slave/unittests/recipe_simulation_test.py train`.
615
616 ## Where's the docs on `*.py`?
617
618 Check the docstrings in `*.py`. `<trollface text="Problem?"/>`
619
620 In addition, most recipe modules have an `example.py` file which exercises most
621 of the code in the module for both test coverage and example purposes.
622
623 If you want to know what keys a step dictionary can take, take a look at
624 `third_party/recipe_engine/main.py`.
625
OLDNEW
« no previous file with comments | « third_party/recipe_engine/config_types.py ('k') | third_party/recipe_engine/expect_tests/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698