16d528ed9Sopenharmony_ci#!/bin/sh 26d528ed9Sopenharmony_ci# Copyright 2019 The LUCI Authors. All rights reserved. 36d528ed9Sopenharmony_ci# Use of this source code is governed under the Apache License, Version 2.0 46d528ed9Sopenharmony_ci# that can be found in the LICENSE file. 56d528ed9Sopenharmony_ci 66d528ed9Sopenharmony_ci# We want to run python in unbuffered mode; however shebangs on linux grab the 76d528ed9Sopenharmony_ci# entire rest of the shebang line as a single argument, leading to errors like: 86d528ed9Sopenharmony_ci# 96d528ed9Sopenharmony_ci# /usr/bin/env: 'python3 -u': No such file or directory 106d528ed9Sopenharmony_ci# 116d528ed9Sopenharmony_ci# This little shell hack is a triple-quoted noop in python, but in sh it 126d528ed9Sopenharmony_ci# evaluates to re-exec'ing this script in unbuffered mode. 136d528ed9Sopenharmony_ci# pylint: disable=pointless-string-statement 146d528ed9Sopenharmony_ci''''exec python3 -u -- "$0" ${1+"$@"} # ''' 156d528ed9Sopenharmony_ci# vi: syntax=python 166d528ed9Sopenharmony_ci"""Bootstrap script to clone and forward to the recipe engine tool. 176d528ed9Sopenharmony_ci 186d528ed9Sopenharmony_ci******************* 196d528ed9Sopenharmony_ci** DO NOT MODIFY ** 206d528ed9Sopenharmony_ci******************* 216d528ed9Sopenharmony_ci 226d528ed9Sopenharmony_ciThis is a copy of https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipes.py. 236d528ed9Sopenharmony_ciTo fix bugs, fix in the googlesource repo then run the autoroller. 246d528ed9Sopenharmony_ci""" 256d528ed9Sopenharmony_ci 266d528ed9Sopenharmony_ci# pylint: disable=wrong-import-position 276d528ed9Sopenharmony_ciimport argparse 286d528ed9Sopenharmony_ciimport errno 296d528ed9Sopenharmony_ciimport json 306d528ed9Sopenharmony_ciimport logging 316d528ed9Sopenharmony_ciimport os 326d528ed9Sopenharmony_ciimport subprocess 336d528ed9Sopenharmony_ciimport sys 346d528ed9Sopenharmony_ci 356d528ed9Sopenharmony_cifrom collections import namedtuple 366d528ed9Sopenharmony_cifrom io import open # pylint: disable=redefined-builtin 376d528ed9Sopenharmony_ci 386d528ed9Sopenharmony_citry: 396d528ed9Sopenharmony_ci import urllib.parse as urlparse 406d528ed9Sopenharmony_ciexcept ImportError: 416d528ed9Sopenharmony_ci import urlparse 426d528ed9Sopenharmony_ci 436d528ed9Sopenharmony_ci# The dependency entry for the recipe_engine in the client repo's recipes.cfg 446d528ed9Sopenharmony_ci# 456d528ed9Sopenharmony_ci# url (str) - the url to the engine repo we want to use. 466d528ed9Sopenharmony_ci# revision (str) - the git revision for the engine to get. 476d528ed9Sopenharmony_ci# branch (str) - the branch to fetch for the engine as an absolute ref (e.g. 486d528ed9Sopenharmony_ci# refs/heads/main) 496d528ed9Sopenharmony_ciEngineDep = namedtuple('EngineDep', 'url revision branch') 506d528ed9Sopenharmony_ci 516d528ed9Sopenharmony_ci 526d528ed9Sopenharmony_ciclass MalformedRecipesCfg(Exception): 536d528ed9Sopenharmony_ci 546d528ed9Sopenharmony_ci def __init__(self, msg, path): 556d528ed9Sopenharmony_ci full_message = 'malformed recipes.cfg: %s: %r' % (msg, path) 566d528ed9Sopenharmony_ci super(MalformedRecipesCfg, self).__init__(full_message) 576d528ed9Sopenharmony_ci 586d528ed9Sopenharmony_ci 596d528ed9Sopenharmony_cidef parse(repo_root, recipes_cfg_path): 606d528ed9Sopenharmony_ci """Parse is a lightweight a recipes.cfg file parser. 616d528ed9Sopenharmony_ci 626d528ed9Sopenharmony_ci Args: 636d528ed9Sopenharmony_ci repo_root (str) - native path to the root of the repo we're trying to run 646d528ed9Sopenharmony_ci recipes for. 656d528ed9Sopenharmony_ci recipes_cfg_path (str) - native path to the recipes.cfg file to process. 666d528ed9Sopenharmony_ci 676d528ed9Sopenharmony_ci Returns (as tuple): 686d528ed9Sopenharmony_ci engine_dep (EngineDep|None): The recipe_engine dependency, or None, if the 696d528ed9Sopenharmony_ci current repo IS the recipe_engine. 706d528ed9Sopenharmony_ci recipes_path (str) - native path to where the recipes live inside of the 716d528ed9Sopenharmony_ci current repo (i.e. the folder containing `recipes/` and/or 726d528ed9Sopenharmony_ci `recipe_modules`) 736d528ed9Sopenharmony_ci py3_only (bool) - True if this repo has been marked as ONLY supporting 746d528ed9Sopenharmony_ci python3. 756d528ed9Sopenharmony_ci """ 766d528ed9Sopenharmony_ci with open(recipes_cfg_path, 'r') as fh: 776d528ed9Sopenharmony_ci pb = json.load(fh) 786d528ed9Sopenharmony_ci py3_only = pb.get('py3_only', False) 796d528ed9Sopenharmony_ci 806d528ed9Sopenharmony_ci try: 816d528ed9Sopenharmony_ci if pb['api_version'] != 2: 826d528ed9Sopenharmony_ci raise MalformedRecipesCfg('unknown version %d' % pb['api_version'], 836d528ed9Sopenharmony_ci recipes_cfg_path) 846d528ed9Sopenharmony_ci 856d528ed9Sopenharmony_ci # If we're running ./recipes.py from the recipe_engine repo itself, then 866d528ed9Sopenharmony_ci # return None to signal that there's no EngineDep. 876d528ed9Sopenharmony_ci repo_name = pb.get('repo_name') 886d528ed9Sopenharmony_ci if not repo_name: 896d528ed9Sopenharmony_ci repo_name = pb['project_id'] 906d528ed9Sopenharmony_ci if repo_name == 'recipe_engine': 916d528ed9Sopenharmony_ci return None, pb.get('recipes_path', ''), py3_only 926d528ed9Sopenharmony_ci 936d528ed9Sopenharmony_ci engine = pb['deps']['recipe_engine'] 946d528ed9Sopenharmony_ci 956d528ed9Sopenharmony_ci if 'url' not in engine: 966d528ed9Sopenharmony_ci raise MalformedRecipesCfg( 976d528ed9Sopenharmony_ci 'Required field "url" in dependency "recipe_engine" not found', 986d528ed9Sopenharmony_ci recipes_cfg_path) 996d528ed9Sopenharmony_ci 1006d528ed9Sopenharmony_ci engine.setdefault('revision', '') 1016d528ed9Sopenharmony_ci engine.setdefault('branch', 'refs/heads/main') 1026d528ed9Sopenharmony_ci recipes_path = pb.get('recipes_path', '') 1036d528ed9Sopenharmony_ci 1046d528ed9Sopenharmony_ci # TODO(iannucci): only support absolute refs 1056d528ed9Sopenharmony_ci if not engine['branch'].startswith('refs/'): 1066d528ed9Sopenharmony_ci engine['branch'] = 'refs/heads/' + engine['branch'] 1076d528ed9Sopenharmony_ci 1086d528ed9Sopenharmony_ci recipes_path = os.path.join(repo_root, 1096d528ed9Sopenharmony_ci recipes_path.replace('/', os.path.sep)) 1106d528ed9Sopenharmony_ci return EngineDep(**engine), recipes_path, py3_only 1116d528ed9Sopenharmony_ci except KeyError as ex: 1126d528ed9Sopenharmony_ci raise MalformedRecipesCfg(str(ex), recipes_cfg_path) 1136d528ed9Sopenharmony_ci 1146d528ed9Sopenharmony_ci 1156d528ed9Sopenharmony_ciIS_WIN = sys.platform.startswith(('win', 'cygwin')) 1166d528ed9Sopenharmony_ci 1176d528ed9Sopenharmony_ci_BAT = '.bat' if IS_WIN else '' 1186d528ed9Sopenharmony_ciGIT = 'git' + _BAT 1196d528ed9Sopenharmony_ciCIPD = 'cipd' + _BAT 1206d528ed9Sopenharmony_ciREQUIRED_BINARIES = {GIT, CIPD} 1216d528ed9Sopenharmony_ci 1226d528ed9Sopenharmony_ci 1236d528ed9Sopenharmony_cidef _is_executable(path): 1246d528ed9Sopenharmony_ci return os.path.isfile(path) and os.access(path, os.X_OK) 1256d528ed9Sopenharmony_ci 1266d528ed9Sopenharmony_ci 1276d528ed9Sopenharmony_ci# TODO: Use shutil.which once we switch to Python3. 1286d528ed9Sopenharmony_cidef _is_on_path(basename): 1296d528ed9Sopenharmony_ci for path in os.environ['PATH'].split(os.pathsep): 1306d528ed9Sopenharmony_ci full_path = os.path.join(path, basename) 1316d528ed9Sopenharmony_ci if _is_executable(full_path): 1326d528ed9Sopenharmony_ci return True 1336d528ed9Sopenharmony_ci return False 1346d528ed9Sopenharmony_ci 1356d528ed9Sopenharmony_ci 1366d528ed9Sopenharmony_cidef _subprocess_call(argv, **kwargs): 1376d528ed9Sopenharmony_ci logging.info('Running %r', argv) 1386d528ed9Sopenharmony_ci return subprocess.call(argv, **kwargs) 1396d528ed9Sopenharmony_ci 1406d528ed9Sopenharmony_ci 1416d528ed9Sopenharmony_cidef _git_check_call(argv, **kwargs): 1426d528ed9Sopenharmony_ci argv = [GIT] + argv 1436d528ed9Sopenharmony_ci logging.info('Running %r', argv) 1446d528ed9Sopenharmony_ci subprocess.check_call(argv, **kwargs) 1456d528ed9Sopenharmony_ci 1466d528ed9Sopenharmony_ci 1476d528ed9Sopenharmony_cidef _git_output(argv, **kwargs): 1486d528ed9Sopenharmony_ci argv = [GIT] + argv 1496d528ed9Sopenharmony_ci logging.info('Running %r', argv) 1506d528ed9Sopenharmony_ci return subprocess.check_output(argv, **kwargs) 1516d528ed9Sopenharmony_ci 1526d528ed9Sopenharmony_ci 1536d528ed9Sopenharmony_cidef parse_args(argv): 1546d528ed9Sopenharmony_ci """This extracts a subset of the arguments that this bootstrap script cares 1556d528ed9Sopenharmony_ci about. Currently this consists of: 1566d528ed9Sopenharmony_ci * an override for the recipe engine in the form of `-O recipe_engine=/path` 1576d528ed9Sopenharmony_ci * the --package option. 1586d528ed9Sopenharmony_ci """ 1596d528ed9Sopenharmony_ci PREFIX = 'recipe_engine=' 1606d528ed9Sopenharmony_ci 1616d528ed9Sopenharmony_ci p = argparse.ArgumentParser(add_help=False) 1626d528ed9Sopenharmony_ci p.add_argument('-O', '--project-override', action='append') 1636d528ed9Sopenharmony_ci p.add_argument('--package', type=os.path.abspath) 1646d528ed9Sopenharmony_ci args, _ = p.parse_known_args(argv) 1656d528ed9Sopenharmony_ci for override in args.project_override or (): 1666d528ed9Sopenharmony_ci if override.startswith(PREFIX): 1676d528ed9Sopenharmony_ci return override[len(PREFIX):], args.package 1686d528ed9Sopenharmony_ci return None, args.package 1696d528ed9Sopenharmony_ci 1706d528ed9Sopenharmony_ci 1716d528ed9Sopenharmony_cidef checkout_engine(engine_path, repo_root, recipes_cfg_path): 1726d528ed9Sopenharmony_ci """Checks out the recipe_engine repo pinned in recipes.cfg. 1736d528ed9Sopenharmony_ci 1746d528ed9Sopenharmony_ci Returns the path to the recipe engine repo and the py3_only boolean. 1756d528ed9Sopenharmony_ci """ 1766d528ed9Sopenharmony_ci dep, recipes_path, py3_only = parse(repo_root, recipes_cfg_path) 1776d528ed9Sopenharmony_ci if dep is None: 1786d528ed9Sopenharmony_ci # we're running from the engine repo already! 1796d528ed9Sopenharmony_ci return os.path.join(repo_root, recipes_path), py3_only 1806d528ed9Sopenharmony_ci 1816d528ed9Sopenharmony_ci url = dep.url 1826d528ed9Sopenharmony_ci 1836d528ed9Sopenharmony_ci if not engine_path and url.startswith('file://'): 1846d528ed9Sopenharmony_ci engine_path = urlparse.urlparse(url).path 1856d528ed9Sopenharmony_ci 1866d528ed9Sopenharmony_ci if not engine_path: 1876d528ed9Sopenharmony_ci revision = dep.revision 1886d528ed9Sopenharmony_ci branch = dep.branch 1896d528ed9Sopenharmony_ci 1906d528ed9Sopenharmony_ci # Ensure that we have the recipe engine cloned. 1916d528ed9Sopenharmony_ci engine_path = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine') 1926d528ed9Sopenharmony_ci 1936d528ed9Sopenharmony_ci with open(os.devnull, 'w') as NUL: 1946d528ed9Sopenharmony_ci # Note: this logic mirrors the logic in recipe_engine/fetch.py 1956d528ed9Sopenharmony_ci _git_check_call(['init', engine_path], stdout=NUL) 1966d528ed9Sopenharmony_ci 1976d528ed9Sopenharmony_ci try: 1986d528ed9Sopenharmony_ci _git_check_call(['rev-parse', '--verify', 1996d528ed9Sopenharmony_ci '%s^{commit}' % revision], 2006d528ed9Sopenharmony_ci cwd=engine_path, 2016d528ed9Sopenharmony_ci stdout=NUL, 2026d528ed9Sopenharmony_ci stderr=NUL) 2036d528ed9Sopenharmony_ci except subprocess.CalledProcessError: 2046d528ed9Sopenharmony_ci _git_check_call(['fetch', '--quiet', url, branch], 2056d528ed9Sopenharmony_ci cwd=engine_path, 2066d528ed9Sopenharmony_ci stdout=NUL) 2076d528ed9Sopenharmony_ci 2086d528ed9Sopenharmony_ci try: 2096d528ed9Sopenharmony_ci _git_check_call(['diff', '--quiet', revision], cwd=engine_path) 2106d528ed9Sopenharmony_ci except subprocess.CalledProcessError: 2116d528ed9Sopenharmony_ci index_lock = os.path.join(engine_path, '.git', 'index.lock') 2126d528ed9Sopenharmony_ci try: 2136d528ed9Sopenharmony_ci os.remove(index_lock) 2146d528ed9Sopenharmony_ci except OSError as exc: 2156d528ed9Sopenharmony_ci if exc.errno != errno.ENOENT: 2166d528ed9Sopenharmony_ci logging.warn('failed to remove %r, reset will fail: %s', index_lock, 2176d528ed9Sopenharmony_ci exc) 2186d528ed9Sopenharmony_ci _git_check_call(['reset', '-q', '--hard', revision], cwd=engine_path) 2196d528ed9Sopenharmony_ci 2206d528ed9Sopenharmony_ci # If the engine has refactored/moved modules we need to clean all .pyc files 2216d528ed9Sopenharmony_ci # or things will get squirrely. 2226d528ed9Sopenharmony_ci _git_check_call(['clean', '-qxf'], cwd=engine_path) 2236d528ed9Sopenharmony_ci 2246d528ed9Sopenharmony_ci return engine_path, py3_only 2256d528ed9Sopenharmony_ci 2266d528ed9Sopenharmony_ci 2276d528ed9Sopenharmony_cidef main(): 2286d528ed9Sopenharmony_ci for required_binary in REQUIRED_BINARIES: 2296d528ed9Sopenharmony_ci if not _is_on_path(required_binary): 2306d528ed9Sopenharmony_ci return 'Required binary is not found on PATH: %s' % required_binary 2316d528ed9Sopenharmony_ci 2326d528ed9Sopenharmony_ci if '--verbose' in sys.argv: 2336d528ed9Sopenharmony_ci logging.getLogger().setLevel(logging.INFO) 2346d528ed9Sopenharmony_ci 2356d528ed9Sopenharmony_ci args = sys.argv[1:] 2366d528ed9Sopenharmony_ci engine_override, recipes_cfg_path = parse_args(args) 2376d528ed9Sopenharmony_ci 2386d528ed9Sopenharmony_ci if recipes_cfg_path: 2396d528ed9Sopenharmony_ci # calculate repo_root from recipes_cfg_path 2406d528ed9Sopenharmony_ci repo_root = os.path.dirname( 2416d528ed9Sopenharmony_ci os.path.dirname(os.path.dirname(recipes_cfg_path))) 2426d528ed9Sopenharmony_ci else: 2436d528ed9Sopenharmony_ci # find repo_root with git and calculate recipes_cfg_path 2446d528ed9Sopenharmony_ci repo_root = ( 2456d528ed9Sopenharmony_ci _git_output(['rev-parse', '--show-toplevel'], 2466d528ed9Sopenharmony_ci cwd=os.path.abspath(os.path.dirname(__file__))).strip()) 2476d528ed9Sopenharmony_ci repo_root = os.path.abspath(repo_root).decode() 2486d528ed9Sopenharmony_ci recipes_cfg_path = os.path.join(repo_root, 'infra', 'config', 'recipes.cfg') 2496d528ed9Sopenharmony_ci args = ['--package', recipes_cfg_path] + args 2506d528ed9Sopenharmony_ci engine_path, py3_only = checkout_engine(engine_override, repo_root, recipes_cfg_path) 2516d528ed9Sopenharmony_ci 2526d528ed9Sopenharmony_ci using_py3 = py3_only or os.getenv('RECIPES_USE_PY3') == 'true' 2536d528ed9Sopenharmony_ci vpython = ('vpython' + ('3' if using_py3 else '') + _BAT) 2546d528ed9Sopenharmony_ci if not _is_on_path(vpython): 2556d528ed9Sopenharmony_ci return 'Required binary is not found on PATH: %s' % vpython 2566d528ed9Sopenharmony_ci 2576d528ed9Sopenharmony_ci argv = ([ 2586d528ed9Sopenharmony_ci vpython, '-u', os.path.join(engine_path, 'recipe_engine', 'main.py'), 2596d528ed9Sopenharmony_ci ] + args) 2606d528ed9Sopenharmony_ci 2616d528ed9Sopenharmony_ci if IS_WIN: 2626d528ed9Sopenharmony_ci # No real 'exec' on windows; set these signals to ignore so that they 2636d528ed9Sopenharmony_ci # propagate to our children but we still wait for the child process to quit. 2646d528ed9Sopenharmony_ci import signal 2656d528ed9Sopenharmony_ci signal.signal(signal.SIGBREAK, signal.SIG_IGN) 2666d528ed9Sopenharmony_ci signal.signal(signal.SIGINT, signal.SIG_IGN) 2676d528ed9Sopenharmony_ci signal.signal(signal.SIGTERM, signal.SIG_IGN) 2686d528ed9Sopenharmony_ci return _subprocess_call(argv) 2696d528ed9Sopenharmony_ci else: 2706d528ed9Sopenharmony_ci os.execvp(argv[0], argv) 2716d528ed9Sopenharmony_ci 2726d528ed9Sopenharmony_ci 2736d528ed9Sopenharmony_ciif __name__ == '__main__': 2746d528ed9Sopenharmony_ci sys.exit(main()) 275