Index: recipes.py |
diff --git a/recipes.py b/recipes.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..79ecf5bb5bc21d4495c525c0df1a6233b2cdc09a |
--- /dev/null |
+++ b/recipes.py |
@@ -0,0 +1,253 @@ |
+#!/usr/bin/env python |
+ |
+"""Tool to interact with recipe repositories. |
+ |
+This tool operates on the nearest ancestor directory containing an |
+infra/config/recipes.cfg. |
+""" |
+ |
+import argparse |
+import ast |
+import json |
+import logging |
+import os |
+import subprocess |
+import sys |
+ |
+ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
+sys.path.insert(0, os.path.join(ROOT_DIR, 'recipe_engine', 'third_party')) |
+sys.path.insert(0, ROOT_DIR) |
+ |
+def get_package_config(args): |
+ from recipe_engine import package |
+ |
+ assert args.package, 'No recipe config (--package) given.' |
+ assert os.path.exists(args.package), ( |
+ 'Given recipes config file %s does not exist.' % args.package) |
+ return ( |
+ package.InfraRepoConfig().from_recipes_cfg(args.package), |
+ package.ProtoFile(args.package) |
+ ) |
+ |
+ |
+def simulation_test(package_deps, args): |
+ from recipe_engine import simulation_test |
+ simulation_test.main(package_deps, args=json.loads(args.args)) |
+ |
+ |
+def lint(package_deps, args): |
+ from recipe_engine import lint_test |
+ lint_test.main(package_deps, args.whitelist or []) |
+ |
+ |
+def run(package_deps, args): |
+ from recipe_engine import run as recipe_run |
+ from recipe_engine import loader |
+ from recipe_engine import package |
+ from recipe_engine.third_party import annotator |
+ |
+ def get_properties_from_args(args): |
+ properties = dict(x.split('=', 1) for x in args) |
+ for key, val in properties.iteritems(): |
+ try: |
+ properties[key] = ast.literal_eval(val) |
+ except (ValueError, SyntaxError): |
+ pass # If a value couldn't be evaluated, keep the string version |
+ return properties |
+ |
+ def get_properties_from_file(filename): |
+ properties_file = sys.stdin if filename == '-' else open(filename) |
+ properties = ast.literal_eval(properties_file.read()) |
+ assert isinstance(properties, dict) |
+ |
+ def get_properties_from_json(args): |
+ return ast.literal_eval(args.properties) |
+ |
+ arg_properties = get_properties_from_args(args.props) |
+ assert len(filter(bool, |
+ [arg_properties, args.properties_file, args.properties])) <= 1, ( |
+ 'Only one source of properties is allowed') |
+ if args.properties: |
+ properties = get_properties_from_json(args.properties) |
+ elif args.properties_file: |
+ properties = get_properties_from_file(args.properties_file) |
+ else: |
+ properties = arg_properties |
+ |
+ properties['recipe'] = args.recipe |
+ |
+ os.environ['PYTHONUNBUFFERED'] = '1' |
+ os.environ['PYTHONIOENCODING'] = 'UTF-8' |
+ |
+ universe = loader.RecipeUniverse(package_deps) |
+ |
+ workdir = (args.workdir or |
+ os.path.join(os.path.dirname(os.path.realpath(__file__)), 'workdir')) |
+ logging.info('Using %s as work directory' % workdir) |
+ if not os.path.exists(workdir): |
+ os.makedirs(workdir) |
+ |
+ old_cwd = os.getcwd() |
+ os.chdir(workdir) |
+ try: |
+ ret = recipe_run.run_steps( |
+ properties, annotator.StructuredAnnotationStream(), universe=universe) |
+ return ret.status_code |
+ finally: |
+ os.chdir(old_cwd) |
+ |
+ |
+def roll(args): |
+ from recipe_engine import package |
+ repo_root, config_file = get_package_config(args) |
+ context = package.PackageContext.from_proto_file(repo_root, config_file) |
+ package_spec = package.PackageSpec.load_proto(config_file) |
+ |
+ for update in package_spec.iterate_consistent_updates(config_file, context): |
+ config_file.write(update.spec.dump()) |
+ print 'Wrote %s' % config_file.path |
+ |
+ updated_deps = { |
+ dep_id: dep.revision |
+ for dep_id, dep in update.spec.deps.iteritems() |
+ if dep.revision != package_spec.deps[dep_id].revision |
+ } |
+ print 'To commit this roll, run:' |
+ print ' '.join([ |
+ 'git commit -a -m "Roll dependencies"', |
+ ' '.join([ '-m "Roll %s to %s"' % (dep_id, rev) |
+ for dep_id, rev in sorted(updated_deps.iteritems())]), |
+ ]) |
+ |
+ break |
+ else: |
+ print 'No consistent rolls found' |
+ |
+ |
+def doc(package_deps, args): |
+ from recipe_engine import doc |
+ doc.main(package_deps) |
+ |
+ |
+def main(): |
+ from recipe_engine import package |
+ |
+ # Super-annoyingly, we need to manually parse for simulation_test since |
+ # argparse is bonkers and doesn't allow us to forward --help to subcommands. |
+ if 'simulation_test' in sys.argv: |
+ index = sys.argv.index('simulation_test') |
+ sys.argv = sys.argv[:index+1] + [json.dumps(sys.argv[index+1:])] |
+ |
+ parser = argparse.ArgumentParser(description='Do things with recipes.') |
+ |
+ parser.add_argument( |
+ '--package', |
+ help='Package to operate on (directory containing ' |
+ 'infra/config/recipes.cfg)') |
+ parser.add_argument( |
+ '--verbose', '-v', action='store_true', |
+ help='Increase logging verboisty') |
+ parser.add_argument( |
+ '--no-fetch', action='store_true', |
+ help='Disable automatic fetching') |
+ parser.add_argument( |
+ '--bootstrap-script', |
+ help='Path to the script used to bootstrap this tool (internal use only)') |
+ |
+ subp = parser.add_subparsers() |
+ |
+ fetch_p = subp.add_parser( |
+ 'fetch', |
+ help='Fetch and update dependencies.') |
+ fetch_p.set_defaults(command='fetch') |
+ |
+ simulation_test_p = subp.add_parser('simulation_test', |
+ help='Generate or check expectations by simulating with mock actions') |
+ simulation_test_p.set_defaults(command='simulation_test') |
+ simulation_test_p.add_argument('args') |
+ |
+ lint_p = subp.add_parser( |
+ 'lint', |
+ help='Check recipes for stylistic and hygenic issues') |
+ lint_p.set_defaults(command='lint') |
+ |
+ lint_p.add_argument( |
+ '--whitelist', '-w', action='append', |
+ help='A regexp matching module names to add to the default whitelist. ' |
+ 'Use multiple times to add multiple patterns,') |
+ |
+ run_p = subp.add_parser( |
+ 'run', |
+ help='Run a recipe locally') |
+ run_p.set_defaults(command='run') |
+ run_p.add_argument( |
+ '--properties-file', |
+ help='A file containing a json blob of properties') |
+ run_p.add_argument( |
+ '--properties', |
+ help='A json string containing the properties') |
+ run_p.add_argument( |
+ '--workdir', |
+ help='The working directory of recipe execution') |
+ run_p.add_argument( |
+ 'recipe', |
+ help='The recipe to execute') |
+ run_p.add_argument( |
+ 'props', nargs=argparse.REMAINDER, |
+ help='A list of property pairs; e.g. mastername=chromium.linux ' |
+ 'issue=12345') |
+ |
+ roll_p = subp.add_parser( |
+ 'roll', |
+ help='Roll dependencies of a recipe package forward (implies fetch)') |
+ roll_p.set_defaults(command='roll') |
+ |
+ show_me_the_modules_p = subp.add_parser( |
+ 'doc', |
+ help='List all known modules reachable from the current package with ' |
+ 'various info about each') |
+ show_me_the_modules_p.set_defaults(command='doc') |
+ |
+ args = parser.parse_args() |
+ |
+ if args.verbose: |
+ logging.getLogger().setLevel(logging.INFO) |
+ |
+ repo_root, config_file = get_package_config(args) |
+ package_deps = package.PackageDeps.create( |
+ repo_root, config_file, allow_fetch=not args.no_fetch) |
+ |
+ if args.command == 'fetch': |
+ # We already did everything in the create() call above. |
+ assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' |
+ return 0 |
+ if args.command == 'simulation_test': |
+ return simulation_test(package_deps, args) |
+ elif args.command == 'lint': |
+ return lint(package_deps, args) |
+ elif args.command == 'run': |
+ return run(package_deps, args) |
+ elif args.command == 'roll': |
+ return roll(args) |
+ elif args.command == 'doc': |
+ return doc(package_deps, args) |
+ else: |
+ print """Dear sir or madam, |
+ It has come to my attention that a quite impossible condition has come |
+ to pass in the specification you have issued a request for us to fulfill. |
+ It is with a heavy heart that I inform you that, at the present juncture, |
+ there is no conceivable next action to be taken upon your request, and as |
+ such, we have decided to abort the request with a nonzero status code. We |
+ hope that your larger goals have not been put at risk due to this |
+ unfortunate circumstance, and wish you the best in deciding the next action |
+ in your venture and larger life. |
+ |
+ Warmly, |
+ recipes.py |
+ """ |
+ return 1 |
+ |
+ return 0 |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |