Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
google
GitHub Repository: google/crosvm
Path: blob/main/infra/recipes.py
5392 views
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
21
This is a copy of https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipes.py.
22
To fix bugs, fix in the googlesource repo then run the autoroller.
23
"""
24
25
# pylint: disable=wrong-import-position
26
import argparse
27
import errno
28
import json
29
import logging
30
import os
31
import shutil
32
import subprocess
33
import sys
34
35
import urllib.parse as urlparse
36
37
from 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)
46
EngineDep = namedtuple('EngineDep', 'url revision branch')
47
48
49
class 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
56
def 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:
89
raise MalformedRecipesCfg(
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
108
IS_WIN = sys.platform.startswith(('win', 'cygwin'))
109
110
_BAT = '.bat' if IS_WIN else ''
111
GIT = 'git' + _BAT
112
CIPD = 'cipd' + _BAT
113
REQUIRED_BINARIES = {GIT, CIPD}
114
115
116
def _is_executable(path):
117
return os.path.isfile(path) and os.access(path, os.X_OK)
118
119
120
def _subprocess_call(argv, **kwargs):
121
logging.info('Running %r', argv)
122
return subprocess.call(argv, **kwargs)
123
124
125
def _git_check_call(argv, **kwargs):
126
argv = [GIT] + argv
127
logging.info('Running %r', argv)
128
subprocess.check_call(argv, **kwargs)
129
130
131
def _git_output(argv, **kwargs):
132
argv = [GIT] + argv
133
logging.info('Running %r', argv)
134
return subprocess.check_output(argv, **kwargs)
135
136
137
def 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
155
def 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
209
def 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 overwrite PYTHONPATH here on purpose; We don't want any conflicting
239
# environmental path leaking through into the recipe_engine which manages its
240
# environment entirely via vpython.
241
os.environ['PYTHONPATH'] = engine_path
242
243
spec = '.vpython3'
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'
249
250
argv = ([
251
vpython,
252
'-vpython-spec',
253
os.path.join(engine_path, spec),
254
'-u',
255
os.path.join(engine_path, 'recipe_engine', 'main.py'),
256
] + args)
257
258
if IS_WIN:
259
# No real 'exec' on windows; set these signals to ignore so that they
260
# propagate to our children but we still wait for the child process to quit.
261
import signal # pylint: disable=import-outside-toplevel
262
signal.signal(signal.SIGBREAK, signal.SIG_IGN) # pylint: disable=no-member
263
signal.signal(signal.SIGINT, signal.SIG_IGN)
264
signal.signal(signal.SIGTERM, signal.SIG_IGN)
265
return _subprocess_call(argv)
266
267
os.execvp(argv[0], argv)
268
return -1 # should never occur
269
270
271
if __name__ == '__main__':
272
sys.exit(main())
273
274