14''''exec python3 -u -- "$0" ${1+"$@"} # '''
15"""Bootstrap script to clone and forward to the recipe engine tool.
21This is a copy of https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipes.py.
22To fix bugs, fix in the googlesource repo then run the autoroller.
35import urllib.parse
as urlparse
37from collections
import namedtuple
46EngineDep = namedtuple(
'EngineDep',
'url revision branch')
52 full_message = f
'malformed recipes.cfg: {msg}: {path!r}'
56def parse(repo_root, recipes_cfg_path):
57 """Parse is a lightweight a recipes.cfg file parser.
60 repo_root (str) - native path to the root of the repo we're trying to run
62 recipes_cfg_path (str) - native path to the recipes.cfg file to process.
65 engine_dep (EngineDep|
None): The recipe_engine dependency,
or None,
if the
66 current repo IS the recipe_engine.
67 recipes_path (str) - native path to where the recipes live inside of the
68 current repo (i.e. the folder containing `recipes/`
and/
or
71 with open(recipes_cfg_path,
'r', encoding=
'utf-8')
as file:
72 recipes_cfg = json.load(file)
75 if (version := recipes_cfg[
'api_version']) != 2:
80 repo_name = recipes_cfg.get(
'repo_name')
82 repo_name = recipes_cfg[
'project_id']
83 if repo_name ==
'recipe_engine':
84 return None, recipes_cfg.get(
'recipes_path',
'')
86 engine = recipes_cfg[
'deps'][
'recipe_engine']
88 if 'url' not in engine:
90 'Required field "url" in dependency "recipe_engine" not found',
93 engine.setdefault(
'revision',
'')
94 engine.setdefault(
'branch',
'refs/heads/main')
95 recipes_path = recipes_cfg.get(
'recipes_path',
'')
98 if not engine[
'branch'].startswith(
'refs/'):
99 engine[
'branch'] =
'refs/heads/' + engine[
'branch']
101 recipes_path = os.path.join(repo_root,
102 recipes_path.replace(
'/', os.path.sep))
104 except KeyError
as ex:
108IS_WIN = sys.platform.startswith((
'win',
'cygwin'))
110_BAT =
'.bat' if IS_WIN
else ''
113REQUIRED_BINARIES = {GIT, CIPD}
116def _is_executable(path):
117 return os.path.isfile(path)
and os.access(path, os.X_OK)
120def _subprocess_call(argv, **kwargs):
121 logging.info(
'Running %r', argv)
122 return subprocess.call(argv, **kwargs)
125def _git_check_call(argv, **kwargs):
127 logging.info(
'Running %r', argv)
128 subprocess.check_call(argv, **kwargs)
131def _git_output(argv, **kwargs):
133 logging.info(
'Running %r', argv)
134 return subprocess.check_output(argv, **kwargs)
138 """This extracts a subset of the arguments that this bootstrap script cares
139 about. Currently this consists of:
140 * an override for the recipe engine
in the form of `-O recipe_engine=/path`
141 * the --package option.
143 override_prefix = 'recipe_engine='
145 parser = argparse.ArgumentParser(add_help=
False)
146 parser.add_argument(
'-O',
'--project-override', action=
'append')
147 parser.add_argument(
'--package', type=os.path.abspath)
148 args, _ = parser.parse_known_args(argv)
149 for override
in args.project_override
or ():
150 if override.startswith(override_prefix):
151 return override[
len(override_prefix):], args.package
152 return None, args.package
156 """Checks out the recipe_engine repo pinned in recipes.cfg.
158 Returns the path to the recipe engine repo.
160 dep, recipes_path = parse(repo_root, recipes_cfg_path)
163 return os.path.join(repo_root, recipes_path)
167 if not engine_path
and url.startswith(
'file://'):
168 engine_path = urlparse.urlparse(url).path
171 revision = dep.revision
175 engine_path = os.path.join(recipes_path,
'.recipe_deps',
'recipe_engine')
178 _git_check_call([
'init', engine_path], stdout=subprocess.DEVNULL)
181 _git_check_call([
'rev-parse',
'--verify', f
'{revision}^{{commit}}'],
183 stdout=subprocess.DEVNULL,
184 stderr=subprocess.DEVNULL)
185 except subprocess.CalledProcessError:
186 _git_check_call([
'fetch',
'--quiet', url, branch],
188 stdout=subprocess.DEVNULL)
191 _git_check_call([
'diff',
'--quiet', revision], cwd=engine_path)
192 except subprocess.CalledProcessError:
193 index_lock = os.path.join(engine_path,
'.git',
'index.lock')
195 os.remove(index_lock)
196 except OSError
as exc:
197 if exc.errno != errno.ENOENT:
198 logging.warning(
'failed to remove %r, reset will fail: %s',
200 _git_check_call([
'reset',
'-q',
'--hard', revision], cwd=engine_path)
204 _git_check_call([
'clean',
'-qxf'], cwd=engine_path)
210 for required_binary
in REQUIRED_BINARIES:
211 if not shutil.which(required_binary):
212 return f
'Required binary is not found on PATH: {required_binary}'
214 if '--verbose' in sys.argv:
215 logging.getLogger().setLevel(logging.INFO)
218 engine_override, recipes_cfg_path =
parse_args(args)
222 repo_root = os.path.dirname(
223 os.path.dirname(os.path.dirname(recipes_cfg_path)))
227 _git_output([
'rev-parse',
'--show-toplevel'],
228 cwd=os.path.abspath(os.path.dirname(__file__))).strip())
229 repo_root = os.path.abspath(repo_root).
decode()
230 recipes_cfg_path = os.path.join(repo_root,
'infra',
'config',
'recipes.cfg')
231 args = [
'--package', recipes_cfg_path] + args
232 engine_path =
checkout_engine(engine_override, repo_root, recipes_cfg_path)
234 vpython =
'vpython3' + _BAT
235 if not shutil.which(vpython):
236 return f
'Required binary is not found on PATH: {vpython}'
241 os.environ[
'PYTHONPATH'] = engine_path
244 debugger = os.environ.get(
'RECIPE_DEBUGGER',
'')
245 if debugger.startswith(
'pycharm'):
246 spec =
'.pycharm.vpython3'
247 elif debugger.startswith(
'vscode'):
248 spec =
'.vscode.vpython3'
253 os.path.join(engine_path, spec),
255 os.path.join(engine_path,
'recipe_engine',
'main.py'),
262 signal.signal(signal.SIGBREAK, signal.SIG_IGN)
263 signal.signal(signal.SIGINT, signal.SIG_IGN)
264 signal.signal(signal.SIGTERM, signal.SIG_IGN)
265 return _subprocess_call(argv)
267 os.execvp(argv[0], argv)
271if __name__ ==
'__main__':
def parse(repo_root, recipes_cfg_path)
def checkout_engine(engine_path, repo_root, recipes_cfg_path)
static DecodeResult decode(std::string path)