Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
recipes.py
Go to the documentation of this file.
1#!/bin/sh
2# Copyright 2019 The LUCI Authors. All rights reserved.
3# Use of this source code is governed under the Apache License, Version 2.0
4# that can be found in the LICENSE file.
5
6# We want to run python in unbuffered mode; however shebangs on linux grab the
7# entire rest of the shebang line as a single argument, leading to errors like:
8#
9# /usr/bin/env: 'python3 -u': No such file or directory
10#
11# This little shell hack is a triple-quoted noop in python, but in sh it
12# evaluates to re-exec'ing this script in unbuffered mode.
13# pylint: disable=pointless-string-statement
14''''exec python3 -u -- "$0" ${1+"$@"} # '''
15"""Bootstrap script to clone and forward to the recipe engine tool.
16
17*******************
18** DO NOT MODIFY **
19*******************
20
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.
23"""
24
25# pylint: disable=wrong-import-position
26import argparse
27import errno
28import json
29import logging
30import os
31import shutil
32import subprocess
33import sys
34
35import urllib.parse as urlparse
36
37from collections import namedtuple
38
39
40# The dependency entry for the recipe_engine in the client repo's recipes.cfg
41#
42# url (str) - the url to the engine repo we want to use.
43# revision (str) - the git revision for the engine to get.
44# branch (str) - the branch to fetch for the engine as an absolute ref (e.g.
45# refs/heads/main)
46EngineDep = namedtuple('EngineDep', 'url revision branch')
47
48
49class MalformedRecipesCfg(Exception):
50
51 def __init__(self, msg, path):
52 full_message = f'malformed recipes.cfg: {msg}: {path!r}'
53 super().__init__(full_message)
54
55
56def parse(repo_root, recipes_cfg_path):
57 """Parse is a lightweight a recipes.cfg file parser.
58
59 Args:
60 repo_root (str) - native path to the root of the repo we're trying to run
61 recipes for.
62 recipes_cfg_path (str) - native path to the recipes.cfg file to process.
63
64 Returns (as tuple):
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
69 `recipe_modules`)
70 """
71 with open(recipes_cfg_path, 'r', encoding='utf-8') as file:
72 recipes_cfg = json.load(file)
73
74 try:
75 if (version := recipes_cfg['api_version']) != 2:
76 raise MalformedRecipesCfg(f'unknown version {version}', recipes_cfg_path)
77
78 # If we're running ./recipes.py from the recipe_engine repo itself, then
79 # return None to signal that there's no EngineDep.
80 repo_name = recipes_cfg.get('repo_name')
81 if not repo_name:
82 repo_name = recipes_cfg['project_id']
83 if repo_name == 'recipe_engine':
84 return None, recipes_cfg.get('recipes_path', '')
85
86 engine = recipes_cfg['deps']['recipe_engine']
87
88 if 'url' not in engine:
90 'Required field "url" in dependency "recipe_engine" not found',
91 recipes_cfg_path)
92
93 engine.setdefault('revision', '')
94 engine.setdefault('branch', 'refs/heads/main')
95 recipes_path = recipes_cfg.get('recipes_path', '')
96
97 # TODO(iannucci): only support absolute refs
98 if not engine['branch'].startswith('refs/'):
99 engine['branch'] = 'refs/heads/' + engine['branch']
100
101 recipes_path = os.path.join(repo_root,
102 recipes_path.replace('/', os.path.sep))
103 return EngineDep(**engine), recipes_path
104 except KeyError as ex:
105 raise MalformedRecipesCfg(str(ex), recipes_cfg_path) from ex
106
107
108IS_WIN = sys.platform.startswith(('win', 'cygwin'))
109
110_BAT = '.bat' if IS_WIN else ''
111GIT = 'git' + _BAT
112CIPD = 'cipd' + _BAT
113REQUIRED_BINARIES = {GIT, CIPD}
114
115
117 return os.path.isfile(path) and os.access(path, os.X_OK)
118
119
120def _subprocess_call(argv, **kwargs):
121 logging.info('Running %r', argv)
122 return subprocess.call(argv, **kwargs)
123
124
125def _git_check_call(argv, **kwargs):
126 argv = [GIT] + argv
127 logging.info('Running %r', argv)
128 subprocess.check_call(argv, **kwargs)
129
130
131def _git_output(argv, **kwargs):
132 argv = [GIT] + argv
133 logging.info('Running %r', argv)
134 return subprocess.check_output(argv, **kwargs)
135
136
137def parse_args(argv):
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.
142 """
143 override_prefix = 'recipe_engine='
144
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
153
154
155def checkout_engine(engine_path, repo_root, recipes_cfg_path):
156 """Checks out the recipe_engine repo pinned in recipes.cfg.
157
158 Returns the path to the recipe engine repo.
159 """
160 dep, recipes_path = parse(repo_root, recipes_cfg_path)
161 if dep is None:
162 # we're running from the engine repo already!
163 return os.path.join(repo_root, recipes_path)
164
165 url = dep.url
166
167 if not engine_path and url.startswith('file://'):
168 engine_path = urlparse.urlparse(url).path
169
170 if not engine_path:
171 revision = dep.revision
172 branch = dep.branch
173
174 # Ensure that we have the recipe engine cloned.
175 engine_path = os.path.join(recipes_path, '.recipe_deps', 'recipe_engine')
176
177 # Note: this logic mirrors the logic in recipe_engine/fetch.py
178 _git_check_call(['init', engine_path], stdout=subprocess.DEVNULL)
179
180 try:
181 _git_check_call(['rev-parse', '--verify', f'{revision}^{{commit}}'],
182 cwd=engine_path,
183 stdout=subprocess.DEVNULL,
184 stderr=subprocess.DEVNULL)
185 except subprocess.CalledProcessError:
186 _git_check_call(['fetch', '--quiet', url, branch],
187 cwd=engine_path,
188 stdout=subprocess.DEVNULL)
189
190 try:
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')
194 try:
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',
199 index_lock, exc)
200 _git_check_call(['reset', '-q', '--hard', revision], cwd=engine_path)
201
202 # If the engine has refactored/moved modules we need to clean all .pyc files
203 # or things will get squirrely.
204 _git_check_call(['clean', '-qxf'], cwd=engine_path)
205
206 return engine_path
207
208
209def main():
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}'
213
214 if '--verbose' in sys.argv:
215 logging.getLogger().setLevel(logging.INFO)
216
217 args = sys.argv[1:]
218 engine_override, recipes_cfg_path = parse_args(args)
219
220 if recipes_cfg_path:
221 # calculate repo_root from recipes_cfg_path
222 repo_root = os.path.dirname(
223 os.path.dirname(os.path.dirname(recipes_cfg_path)))
224 else:
225 # find repo_root with git and calculate recipes_cfg_path
226 repo_root = (
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)
233
234 vpython = 'vpython3' + _BAT
235 if not shutil.which(vpython):
236 return f'Required binary is not found on PATH: {vpython}'
237
238 # We unset PYTHONPATH here in case the user has conflicting environmental
239 # things we don't want them to leak through into the recipe_engine which
240 # manages its environment entirely via vpython.
241 #
242 # NOTE: os.unsetenv unhelpfully doesn't exist on all platforms until python3.9
243 # so we have to use the cutesy `pop` formulation below until then...
244 os.environ.pop("PYTHONPATH", None)
245
246 spec = '.vpython3'
247 debugger = os.environ.get('RECIPE_DEBUGGER', '')
248 if debugger.startswith('pycharm'):
249 spec = '.pycharm.vpython3'
250 elif debugger.startswith('vscode'):
251 spec = '.vscode.vpython3'
252
253 argv = ([
254 vpython,
255 '-vpython-spec',
256 os.path.join(engine_path, spec),
257 '-u',
258 os.path.join(engine_path, 'recipe_engine', 'main.py'),
259 ] + args)
260
261 if IS_WIN:
262 # No real 'exec' on windows; set these signals to ignore so that they
263 # propagate to our children but we still wait for the child process to quit.
264 import signal # pylint: disable=import-outside-toplevel
265 signal.signal(signal.SIGBREAK, signal.SIG_IGN) # pylint: disable=no-member
266 signal.signal(signal.SIGINT, signal.SIG_IGN)
267 signal.signal(signal.SIGTERM, signal.SIG_IGN)
268 return _subprocess_call(argv)
269
270 os.execvp(argv[0], argv)
271 return -1 # should never occur
272
273
274if __name__ == '__main__':
275 sys.exit(main())
__init__(self, msg, path)
Definition recipes.py:51
Definition main.py:1
_git_check_call(argv, **kwargs)
Definition recipes.py:125
_is_executable(path)
Definition recipes.py:116
_git_output(argv, **kwargs)
Definition recipes.py:131
parse_args(argv)
Definition recipes.py:137
checkout_engine(engine_path, repo_root, recipes_cfg_path)
Definition recipes.py:155
_subprocess_call(argv, **kwargs)
Definition recipes.py:120
EngineDep
Definition recipes.py:46
static DecodeResult decode(std::string path)