OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2015 The LUCI Authors. All rights reserved. | 2 # Copyright 2015 The LUCI Authors. All rights reserved. |
3 # Use of this source code is governed under the Apache License, Version 2.0 | 3 # Use of this source code is governed under the Apache License, Version 2.0 |
4 # that can be found in the LICENSE file. | 4 # that can be found in the LICENSE file. |
5 | 5 |
6 """Tool to interact with recipe repositories. | 6 """Tool to interact with recipe repositories. |
7 | 7 |
8 This tool operates on the nearest ancestor directory containing an | 8 This tool operates on the nearest ancestor directory containing an |
9 infra/config/recipes.cfg. | 9 infra/config/recipes.cfg. |
10 """ | 10 """ |
11 | 11 |
12 import argparse | 12 import argparse |
13 import json | 13 import json |
14 import logging | 14 import logging |
15 import os | 15 import os |
16 import subprocess | 16 import subprocess |
17 import sys | 17 import sys |
18 | 18 |
19 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | 19 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
20 sys.path.insert(0, ROOT_DIR) | 20 sys.path.insert(0, ROOT_DIR) |
21 | 21 |
22 from recipe_engine import env | 22 from recipe_engine import env |
23 from recipe_engine import arguments_pb2 | |
24 from google.protobuf import json_format as jsonpb | |
23 | 25 |
24 def get_package_config(args): | 26 def get_package_config(args): |
25 from recipe_engine import package | 27 from recipe_engine import package |
26 | 28 |
27 assert args.package, 'No recipe config (--package) given.' | 29 assert args.package, 'No recipe config (--package) given.' |
28 assert os.path.exists(args.package), ( | 30 assert os.path.exists(args.package), ( |
29 'Given recipes config file %s does not exist.' % args.package) | 31 'Given recipes config file %s does not exist.' % args.package) |
30 return ( | 32 return ( |
31 package.InfraRepoConfig().from_recipes_cfg(args.package), | 33 package.InfraRepoConfig().from_recipes_cfg(args.package), |
32 package.ProtoFile(args.package) | 34 package.ProtoFile(args.package) |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
77 with s.new_log_stream('reason') as l: | 79 with s.new_log_stream('reason') as l: |
78 for line in recipe_result.result['reason'].splitlines(): | 80 for line in recipe_result.result['reason'].splitlines(): |
79 l.write_line(line) | 81 l.write_line(line) |
80 | 82 |
81 if 'status_code' in recipe_result.result: | 83 if 'status_code' in recipe_result.result: |
82 return recipe_result.result['status_code'] | 84 return recipe_result.result['status_code'] |
83 else: | 85 else: |
84 return 0 | 86 return 0 |
85 | 87 |
86 | 88 |
87 def run(package_deps, args): | 89 def run(package_deps, args, op_args): |
88 from recipe_engine import run as recipe_run | 90 from recipe_engine import run as recipe_run |
89 from recipe_engine import loader | 91 from recipe_engine import loader |
90 from recipe_engine import step_runner | 92 from recipe_engine import step_runner |
91 from recipe_engine import stream | 93 from recipe_engine import stream |
92 | 94 |
93 def get_properties_from_args(args): | 95 def get_properties_from_args(args): |
94 properties = dict(x.split('=', 1) for x in args) | 96 properties = dict(x.split('=', 1) for x in args) |
95 for key, val in properties.iteritems(): | 97 for key, val in properties.iteritems(): |
96 try: | 98 try: |
97 properties[key] = json.loads(val) | 99 properties[key] = json.loads(val) |
98 except (ValueError, SyntaxError): | 100 except (ValueError, SyntaxError): |
99 pass # If a value couldn't be evaluated, keep the string version | 101 pass # If a value couldn't be evaluated, keep the string version |
100 return properties | 102 return properties |
101 | 103 |
102 def get_properties_from_file(filename): | 104 def get_properties_from_file(filename): |
103 properties_file = sys.stdin if filename == '-' else open(filename) | 105 properties_file = sys.stdin if filename == '-' else open(filename) |
104 properties = json.load(properties_file) | 106 properties = json.load(properties_file) |
105 assert isinstance(properties, dict) | 107 assert isinstance(properties, dict) |
106 return properties | 108 return properties |
107 | 109 |
108 def get_properties_from_json(props): | 110 def get_properties_from_json(props): |
109 return json.loads(props) | 111 return json.loads(props) |
110 | 112 |
113 def get_properties_from_operational_args(op_args): | |
114 if not op_args.properties.property: | |
115 return None | |
116 return _op_properties_to_dict(op_args.properties.property) | |
117 | |
118 | |
111 arg_properties = get_properties_from_args(args.props) | 119 arg_properties = get_properties_from_args(args.props) |
120 op_properties = get_properties_from_operational_args(op_args) | |
112 assert len(filter(bool, | 121 assert len(filter(bool, |
113 [arg_properties, args.properties_file, args.properties])) <= 1, ( | 122 (arg_properties, args.properties_file, args.properties, |
123 op_properties))) <= 1, ( | |
114 'Only one source of properties is allowed') | 124 'Only one source of properties is allowed') |
115 if args.properties: | 125 if args.properties: |
116 properties = get_properties_from_json(args.properties) | 126 properties = get_properties_from_json(args.properties) |
117 elif args.properties_file: | 127 elif args.properties_file: |
118 properties = get_properties_from_file(args.properties_file) | 128 properties = get_properties_from_file(args.properties_file) |
129 elif op_properties is not None: | |
130 properties = op_properties | |
119 else: | 131 else: |
120 properties = arg_properties | 132 properties = arg_properties |
121 | 133 |
122 properties['recipe'] = args.recipe | 134 properties['recipe'] = args.recipe |
123 | 135 |
124 os.environ['PYTHONUNBUFFERED'] = '1' | 136 os.environ['PYTHONUNBUFFERED'] = '1' |
125 os.environ['PYTHONIOENCODING'] = 'UTF-8' | 137 os.environ['PYTHONIOENCODING'] = 'UTF-8' |
126 | 138 |
127 _, config_file = get_package_config(args) | 139 _, config_file = get_package_config(args) |
128 universe = loader.UniverseView( | 140 universe = loader.UniverseView( |
129 loader.RecipeUniverse( | 141 loader.RecipeUniverse( |
130 package_deps, config_file), package_deps.root_package) | 142 package_deps, config_file), package_deps.root_package) |
131 | 143 |
132 workdir = (args.workdir or | 144 workdir = (args.workdir or |
133 os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir')) | 145 os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir')) |
134 logging.info('Using %s as work directory' % workdir) | 146 logging.info('Using %s as work directory' % workdir) |
135 if not os.path.exists(workdir): | 147 if not os.path.exists(workdir): |
136 os.makedirs(workdir) | 148 os.makedirs(workdir) |
137 | 149 |
138 old_cwd = os.getcwd() | 150 old_cwd = os.getcwd() |
139 os.chdir(workdir) | 151 os.chdir(workdir) |
140 stream_engine = stream.ProductStreamEngine( | 152 stream_engine = stream.ProductStreamEngine( |
141 stream.StreamEngineInvariants(), | 153 stream.StreamEngineInvariants(), |
142 stream.AnnotatorStreamEngine(sys.stdout, emit_timestamps=args.timestamps)) | 154 stream.AnnotatorStreamEngine( |
155 sys.stdout, emit_timestamps=(args.timestamps or | |
156 op_args.annotation_flags.emit_timestamp))) | |
143 with stream_engine: | 157 with stream_engine: |
144 try: | 158 try: |
145 ret = recipe_run.run_steps( | 159 ret = recipe_run.run_steps( |
146 properties, stream_engine, | 160 properties, stream_engine, |
147 step_runner.SubprocessStepRunner(stream_engine), | 161 step_runner.SubprocessStepRunner(stream_engine), |
148 universe=universe) | 162 universe=universe) |
149 finally: | 163 finally: |
150 os.chdir(old_cwd) | 164 os.chdir(old_cwd) |
151 | 165 |
152 return handle_recipe_return(ret, args.output_result_json, stream_engine) | 166 return handle_recipe_return(ret, args.output_result_json, stream_engine) |
(...skipping 68 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
221 | 235 |
222 def info(args): | 236 def info(args): |
223 from recipe_engine import package | 237 from recipe_engine import package |
224 repo_root, config_file = get_package_config(args) | 238 repo_root, config_file = get_package_config(args) |
225 package_spec = package.PackageSpec.load_proto(config_file) | 239 package_spec = package.PackageSpec.load_proto(config_file) |
226 | 240 |
227 if args.recipes_dir: | 241 if args.recipes_dir: |
228 print package_spec.recipes_path | 242 print package_spec.recipes_path |
229 | 243 |
230 | 244 |
245 def _op_properties_to_dict(pmap): | |
246 """Adds an arguments_pb2.PropertyList entry into a dictionary. | |
martiniss
2016/08/10 21:38:25
"into a dictionary"? This returns a dictionary...
dnj
2016/08/10 22:04:41
Acknowledged.
| |
247 | |
248 Args: | |
249 pmap (Map): Protobuf map of property name (str) to arguments_pb2.Property. | |
250 """ | |
251 d = {} | |
252 | |
253 for k in pmap: | |
254 prop = pmap[k] | |
255 typ = prop.WhichOneof('value') | |
256 if typ == 's': | |
257 v = prop.s | |
258 elif typ == 'int': | |
259 v = prop.int | |
260 elif typ == 'uint': | |
261 v = prop.uint | |
262 elif typ == 'd': | |
263 v = prop.d | |
264 elif typ == 'b': | |
265 v = prop.b | |
266 elif typ == 'data': | |
267 v = prop.data | |
268 elif typ == 'properties': | |
269 v = _op_properties_to_dict(prop.properties.property) | |
270 elif typ is None: | |
271 v = None | |
272 else: | |
273 raise ValueError('Unknown property field [%s]' % (typ,)) | |
274 d[k] = v | |
275 return d | |
276 | |
277 | |
231 def main(): | 278 def main(): |
232 from recipe_engine import package | 279 from recipe_engine import package |
233 | 280 |
234 # Super-annoyingly, we need to manually parse for simulation_test since | 281 # Super-annoyingly, we need to manually parse for simulation_test since |
235 # argparse is bonkers and doesn't allow us to forward --help to subcommands. | 282 # argparse is bonkers and doesn't allow us to forward --help to subcommands. |
236 # Save old_args for if we're using bootstrap | 283 # Save old_args for if we're using bootstrap |
237 original_sys_argv = sys.argv[:] | 284 original_sys_argv = sys.argv[:] |
238 if 'simulation_test' in sys.argv: | 285 if 'simulation_test' in sys.argv: |
239 index = sys.argv.index('simulation_test') | 286 index = sys.argv.index('simulation_test') |
240 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] | 287 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] |
(...skipping 18 matching lines...) Expand all Loading... | |
259 parser.add_argument( | 306 parser.add_argument( |
260 '--bootstrap-script', | 307 '--bootstrap-script', |
261 help='Path to the script used to bootstrap this tool (internal use only)') | 308 help='Path to the script used to bootstrap this tool (internal use only)') |
262 parser.add_argument('-O', '--project-override', metavar='ID=PATH', | 309 parser.add_argument('-O', '--project-override', metavar='ID=PATH', |
263 action=ProjectOverrideAction, | 310 action=ProjectOverrideAction, |
264 help='Override a project repository path with a local one.') | 311 help='Override a project repository path with a local one.') |
265 parser.add_argument( | 312 parser.add_argument( |
266 '--use-bootstrap', action='store_true', | 313 '--use-bootstrap', action='store_true', |
267 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' | 314 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' |
268 ' with required python dependencies.') | 315 ' with required python dependencies.') |
316 parser.add_argument( | |
317 '--operational-args-path', action='store', | |
318 help='The path to an operational Arguments file. If provided, this file ' | |
319 'must contain a JSONPB-encoded Arguments protobuf message, and will ' | |
320 'be integrated into the runtime parameters.') | |
269 | 321 |
270 subp = parser.add_subparsers() | 322 subp = parser.add_subparsers() |
271 | 323 |
272 fetch_p = subp.add_parser( | 324 fetch_p = subp.add_parser( |
273 'fetch', | 325 'fetch', |
274 description='Fetch and update dependencies.') | 326 description='Fetch and update dependencies.') |
275 fetch_p.set_defaults(command='fetch') | 327 fetch_p.set_defaults(command='fetch') |
276 | 328 |
277 simulation_test_p = subp.add_parser( | 329 simulation_test_p = subp.add_parser( |
278 'simulation_test', | 330 'simulation_test', |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
400 info_p = subp.add_parser( | 452 info_p = subp.add_parser( |
401 'info', | 453 'info', |
402 description='Query information about the current recipe package') | 454 description='Query information about the current recipe package') |
403 info_p.set_defaults(command='info') | 455 info_p.set_defaults(command='info') |
404 info_p.add_argument( | 456 info_p.add_argument( |
405 '--recipes-dir', action='store_true', | 457 '--recipes-dir', action='store_true', |
406 help='Get the subpath where the recipes live relative to repository root') | 458 help='Get the subpath where the recipes live relative to repository root') |
407 | 459 |
408 args = parser.parse_args() | 460 args = parser.parse_args() |
409 | 461 |
462 # Load/parse operational arguments. | |
martiniss
2016/08/10 21:38:25
Why is this done down here, vs in the run function
dnj
2016/08/10 22:04:41
The idea is that the operational parameters may so
| |
463 op_args = arguments_pb2.Arguments() | |
464 if args.operational_args_path is not None: | |
465 with open(args.operational_args_path) as fd: | |
466 data = fd.read() | |
467 jsonpb.Parse(data, op_args) | |
468 | |
410 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): | 469 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): |
411 subprocess.check_call( | 470 subprocess.check_call( |
412 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', | 471 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', |
413 'bootstrap/deps.pyl', 'ENV'], | 472 'bootstrap/deps.pyl', 'ENV'], |
414 cwd=os.path.dirname(os.path.realpath(__file__))) | 473 cwd=os.path.dirname(os.path.realpath(__file__))) |
415 | 474 |
416 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' | 475 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' |
417 args = sys.argv | 476 args = sys.argv |
418 return subprocess.call( | 477 return subprocess.call( |
419 [os.path.join( | 478 [os.path.join( |
(...skipping 28 matching lines...) Expand all Loading... | |
448 | 507 |
449 if args.command == 'fetch': | 508 if args.command == 'fetch': |
450 # We already did everything in the create() call above. | 509 # We already did everything in the create() call above. |
451 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' | 510 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' |
452 return 0 | 511 return 0 |
453 if args.command == 'simulation_test': | 512 if args.command == 'simulation_test': |
454 return simulation_test(package_deps, args) | 513 return simulation_test(package_deps, args) |
455 elif args.command == 'lint': | 514 elif args.command == 'lint': |
456 return lint(package_deps, args) | 515 return lint(package_deps, args) |
457 elif args.command == 'run': | 516 elif args.command == 'run': |
458 return run(package_deps, args) | 517 return run(package_deps, args, op_args) |
459 elif args.command == 'autoroll': | 518 elif args.command == 'autoroll': |
460 return autoroll(args) | 519 return autoroll(args) |
461 elif args.command == 'depgraph': | 520 elif args.command == 'depgraph': |
462 return depgraph(package_deps, args) | 521 return depgraph(package_deps, args) |
463 elif args.command == 'refs': | 522 elif args.command == 'refs': |
464 return refs(package_deps, args) | 523 return refs(package_deps, args) |
465 elif args.command == 'doc': | 524 elif args.command == 'doc': |
466 return doc(package_deps, args) | 525 return doc(package_deps, args) |
467 elif args.command == 'info': | 526 elif args.command == 'info': |
468 return info(args) | 527 return info(args) |
(...skipping 22 matching lines...) Expand all Loading... | |
491 ret = main() | 550 ret = main() |
492 if not isinstance(ret, int): | 551 if not isinstance(ret, int): |
493 if ret is None: | 552 if ret is None: |
494 ret = 0 | 553 ret = 0 |
495 else: | 554 else: |
496 print >> sys.stderr, ret | 555 print >> sys.stderr, ret |
497 ret = 1 | 556 ret = 1 |
498 sys.stdout.flush() | 557 sys.stdout.flush() |
499 sys.stderr.flush() | 558 sys.stderr.flush() |
500 os._exit(ret) | 559 os._exit(ret) |
OLD | NEW |