blob: 54ad45c2f1cacd822146c6e2d094184fb6904b12 [file] [log] [blame]
Richard Levasseure53b0b72024-01-31 08:53:05 -08001# Copyright 2024 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Functionality shared only by repository rule phase code.
16
17This code should only be loaded and used during the repository phase.
18"""
19
20REPO_DEBUG_ENV_VAR = "RULES_PYTHON_REPO_DEBUG"
Ignas Anikeviciusb4b52fc2024-06-01 13:36:48 +090021REPO_VERBOSITY_ENV_VAR = "RULES_PYTHON_REPO_DEBUG_VERBOSITY"
Richard Levasseure53b0b72024-01-31 08:53:05 -080022
23def _is_repo_debug_enabled(rctx):
24 """Tells if debbugging output is requested during repo operatiosn.
25
26 Args:
27 rctx: repository_ctx object
28
29 Returns:
30 True if enabled, False if not.
31 """
32 return rctx.os.environ.get(REPO_DEBUG_ENV_VAR) == "1"
33
34def _debug_print(rctx, message_cb):
35 """Prints a message if repo debugging is enabled.
36
37 Args:
38 rctx: repository_ctx
39 message_cb: Callable that returns the string to print. Takes
40 no arguments.
41 """
42 if _is_repo_debug_enabled(rctx):
43 print(message_cb()) # buildifier: disable=print
44
Ignas Anikeviciusb4b52fc2024-06-01 13:36:48 +090045def _logger(rctx):
46 """Creates a logger instance for printing messages.
47
48 Args:
49 rctx: repository_ctx object.
50
51 Returns:
52 A struct with attributes logging: trace, debug, info, warn, fail.
53 """
54 if _is_repo_debug_enabled(rctx):
55 verbosity_level = "DEBUG"
56 else:
57 verbosity_level = "WARN"
58
59 env_var_verbosity = rctx.os.environ.get(REPO_VERBOSITY_ENV_VAR)
60 verbosity_level = env_var_verbosity or verbosity_level
61
62 verbosity = {
63 "DEBUG": 2,
64 "INFO": 1,
65 "TRACE": 3,
66 }.get(verbosity_level, 0)
67
68 def _log(enabled_on_verbosity, level, message_cb):
69 if verbosity < enabled_on_verbosity:
70 return
71
72 print("\nrules_python: {}: ".format(level.upper()), message_cb()) # buildifier: disable=print
73
74 return struct(
75 trace = lambda message_cb: _log(3, "TRACE", message_cb),
76 debug = lambda message_cb: _log(2, "DEBUG", message_cb),
77 info = lambda message_cb: _log(1, "INFO", message_cb),
78 warn = lambda message_cb: _log(0, "WARNING", message_cb),
79 )
80
Richard Levasseure53b0b72024-01-31 08:53:05 -080081def _execute_internal(
82 rctx,
83 *,
84 op,
85 fail_on_error = False,
86 arguments,
87 environment = {},
88 **kwargs):
Richard Levasseurac3abf62024-06-17 16:28:33 -070089 """Execute a subprocess with debugging instrumentation.
Richard Levasseure53b0b72024-01-31 08:53:05 -080090
91 Args:
92 rctx: repository_ctx object
93 op: string, brief description of the operation this command
94 represents. Used to succintly describe it in logging and
95 error messages.
96 fail_on_error: bool, True if fail() should be called if the command
97 fails (non-zero exit code), False if not.
98 arguments: list of arguments; see rctx.execute#arguments.
99 environment: optional dict of the environment to run the command
100 in; see rctx.execute#environment.
101 **kwargs: additional kwargs to pass onto rctx.execute
102
103 Returns:
104 exec_result object, see repository_ctx.execute return type.
105 """
106 _debug_print(rctx, lambda: (
107 "repo.execute: {op}: start\n" +
108 " command: {cmd}\n" +
109 " working dir: {cwd}\n" +
110 " timeout: {timeout}\n" +
111 " environment:{env_str}\n"
112 ).format(
113 op = op,
114 cmd = _args_to_str(arguments),
115 cwd = _cwd_to_str(rctx, kwargs),
116 timeout = _timeout_to_str(kwargs),
117 env_str = _env_to_str(environment),
118 ))
119
120 result = rctx.execute(arguments, environment = environment, **kwargs)
121
122 if fail_on_error and result.return_code != 0:
123 fail((
124 "repo.execute: {op}: end: failure:\n" +
125 " command: {cmd}\n" +
126 " return code: {return_code}\n" +
127 " working dir: {cwd}\n" +
128 " timeout: {timeout}\n" +
129 " environment:{env_str}\n" +
130 "{output}"
131 ).format(
132 op = op,
133 cmd = _args_to_str(arguments),
134 return_code = result.return_code,
135 cwd = _cwd_to_str(rctx, kwargs),
136 timeout = _timeout_to_str(kwargs),
137 env_str = _env_to_str(environment),
138 output = _outputs_to_str(result),
139 ))
140 elif _is_repo_debug_enabled(rctx):
141 # buildifier: disable=print
142 print((
143 "repo.execute: {op}: end: {status}\n" +
144 " return code: {return_code}\n" +
145 "{output}"
146 ).format(
147 op = op,
148 status = "success" if result.return_code == 0 else "failure",
149 return_code = result.return_code,
150 output = _outputs_to_str(result),
151 ))
152
153 return result
154
155def _execute_unchecked(*args, **kwargs):
156 """Execute a subprocess.
157
158 Additional information will be printed if debug output is enabled.
159
160 Args:
161 *args: see _execute_internal
162 **kwargs: see _execute_internal
163
164 Returns:
165 exec_result object, see repository_ctx.execute return type.
166 """
167 return _execute_internal(fail_on_error = False, *args, **kwargs)
168
169def _execute_checked(*args, **kwargs):
170 """Execute a subprocess, failing for a non-zero exit code.
171
172 If the command fails, then fail() is called with detailed information
173 about the command and its failure.
174
175 Args:
176 *args: see _execute_internal
177 **kwargs: see _execute_internal
178
179 Returns:
180 exec_result object, see repository_ctx.execute return type.
181 """
182 return _execute_internal(fail_on_error = True, *args, **kwargs)
183
184def _execute_checked_stdout(*args, **kwargs):
185 """Calls execute_checked, but only returns the stdout value."""
186 return _execute_checked(*args, **kwargs).stdout
187
188def _which_checked(rctx, binary_name):
189 """Tests to see if a binary exists, and otherwise fails with a message.
190
191 Args:
192 binary_name: name of the binary to find.
193 rctx: repository context.
194
195 Returns:
196 rctx.Path for the binary.
197 """
198 binary = rctx.which(binary_name)
199 if binary == None:
Jesse Schalken627830e2024-02-27 20:01:05 +1100200 fail((
201 "Unable to find the binary '{binary_name}' on PATH.\n" +
202 " PATH = {path}"
203 ).format(
204 binary_name = binary_name,
205 path = rctx.os.environ.get("PATH"),
206 ))
Richard Levasseure53b0b72024-01-31 08:53:05 -0800207 return binary
208
209def _args_to_str(arguments):
210 return " ".join([_arg_repr(a) for a in arguments])
211
212def _arg_repr(value):
213 if _arg_should_be_quoted(value):
214 return repr(value)
215 else:
216 return str(value)
217
218_SPECIAL_SHELL_CHARS = [" ", "'", '"', "{", "$", "("]
219
220def _arg_should_be_quoted(value):
221 # `value` may be non-str, such as ctx.path objects
222 value_str = str(value)
223 for char in _SPECIAL_SHELL_CHARS:
224 if char in value_str:
225 return True
226 return False
227
228def _cwd_to_str(rctx, kwargs):
229 cwd = kwargs.get("working_directory")
230 if not cwd:
231 cwd = "<default: {}>".format(rctx.path(""))
232 return cwd
233
234def _env_to_str(environment):
235 if not environment:
236 env_str = " <default environment>"
237 else:
238 env_str = "\n".join(["{}={}".format(k, repr(v)) for k, v in environment.items()])
239 env_str = "\n" + env_str
240 return env_str
241
242def _timeout_to_str(kwargs):
243 return kwargs.get("timeout", "<default timeout>")
244
245def _outputs_to_str(result):
246 lines = []
247 items = [
248 ("stdout", result.stdout),
249 ("stderr", result.stderr),
250 ]
251 for name, content in items:
252 if content:
253 lines.append("===== {} start =====".format(name))
254
255 # Prevent adding an extra new line, which makes the output look odd.
256 if content.endswith("\n"):
257 lines.append(content[:-1])
258 else:
259 lines.append(content)
260 lines.append("===== {} end =====".format(name))
261 else:
262 lines.append("<{} empty>".format(name))
263 return "\n".join(lines)
264
265repo_utils = struct(
266 execute_checked = _execute_checked,
267 execute_unchecked = _execute_unchecked,
Ignas Anikevicius74d576f2024-02-08 09:37:52 +0900268 execute_checked_stdout = _execute_checked_stdout,
Richard Levasseure53b0b72024-01-31 08:53:05 -0800269 is_repo_debug_enabled = _is_repo_debug_enabled,
270 debug_print = _debug_print,
271 which_checked = _which_checked,
Ignas Anikeviciusb4b52fc2024-06-01 13:36:48 +0900272 logger = _logger,
Richard Levasseure53b0b72024-01-31 08:53:05 -0800273)