blob: c9a95ca1d7a744eb164a2b9fab0363da8b633898 [file] [log] [blame]
#!/usr/bin/env vpython
# Copyright 2017 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Tool to interact with recipe repositories.
This tool operates on the nearest ancestor directory containing an
infra/config/recipes.cfg.
"""
import logging
import os
import shutil
import subprocess
import sys
import tempfile
# This is necessary to ensure that str literals are by-default assumed to hold
# utf-8. It also makes the implicit str(unicode(...)) act like
# unicode(...).encode('utf-8'), rather than unicode(...).encode('ascii') .
reload(sys)
sys.setdefaultencoding('UTF8')
import urllib3.contrib.pyopenssl
urllib3.contrib.pyopenssl.inject_into_urllib3()
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, ROOT_DIR)
from recipe_engine import env
from recipe_engine import common_args, package, package_io, util
import argparse # this is vendored
from recipe_engine import fetch, lint, bundle, depgraph, analyze, autoroll
from recipe_engine import remote, refs, doc, test, run
# Each of these subcommands has a method:
#
# def add_subparsers(argparse._SubParsersAction): ...
#
# which is expected to add a subparser by calling .add_parser on it. In
# addition, the add_subparsers method should call .set_defaults on the created
# sub-parser, and set the following values:
# func (fn(package_deps, args)) - The function called if the sub command is
# invoked.
# postprocess_func (fn(parser, args)) - A validation/normalization function
# which is called if the sub command is invoked. This function can
# check/adjust the parsed args, calling parser.error if a problem is
# encountered. This function is optional.
# bare_command (bool) - This sub command's func will be called before parsing
# package_deps. This is only used for the `remote` subcommand. See the
# comment in add_common_args for why.
#
# Example:
#
# def add_subparsers(parser):
# sub = parser.add_parser("subcommand", help="a cool subcommand")
# sub.add_argument("--cool_arg", help="it's gotta be cool")
#
# def postprocess_args(parser, args):
# if "cool" not in args.cool_arg:
# parser.error("your cool_arg is not cool!")
#
# sub.set_defaults(func=main)
#
# def main(package_deps, args):
# print args.cool_arg
_SUBCOMMANDS = [
run,
test,
analyze,
autoroll,
bundle,
depgraph,
doc,
fetch,
lint,
refs,
remote,
]
def main():
# Prune all evidence of VPython/VirtualEnv out of the environment. This means
# that recipe engine 'unwraps' vpython VirtualEnv path/env manipulation.
# Invocations of `python` from recipes should never inherit the recipe
# engine's own VirtualEnv.
# Set by VirtualEnv, no need to keep it.
os.environ.pop('VIRTUAL_ENV', None)
# Set by VPython, if recipes want it back they have to set it explicitly.
os.environ.pop('PYTHONNOUSERSITE', None)
# Look for "activate_this.py" in this path, which is installed by VirtualEnv.
# This mechanism is used by vpython as well to sanitize VirtualEnvs from
# $PATH.
os.environ['PATH'] = os.pathsep.join([
p for p in os.environ.get('PATH', '').split(os.pathsep)
if not os.path.isfile(os.path.join(p, 'activate_this.py'))
])
parser = argparse.ArgumentParser(
description='Interact with the recipe system.')
common_postprocess_func = common_args.add_common_args(parser)
subp = parser.add_subparsers(dest='command')
for module in _SUBCOMMANDS:
module.add_subparser(subp)
args = parser.parse_args()
common_postprocess_func(parser, args)
args.postprocess_func(parser, args)
if args.bare_command:
return args.func(None, args)
repo_root = package_io.InfraRepoConfig().from_recipes_cfg(args.package.path)
try:
# TODO(phajdan.jr): gracefully handle inconsistent deps when rolling.
# This fails if the starting point does not have consistent dependency
# graph. When performing an automated roll, it'd make sense to attempt
# to automatically find a consistent state, rather than bailing out.
# Especially that only some subcommands refer to package_deps.
context = package.PackageContext.from_package_pb(
repo_root, args.package.read())
package_deps = package.PackageDeps.create(
context, args.package, overrides=args.project_override)
except subprocess.CalledProcessError:
# A git checkout failed somewhere. Return 2, which is the sign that this is
# an infra failure, rather than a test failure.
return 2
return args.func(package_deps, args)
if __name__ == '__main__':
# Use os._exit instead of sys.exit to prevent the python interpreter from
# hanging on threads/processes which may have been spawned and not reaped
# (e.g. by a leaky test harness).
try:
ret = main()
except Exception as e:
import traceback
traceback.print_exc(file=sys.stderr)
print >> sys.stderr, 'Uncaught exception (%s): %s' % (type(e).__name__, e)
sys.exit(1)
if not isinstance(ret, int):
if ret is None:
ret = 0
else:
print >> sys.stderr, ret
ret = 1
sys.stdout.flush()
sys.stderr.flush()
os._exit(ret)