1cb93a386Sopenharmony_ci#!/bin/sh 2cb93a386Sopenharmony_ci# Copyright 2019 The LUCI Authors. All rights reserved. 3cb93a386Sopenharmony_ci# Use of this source code is governed under the Apache License, Version 2.0 4cb93a386Sopenharmony_ci# that can be found in the LICENSE file. 5cb93a386Sopenharmony_ci 6cb93a386Sopenharmony_ci# We want to run python in unbuffered mode; however shebangs on linux grab the 7cb93a386Sopenharmony_ci# entire rest of the shebang line as a single argument, leading to errors like: 8cb93a386Sopenharmony_ci# 9cb93a386Sopenharmony_ci# /usr/bin/env: 'python3 -u': No such file or directory 10cb93a386Sopenharmony_ci# 11cb93a386Sopenharmony_ci# This little shell hack is a triple-quoted noop in python, but in sh it 12cb93a386Sopenharmony_ci# evaluates to re-exec'ing this script in unbuffered mode. 13cb93a386Sopenharmony_ci# pylint: disable=pointless-string-statement 14cb93a386Sopenharmony_ci''''exec python3 -u -- "$0" ${1+"$@"} # ''' 15cb93a386Sopenharmony_ci# vi: syntax=python 16cb93a386Sopenharmony_ci"""Bootstrap script to clone and forward to the recipe engine tool. 17cb93a386Sopenharmony_ci 18cb93a386Sopenharmony_ci******************* 19cb93a386Sopenharmony_ci** DO NOT MODIFY ** 20cb93a386Sopenharmony_ci******************* 21cb93a386Sopenharmony_ci 22cb93a386Sopenharmony_ciThis is a copy of https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipes.py. 23cb93a386Sopenharmony_ciTo fix bugs, fix in the googlesource repo then run the autoroller. 24cb93a386Sopenharmony_ci""" 25cb93a386Sopenharmony_ci 26cb93a386Sopenharmony_ci# pylint: disable=wrong-import-position 27cb93a386Sopenharmony_ciimport argparse 28cb93a386Sopenharmony_ciimport errno 29cb93a386Sopenharmony_ciimport json 30cb93a386Sopenharmony_ciimport logging 31cb93a386Sopenharmony_ciimport os 32cb93a386Sopenharmony_ciimport subprocess 33cb93a386Sopenharmony_ciimport sys 34cb93a386Sopenharmony_ci 35cb93a386Sopenharmony_cifrom collections import namedtuple 36cb93a386Sopenharmony_cifrom io import open # pylint: disable=redefined-builtin 37cb93a386Sopenharmony_ci 38cb93a386Sopenharmony_citry: 39cb93a386Sopenharmony_ci import urllib.parse as urlparse 40cb93a386Sopenharmony_ciexcept ImportError: 41cb93a386Sopenharmony_ci import urlparse 42cb93a386Sopenharmony_ci 43cb93a386Sopenharmony_ci# The dependency entry for the recipe_engine in the client repo's recipes.cfg 44cb93a386Sopenharmony_ci# 45cb93a386Sopenharmony_ci# url (str) - the url to the engine repo we want to use. 46cb93a386Sopenharmony_ci# revision (str) - the git revision for the engine to get. 47cb93a386Sopenharmony_ci# branch (str) - the branch to fetch for the engine as an absolute ref (e.g. 48cb93a386Sopenharmony_ci# refs/heads/main) 49cb93a386Sopenharmony_ciEngineDep = namedtuple('EngineDep', 'url revision branch') 50cb93a386Sopenharmony_ci 51cb93a386Sopenharmony_ci 52cb93a386Sopenharmony_ciclass MalformedRecipesCfg(Exception): 53cb93a386Sopenharmony_ci 54cb93a386Sopenharmony_ci def __init__(self, msg, path): 55cb93a386Sopenharmony_ci full_message = 'malformed recipes.cfg: %s: %r' % (msg, path) 56cb93a386Sopenharmony_ci super(MalformedRecipesCfg, self).__init__(full_message) 57cb93a386Sopenharmony_ci 58cb93a386Sopenharmony_ci 59cb93a386Sopenharmony_cidef parse(repo_root, recipes_cfg_path): 60cb93a386Sopenharmony_ci """Parse is a lightweight a recipes.cfg file parser. 61cb93a386Sopenharmony_ci 62cb93a386Sopenharmony_ci Args: 63cb93a386Sopenharmony_ci repo_root (str) - native path to the root of the repo we're trying to run 64cb93a386Sopenharmony_ci recipes for. 65cb93a386Sopenharmony_ci recipes_cfg_path (str) - native path to the recipes.cfg file to process. 66cb93a386Sopenharmony_ci 67cb93a386Sopenharmony_ci Returns (as tuple): 68cb93a386Sopenharmony_ci engine_dep (EngineDep|None): The recipe_engine dependency, or None, if the 69cb93a386Sopenharmony_ci current repo IS the recipe_engine. 70cb93a386Sopenharmony_ci recipes_path (str) - native path to where the recipes live inside of the 71cb93a386Sopenharmony_ci current repo (i.e. the folder containing `recipes/` and/or 72cb93a386Sopenharmony_ci `recipe_modules`) 73cb93a386Sopenharmony_ci """ 74cb93a386Sopenharmony_ci with open(recipes_cfg_path, 'r') as fh: 75cb93a386Sopenharmony_ci pb = json.load(fh) 76cb93a386Sopenharmony_ci 77cb93a386Sopenharmony_ci try: 78cb93a386Sopenharmony_ci if pb['api_version'] != 2: 79cb93a386Sopenharmony_ci raise MalformedRecipesCfg('unknown version %d' % pb['api_version'], 80cb93a386Sopenharmony_ci recipes_cfg_path) 81cb93a386Sopenharmony_ci 82cb93a386Sopenharmony_ci # If we're running ./recipes.py from the recipe_engine repo itself, then 83cb93a386Sopenharmony_ci # return None to signal that there's no EngineDep. 84cb93a386Sopenharmony_ci repo_name = pb.get('repo_name') 85cb93a386Sopenharmony_ci if not repo_name: 86cb93a386Sopenharmony_ci repo_name = pb['project_id'] 87cb93a386Sopenharmony_ci if repo_name == 'recipe_engine': 88cb93a386Sopenharmony_ci return None, pb.get('recipes_path', '') 89cb93a386Sopenharmony_ci 90cb93a386Sopenharmony_ci engine = pb['deps']['recipe_engine'] 91cb93a386Sopenharmony_ci 92cb93a386Sopenharmony_ci if 'url' not in engine: 93cb93a386Sopenharmony_ci raise MalformedRecipesCfg( 94cb93a386Sopenharmony_ci 'Required field "url" in dependency "recipe_engine" not found', 95cb93a386Sopenharmony_ci recipes_cfg_path) 96cb93a386Sopenharmony_ci 97cb93a386Sopenharmony_ci engine.setdefault('revision', '') 98cb93a386Sopenharmony_ci engine.setdefault('branch', 'refs/heads/main') 99cb93a386Sopenharmony_ci recipes_path = pb.get('recipes_path', '') 100cb93a386Sopenharmony_ci 101cb93a386Sopenharmony_ci # TODO(iannucci): only support absolute refs 102cb93a386Sopenharmony_ci if not engine['branch'].startswith('refs/'): 103cb93a386Sopenharmony_ci engine['branch'] = 'refs/heads/' + engine['branch'] 104cb93a386Sopenharmony_ci 105cb93a386Sopenharmony_ci recipes_path = os.path.join(repo_root, 106cb93a386Sopenharmony_ci recipes_path.replace('/', os.path.sep)) 107cb93a386Sopenharmony_ci return EngineDep(**engine), recipes_path 108cb93a386Sopenharmony_ci except KeyError as ex: 109cb93a386Sopenharmony_ci raise MalformedRecipesCfg(str(ex), recipes_cfg_path) 110cb93a386Sopenharmony_ci 111cb93a386Sopenharmony_ci 112cb93a386Sopenharmony_ciIS_WIN = sys.platform.startswith(('win', 'cygwin')) 113cb93a386Sopenharmony_ci 114cb93a386Sopenharmony_ci_BAT = '.bat' if IS_WIN else '' 115cb93a386Sopenharmony_ciGIT = 'git' + _BAT 116cb93a386Sopenharmony_ciVPYTHON = ('vpython' + 117cb93a386Sopenharmony_ci ('3' if os.getenv('RECIPES_USE_PY3') == 'true' else '') + 118cb93a386Sopenharmony_ci _BAT) 119cb93a386Sopenharmony_ciCIPD = 'cipd' + _BAT 120cb93a386Sopenharmony_ciREQUIRED_BINARIES = {GIT, VPYTHON, CIPD} 121cb93a386Sopenharmony_ci 122cb93a386Sopenharmony_ci 123cb93a386Sopenharmony_cidef _is_executable(path): 124cb93a386Sopenharmony_ci return os.path.isfile(path) and os.access(path, os.X_OK) 125cb93a386Sopenharmony_ci 126cb93a386Sopenharmony_ci 127cb93a386Sopenharmony_ci# TODO: Use shutil.which once we switch to Python3. 128cb93a386Sopenharmony_cidef _is_on_path(basename): 129cb93a386Sopenharmony_ci for path in os.environ['PATH'].split(os.pathsep): 130cb93a386Sopenharmony_ci full_path = os.path.join(path, basename) 131cb93a386Sopenharmony_ci if _is_executable(full_path): 132cb93a386Sopenharmony_ci return True 133cb93a386Sopenharmony_ci return False 134cb93a386Sopenharmony_ci 135cb93a386Sopenharmony_ci 136cb93a386Sopenharmony_cidef _subprocess_call(argv, **kwargs): 137cb93a386Sopenharmony_ci logging.info('Running %r', argv) 138cb93a386Sopenharmony_ci return subprocess.call(argv, **kwargs) 139cb93a386Sopenharmony_ci 140cb93a386Sopenharmony_ci 141cb93a386Sopenharmony_cidef _git_check_call(argv, **kwargs): 142cb93a386Sopenharmony_ci argv = [GIT] + argv 143cb93a386Sopenharmony_ci logging.info('Running %r', argv) 144cb93a386Sopenharmony_ci subprocess.check_call(argv, **kwargs) 145cb93a386Sopenharmony_ci 146cb93a386Sopenharmony_ci 147cb93a386Sopenharmony_cidef _git_output(argv, **kwargs): 148cb93a386Sopenharmony_ci argv = [GIT] + argv 149cb93a386Sopenharmony_ci logging.info('Running %r', argv) 150cb93a386Sopenharmony_ci return subprocess.check_output(argv, **kwargs) 151cb93a386Sopenharmony_ci 152cb93a386Sopenharmony_ci 153cb93a386Sopenharmony_cidef parse_args(argv): 154cb93a386Sopenharmony_ci """This extracts a subset of the arguments that this bootstrap script cares 155cb93a386Sopenharmony_ci about. Currently this consists of: 156cb93a386Sopenharmony_ci * an override for the recipe engine in the form of `-O recipe_engine=/path` 157cb93a386Sopenharmony_ci * the --package option. 158cb93a386Sopenharmony_ci """ 159cb93a386Sopenharmony_ci PREFIX = 'recipe_engine=' 160cb93a386Sopenharmony_ci 161cb93a386Sopenharmony_ci p = argparse.ArgumentParser(add_help=False) 162cb93a386Sopenharmony_ci p.add_argument('-O', '--project-override', action='append') 163cb93a386Sopenharmony_ci p.add_argument('--package', type=os.path.abspath) 164cb93a386Sopenharmony_ci args, _ = p.parse_known_args(argv) 165cb93a386Sopenharmony_ci for override in args.project_override or (): 166cb93a386Sopenharmony_ci if override.startswith(PREFIX): 167cb93a386Sopenharmony_ci return override[len(PREFIX):], args.package 168cb93a386Sopenharmony_ci return None, args.package 169cb93a386Sopenharmony_ci 170cb93a386Sopenharmony_ci 171cb93a386Sopenharmony_cidef checkout_engine(engine_path, repo_root, recipes_cfg_path): 172cb93a386Sopenharmony_ci dep, recipes_path = parse(repo_root, recipes_cfg_path) 173cb93a386Sopenharmony_ci if dep is None: 174cb93a386Sopenharmony_ci # we're running from the engine repo already! 175cb93a386Sopenharmony_ci return os.path.join(repo_root, recipes_path) 176cb93a386Sopenharmony_ci 177cb93a386Sopenharmony_ci url = dep.url 178cb93a386Sopenharmony_ci 179cb93a386Sopenharmony_ci if not engine_path and url.startswith('file://'): 180cb93a386Sopenharmony_ci engine_path = urlparse.urlparse(url).path 181cb93a386Sopenharmony_ci 182cb93a386Sopenharmony_ci if not engine_path: 183cb93a386Sopenharmony_ci revision = dep.revision 184cb93a386Sopenharmony_ci branch = dep.branch 185cb93a386Sopenharmony_ci 186cb93a386Sopenharmony_ci # Ensure that we have the recipe engine cloned. 187cb93a386Sopenharmony_ci engine_path = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine') 188cb93a386Sopenharmony_ci 189cb93a386Sopenharmony_ci with open(os.devnull, 'w') as NUL: 190cb93a386Sopenharmony_ci # Note: this logic mirrors the logic in recipe_engine/fetch.py 191cb93a386Sopenharmony_ci _git_check_call(['init', engine_path], stdout=NUL) 192cb93a386Sopenharmony_ci 193cb93a386Sopenharmony_ci try: 194cb93a386Sopenharmony_ci _git_check_call(['rev-parse', '--verify', 195cb93a386Sopenharmony_ci '%s^{commit}' % revision], 196cb93a386Sopenharmony_ci cwd=engine_path, 197cb93a386Sopenharmony_ci stdout=NUL, 198cb93a386Sopenharmony_ci stderr=NUL) 199cb93a386Sopenharmony_ci except subprocess.CalledProcessError: 200cb93a386Sopenharmony_ci _git_check_call(['fetch', url, branch], 201cb93a386Sopenharmony_ci cwd=engine_path, 202cb93a386Sopenharmony_ci stdout=NUL, 203cb93a386Sopenharmony_ci stderr=NUL) 204cb93a386Sopenharmony_ci 205cb93a386Sopenharmony_ci try: 206cb93a386Sopenharmony_ci _git_check_call(['diff', '--quiet', revision], cwd=engine_path) 207cb93a386Sopenharmony_ci except subprocess.CalledProcessError: 208cb93a386Sopenharmony_ci index_lock = os.path.join(engine_path, '.git', 'index.lock') 209cb93a386Sopenharmony_ci try: 210cb93a386Sopenharmony_ci os.remove(index_lock) 211cb93a386Sopenharmony_ci except OSError as exc: 212cb93a386Sopenharmony_ci if exc.errno != errno.ENOENT: 213cb93a386Sopenharmony_ci logging.warn('failed to remove %r, reset will fail: %s', index_lock, 214cb93a386Sopenharmony_ci exc) 215cb93a386Sopenharmony_ci _git_check_call(['reset', '-q', '--hard', revision], cwd=engine_path) 216cb93a386Sopenharmony_ci 217cb93a386Sopenharmony_ci # If the engine has refactored/moved modules we need to clean all .pyc files 218cb93a386Sopenharmony_ci # or things will get squirrely. 219cb93a386Sopenharmony_ci _git_check_call(['clean', '-qxf'], cwd=engine_path) 220cb93a386Sopenharmony_ci 221cb93a386Sopenharmony_ci return engine_path 222cb93a386Sopenharmony_ci 223cb93a386Sopenharmony_ci 224cb93a386Sopenharmony_cidef main(): 225cb93a386Sopenharmony_ci for required_binary in REQUIRED_BINARIES: 226cb93a386Sopenharmony_ci if not _is_on_path(required_binary): 227cb93a386Sopenharmony_ci return 'Required binary is not found on PATH: %s' % required_binary 228cb93a386Sopenharmony_ci 229cb93a386Sopenharmony_ci if '--verbose' in sys.argv: 230cb93a386Sopenharmony_ci logging.getLogger().setLevel(logging.INFO) 231cb93a386Sopenharmony_ci 232cb93a386Sopenharmony_ci args = sys.argv[1:] 233cb93a386Sopenharmony_ci engine_override, recipes_cfg_path = parse_args(args) 234cb93a386Sopenharmony_ci 235cb93a386Sopenharmony_ci if recipes_cfg_path: 236cb93a386Sopenharmony_ci # calculate repo_root from recipes_cfg_path 237cb93a386Sopenharmony_ci repo_root = os.path.dirname( 238cb93a386Sopenharmony_ci os.path.dirname(os.path.dirname(recipes_cfg_path))) 239cb93a386Sopenharmony_ci else: 240cb93a386Sopenharmony_ci # find repo_root with git and calculate recipes_cfg_path 241cb93a386Sopenharmony_ci repo_root = ( 242cb93a386Sopenharmony_ci _git_output(['rev-parse', '--show-toplevel'], 243cb93a386Sopenharmony_ci cwd=os.path.abspath(os.path.dirname(__file__))).strip()) 244cb93a386Sopenharmony_ci repo_root = os.path.abspath(repo_root).decode() 245cb93a386Sopenharmony_ci recipes_cfg_path = os.path.join(repo_root, 'infra', 'config', 'recipes.cfg') 246cb93a386Sopenharmony_ci args = ['--package', recipes_cfg_path] + args 247cb93a386Sopenharmony_ci engine_path = checkout_engine(engine_override, repo_root, recipes_cfg_path) 248cb93a386Sopenharmony_ci 249cb93a386Sopenharmony_ci argv = ( 250cb93a386Sopenharmony_ci [VPYTHON, '-u', 251cb93a386Sopenharmony_ci os.path.join(engine_path, 'recipe_engine', 'main.py')] + args) 252cb93a386Sopenharmony_ci 253cb93a386Sopenharmony_ci if IS_WIN: 254cb93a386Sopenharmony_ci # No real 'exec' on windows; set these signals to ignore so that they 255cb93a386Sopenharmony_ci # propagate to our children but we still wait for the child process to quit. 256cb93a386Sopenharmony_ci import signal 257cb93a386Sopenharmony_ci signal.signal(signal.SIGBREAK, signal.SIG_IGN) 258cb93a386Sopenharmony_ci signal.signal(signal.SIGINT, signal.SIG_IGN) 259cb93a386Sopenharmony_ci signal.signal(signal.SIGTERM, signal.SIG_IGN) 260cb93a386Sopenharmony_ci return _subprocess_call(argv) 261cb93a386Sopenharmony_ci else: 262cb93a386Sopenharmony_ci os.execvp(argv[0], argv) 263cb93a386Sopenharmony_ci 264cb93a386Sopenharmony_ci 265cb93a386Sopenharmony_ciif __name__ == '__main__': 266cb93a386Sopenharmony_ci sys.exit(main()) 267