| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright 2015 The LUCI Authors. All rights reserved. | 2 # Copyright 2017 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 json | |
| 13 import logging | 12 import logging |
| 14 import os | 13 import os |
| 15 import shutil | 14 import shutil |
| 16 import subprocess | 15 import subprocess |
| 17 import sys | 16 import sys |
| 18 import tempfile | 17 import tempfile |
| 19 | 18 |
| 20 # This is necessary to ensure that str literals are by-default assumed to hold | 19 # This is necessary to ensure that str literals are by-default assumed to hold |
| 21 # utf-8. It also makes the implicit str(unicode(...)) act like | 20 # utf-8. It also makes the implicit str(unicode(...)) act like |
| 22 # unicode(...).encode('utf-8'), rather than unicode(...).encode('ascii') . | 21 # unicode(...).encode('utf-8'), rather than unicode(...).encode('ascii') . |
| 23 reload(sys) | 22 reload(sys) |
| 24 sys.setdefaultencoding('UTF8') | 23 sys.setdefaultencoding('UTF8') |
| 25 | 24 |
| 26 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | 25 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 27 sys.path.insert(0, ROOT_DIR) | 26 sys.path.insert(0, ROOT_DIR) |
| 28 | 27 |
| 29 from recipe_engine import env | 28 from recipe_engine import env |
| 30 | 29 |
| 30 from recipe_engine import common_args |
| 31 |
| 31 import argparse # this is vendored | 32 import argparse # this is vendored |
| 32 from recipe_engine import arguments_pb2 | |
| 33 from google.protobuf import json_format as jsonpb | |
| 34 | |
| 35 | 33 |
| 36 from recipe_engine import fetch, lint_test, bundle, depgraph, autoroll | 34 from recipe_engine import fetch, lint_test, bundle, depgraph, autoroll |
| 37 from recipe_engine import remote, refs, doc, test, run | 35 from recipe_engine import remote, refs, doc, test, run |
| 38 | 36 |
| 39 | 37 |
| 40 # Each of these subcommands has a method: | 38 # Each of these subcommands has a method: |
| 41 # | 39 # |
| 42 # def add_subparsers(argparse._SubParsersAction): ... | 40 # def add_subparsers(argparse._SubParsersAction): ... |
| 43 # | 41 # |
| 44 # which is expected to add a subparser by calling .add_parser on it. In | 42 # which is expected to add a subparser by calling .add_parser on it. In |
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 76 bundle, | 74 bundle, |
| 77 depgraph, | 75 depgraph, |
| 78 doc, | 76 doc, |
| 79 fetch, | 77 fetch, |
| 80 lint_test, | 78 lint_test, |
| 81 refs, | 79 refs, |
| 82 remote, | 80 remote, |
| 83 ] | 81 ] |
| 84 | 82 |
| 85 | 83 |
| 86 def add_common_args(parser): | |
| 87 from recipe_engine import package_io | |
| 88 | |
| 89 class ProjectOverrideAction(argparse.Action): | |
| 90 def __call__(self, parser, namespace, values, option_string=None): | |
| 91 p = values.split('=', 2) | |
| 92 if len(p) != 2: | |
| 93 raise ValueError('Override must have the form: repo=path') | |
| 94 project_id, path = p | |
| 95 | |
| 96 v = getattr(namespace, self.dest, None) | |
| 97 if v is None: | |
| 98 v = {} | |
| 99 setattr(namespace, self.dest, v) | |
| 100 | |
| 101 if v.get(project_id): | |
| 102 raise ValueError('An override is already defined for [%s] (%s)' % ( | |
| 103 project_id, v[project_id])) | |
| 104 path = os.path.abspath(os.path.expanduser(path)) | |
| 105 if not os.path.isdir(path): | |
| 106 raise ValueError('Override path [%s] is not a directory' % (path,)) | |
| 107 v[project_id] = path | |
| 108 | |
| 109 def package_type(value): | |
| 110 if not os.path.isfile(value): | |
| 111 raise argparse.ArgumentTypeError( | |
| 112 'Given recipes config file %r does not exist.' % (value,)) | |
| 113 return package_io.PackageFile(value) | |
| 114 | |
| 115 parser.add_argument( | |
| 116 '--package', | |
| 117 type=package_type, | |
| 118 help='Path to recipes.cfg of the recipe package to operate on' | |
| 119 ', usually in infra/config/recipes.cfg') | |
| 120 parser.add_argument( | |
| 121 '--deps-path', | |
| 122 type=os.path.abspath, | |
| 123 help='Path where recipe engine dependencies will be extracted. Specify ' | |
| 124 '"-" to use a temporary directory for deps, which will be cleaned ' | |
| 125 'up on exit.') | |
| 126 parser.add_argument( | |
| 127 '--verbose', '-v', action='count', | |
| 128 help='Increase logging verboisty') | |
| 129 # TODO(phajdan.jr): Figure out if we need --no-fetch; remove if not. | |
| 130 parser.add_argument( | |
| 131 '--no-fetch', action='store_true', | |
| 132 help='Disable automatic fetching') | |
| 133 parser.add_argument('-O', '--project-override', metavar='ID=PATH', | |
| 134 action=ProjectOverrideAction, | |
| 135 help='Override a project repository path with a local one.') | |
| 136 parser.add_argument( | |
| 137 # Use 'None' as default so that we can recognize when none of the | |
| 138 # bootstrap options were passed. | |
| 139 '--use-bootstrap', action='store_true', default=None, | |
| 140 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' | |
| 141 ' with required python dependencies.') | |
| 142 parser.add_argument( | |
| 143 '--bootstrap-vpython-path', metavar='PATH', | |
| 144 help='Specify the `vpython` executable path to use when bootstrapping (' | |
| 145 'requires --use-bootstrap).') | |
| 146 parser.add_argument( | |
| 147 '--disable-bootstrap', action='store_false', dest='use_bootstrap', | |
| 148 help='Disables bootstrap (see --use-bootstrap)') | |
| 149 | |
| 150 def operational_args_type(value): | |
| 151 with open(value) as fd: | |
| 152 return jsonpb.ParseDict(json.load(fd), arguments_pb2.Arguments()) | |
| 153 | |
| 154 parser.set_defaults( | |
| 155 operational_args=arguments_pb2.Arguments(), | |
| 156 bare_command=False, # don't call postprocess_func, don't use package_deps | |
| 157 postprocess_func=lambda parser, args: None, | |
| 158 ) | |
| 159 | |
| 160 parser.add_argument( | |
| 161 '--operational-args-path', | |
| 162 dest='operational_args', | |
| 163 type=operational_args_type, | |
| 164 help='The path to an operational Arguments file. If provided, this file ' | |
| 165 'must contain a JSONPB-encoded Arguments protobuf message, and will ' | |
| 166 'be integrated into the runtime parameters.') | |
| 167 | |
| 168 def post_process_args(parser, args): | |
| 169 if args.bare_command: | |
| 170 # TODO(iannucci): this is gross, and only for the remote subcommand; | |
| 171 # remote doesn't behave like ANY other commands. A way to solve this will | |
| 172 # be to allow --package to take a remote repo and then simply remove the | |
| 173 # remote subcommand entirely. | |
| 174 if args.package is not None: | |
| 175 parser.error('%s forbids --package' % args.command) | |
| 176 else: | |
| 177 if not args.package: | |
| 178 parser.error('%s requires --package' % args.command) | |
| 179 | |
| 180 return post_process_args | |
| 181 | |
| 182 | |
| 183 def main(): | 84 def main(): |
| 184 parser = argparse.ArgumentParser( | 85 parser = argparse.ArgumentParser( |
| 185 description='Interact with the recipe system.') | 86 description='Interact with the recipe system.') |
| 186 | 87 |
| 187 common_postprocess_func = add_common_args(parser) | 88 common_postprocess_func = common_args.add_common_args(parser) |
| 188 | 89 |
| 189 subp = parser.add_subparsers() | 90 subp = parser.add_subparsers() |
| 190 for module in _SUBCOMMANDS: | 91 for module in _SUBCOMMANDS: |
| 191 module.add_subparser(subp) | 92 module.add_subparser(subp) |
| 192 | 93 |
| 193 args = parser.parse_args() | 94 args = parser.parse_args() |
| 194 common_postprocess_func(parser, args) | 95 common_postprocess_func(parser, args) |
| 195 args.postprocess_func(parser, args) | 96 args.postprocess_func(parser, args) |
| 196 | 97 |
| 197 # TODO(iannucci): We should always do logging.basicConfig() (probably with | |
| 198 # logging.WARNING), even if no verbose is passed. However we need to be | |
| 199 # careful as this could cause issues with spurious/unexpected output. I think | |
| 200 # it's risky enough to do in a different CL. | |
| 201 | |
| 202 if args.verbose > 0: | |
| 203 logging.basicConfig() | |
| 204 logging.getLogger().setLevel(logging.INFO) | |
| 205 if args.verbose > 1: | |
| 206 logging.getLogger().setLevel(logging.DEBUG) | |
| 207 | |
| 208 # If we're bootstrapping, construct our bootstrap environment. If we're | 98 # If we're bootstrapping, construct our bootstrap environment. If we're |
| 209 # using a custom deps path, install our enviornment there too. | 99 # using a custom deps path, install our enviornment there too. |
| 210 if args.use_bootstrap and not env.USING_BOOTSTRAP: | 100 if args.use_bootstrap and not env.USING_BOOTSTRAP: |
| 211 logging.debug('Bootstrapping recipe engine through vpython...') | 101 logging.debug('Bootstrapping recipe engine through vpython...') |
| 212 | 102 |
| 213 bootstrap_env = os.environ.copy() | 103 bootstrap_env = os.environ.copy() |
| 214 bootstrap_env[env.BOOTSTRAP_ENV_KEY] = '1' | 104 bootstrap_env[env.BOOTSTRAP_ENV_KEY] = '1' |
| 215 | 105 |
| 216 cmd = [ | 106 cmd = [ |
| 217 sys.executable, | 107 sys.executable, |
| (...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 273 | 163 |
| 274 if not isinstance(ret, int): | 164 if not isinstance(ret, int): |
| 275 if ret is None: | 165 if ret is None: |
| 276 ret = 0 | 166 ret = 0 |
| 277 else: | 167 else: |
| 278 print >> sys.stderr, ret | 168 print >> sys.stderr, ret |
| 279 ret = 1 | 169 ret = 1 |
| 280 sys.stdout.flush() | 170 sys.stdout.flush() |
| 281 sys.stderr.flush() | 171 sys.stderr.flush() |
| 282 os._exit(ret) | 172 os._exit(ret) |
| OLD | NEW |