Chromium Code Reviews| 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 # Map of arguments_pb2.Property "value" oneof conversion functions. | |
|
martiniss
2016/08/10 22:54:45
Add comment saying "Keep in sync with recipe_engin
dnj
2016/08/11 01:21:31
Done.
| |
| 246 _OP_PROPERTY_CONV = { | |
| 247 's': lambda prop: prop.s, | |
| 248 'int': lambda prop: prop.int, | |
| 249 'uint': lambda prop: prop.uint, | |
| 250 'd': lambda prop: prop.d, | |
| 251 'b': lambda prop: prop.b, | |
| 252 'data': lambda prop: prop.data, | |
| 253 'map': lambda prop: _op_properties_to_dict(prop.map.property), | |
| 254 'list': lambda prop: [_op_property_value(v) for v in prop.list.property], | |
| 255 } | |
| 256 | |
| 257 def _op_property_value(prop): | |
| 258 """Returns the Python-converted value of an arguments_pb2.Property. | |
| 259 | |
| 260 Args: | |
| 261 prop (arguments_pb2.Property): property to convert. | |
| 262 Returns: The converted value. | |
| 263 Raises: | |
| 264 ValueError: If "prop" is incomplete or invalid. | |
| 265 """ | |
| 266 typ = prop.WhichOneof('value') | |
| 267 conv = _OP_PROPERTY_CONV.get(typ) | |
| 268 if not conv: | |
| 269 raise ValueError('Unknown property field [%s]' % (typ,)) | |
| 270 return conv(prop) | |
| 271 | |
| 272 | |
| 273 def _op_properties_to_dict(pmap): | |
| 274 """Creates a properties dictionary from an arguments_pb2.PropertyMap entry. | |
| 275 | |
| 276 Args: | |
| 277 pmap (arguments_pb2.PropertyMap): Map to convert to dictionary form. | |
| 278 Returns (dict): A dictionary derived from the properties in "pmap". | |
| 279 """ | |
| 280 return dict((k, _op_property_value(pmap[k])) for k in pmap) | |
| 281 | |
| 282 | |
| 231 def main(): | 283 def main(): |
| 232 from recipe_engine import package | 284 from recipe_engine import package |
| 233 | 285 |
| 234 # Super-annoyingly, we need to manually parse for simulation_test since | 286 # 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. | 287 # argparse is bonkers and doesn't allow us to forward --help to subcommands. |
| 236 # Save old_args for if we're using bootstrap | 288 # Save old_args for if we're using bootstrap |
| 237 original_sys_argv = sys.argv[:] | 289 original_sys_argv = sys.argv[:] |
| 238 if 'simulation_test' in sys.argv: | 290 if 'simulation_test' in sys.argv: |
| 239 index = sys.argv.index('simulation_test') | 291 index = sys.argv.index('simulation_test') |
| 240 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] | 292 sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 259 parser.add_argument( | 311 parser.add_argument( |
| 260 '--bootstrap-script', | 312 '--bootstrap-script', |
| 261 help='Path to the script used to bootstrap this tool (internal use only)') | 313 help='Path to the script used to bootstrap this tool (internal use only)') |
| 262 parser.add_argument('-O', '--project-override', metavar='ID=PATH', | 314 parser.add_argument('-O', '--project-override', metavar='ID=PATH', |
| 263 action=ProjectOverrideAction, | 315 action=ProjectOverrideAction, |
| 264 help='Override a project repository path with a local one.') | 316 help='Override a project repository path with a local one.') |
| 265 parser.add_argument( | 317 parser.add_argument( |
| 266 '--use-bootstrap', action='store_true', | 318 '--use-bootstrap', action='store_true', |
| 267 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' | 319 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' |
| 268 ' with required python dependencies.') | 320 ' with required python dependencies.') |
| 321 parser.add_argument( | |
| 322 '--operational-args-path', action='store', | |
| 323 help='The path to an operational Arguments file. If provided, this file ' | |
| 324 'must contain a JSONPB-encoded Arguments protobuf message, and will ' | |
| 325 'be integrated into the runtime parameters.') | |
| 269 | 326 |
| 270 subp = parser.add_subparsers() | 327 subp = parser.add_subparsers() |
| 271 | 328 |
| 272 fetch_p = subp.add_parser( | 329 fetch_p = subp.add_parser( |
| 273 'fetch', | 330 'fetch', |
| 274 description='Fetch and update dependencies.') | 331 description='Fetch and update dependencies.') |
| 275 fetch_p.set_defaults(command='fetch') | 332 fetch_p.set_defaults(command='fetch') |
| 276 | 333 |
| 277 simulation_test_p = subp.add_parser( | 334 simulation_test_p = subp.add_parser( |
| 278 'simulation_test', | 335 'simulation_test', |
| (...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 400 info_p = subp.add_parser( | 457 info_p = subp.add_parser( |
| 401 'info', | 458 'info', |
| 402 description='Query information about the current recipe package') | 459 description='Query information about the current recipe package') |
| 403 info_p.set_defaults(command='info') | 460 info_p.set_defaults(command='info') |
| 404 info_p.add_argument( | 461 info_p.add_argument( |
| 405 '--recipes-dir', action='store_true', | 462 '--recipes-dir', action='store_true', |
| 406 help='Get the subpath where the recipes live relative to repository root') | 463 help='Get the subpath where the recipes live relative to repository root') |
| 407 | 464 |
| 408 args = parser.parse_args() | 465 args = parser.parse_args() |
| 409 | 466 |
| 467 # Load/parse operational arguments. | |
| 468 op_args = arguments_pb2.Arguments() | |
| 469 if args.operational_args_path is not None: | |
| 470 with open(args.operational_args_path) as fd: | |
| 471 data = fd.read() | |
| 472 jsonpb.Parse(data, op_args) | |
| 473 | |
| 410 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): | 474 if args.use_bootstrap and not os.environ.pop('RECIPES_RUN_BOOTSTRAP', None): |
| 411 subprocess.check_call( | 475 subprocess.check_call( |
| 412 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', | 476 [sys.executable, 'bootstrap/bootstrap.py', '--deps-file', |
| 413 'bootstrap/deps.pyl', 'ENV'], | 477 'bootstrap/deps.pyl', 'ENV'], |
| 414 cwd=os.path.dirname(os.path.realpath(__file__))) | 478 cwd=os.path.dirname(os.path.realpath(__file__))) |
| 415 | 479 |
| 416 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' | 480 os.environ['RECIPES_RUN_BOOTSTRAP'] = '1' |
| 417 args = sys.argv | 481 args = sys.argv |
| 418 return subprocess.call( | 482 return subprocess.call( |
| 419 [os.path.join( | 483 [os.path.join( |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 448 | 512 |
| 449 if args.command == 'fetch': | 513 if args.command == 'fetch': |
| 450 # We already did everything in the create() call above. | 514 # We already did everything in the create() call above. |
| 451 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' | 515 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' |
| 452 return 0 | 516 return 0 |
| 453 if args.command == 'simulation_test': | 517 if args.command == 'simulation_test': |
| 454 return simulation_test(package_deps, args) | 518 return simulation_test(package_deps, args) |
| 455 elif args.command == 'lint': | 519 elif args.command == 'lint': |
| 456 return lint(package_deps, args) | 520 return lint(package_deps, args) |
| 457 elif args.command == 'run': | 521 elif args.command == 'run': |
| 458 return run(package_deps, args) | 522 return run(package_deps, args, op_args) |
| 459 elif args.command == 'autoroll': | 523 elif args.command == 'autoroll': |
| 460 return autoroll(args) | 524 return autoroll(args) |
| 461 elif args.command == 'depgraph': | 525 elif args.command == 'depgraph': |
| 462 return depgraph(package_deps, args) | 526 return depgraph(package_deps, args) |
| 463 elif args.command == 'refs': | 527 elif args.command == 'refs': |
| 464 return refs(package_deps, args) | 528 return refs(package_deps, args) |
| 465 elif args.command == 'doc': | 529 elif args.command == 'doc': |
| 466 return doc(package_deps, args) | 530 return doc(package_deps, args) |
| 467 elif args.command == 'info': | 531 elif args.command == 'info': |
| 468 return info(args) | 532 return info(args) |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 491 ret = main() | 555 ret = main() |
| 492 if not isinstance(ret, int): | 556 if not isinstance(ret, int): |
| 493 if ret is None: | 557 if ret is None: |
| 494 ret = 0 | 558 ret = 0 |
| 495 else: | 559 else: |
| 496 print >> sys.stderr, ret | 560 print >> sys.stderr, ret |
| 497 ret = 1 | 561 ret = 1 |
| 498 sys.stdout.flush() | 562 sys.stdout.flush() |
| 499 sys.stderr.flush() | 563 sys.stderr.flush() |
| 500 os._exit(ret) | 564 os._exit(ret) |
| OLD | NEW |