Richard Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 1 | # 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 | |
| 17 | This code should only be loaded and used during the repository phase. |
| 18 | """ |
| 19 | |
| 20 | REPO_DEBUG_ENV_VAR = "RULES_PYTHON_REPO_DEBUG" |
Ignas Anikevicius | b4b52fc | 2024-06-01 13:36:48 +0900 | [diff] [blame] | 21 | REPO_VERBOSITY_ENV_VAR = "RULES_PYTHON_REPO_DEBUG_VERBOSITY" |
Richard Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 22 | |
| 23 | def _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 | |
| 34 | def _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 Anikevicius | b4b52fc | 2024-06-01 13:36:48 +0900 | [diff] [blame] | 45 | def _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 Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 81 | def _execute_internal( |
| 82 | rctx, |
| 83 | *, |
| 84 | op, |
| 85 | fail_on_error = False, |
| 86 | arguments, |
| 87 | environment = {}, |
| 88 | **kwargs): |
Richard Levasseur | ac3abf6 | 2024-06-17 16:28:33 -0700 | [diff] [blame] | 89 | """Execute a subprocess with debugging instrumentation. |
Richard Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 90 | |
| 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 | |
| 155 | def _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 | |
| 169 | def _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 | |
| 184 | def _execute_checked_stdout(*args, **kwargs): |
| 185 | """Calls execute_checked, but only returns the stdout value.""" |
| 186 | return _execute_checked(*args, **kwargs).stdout |
| 187 | |
| 188 | def _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 Schalken | 627830e | 2024-02-27 20:01:05 +1100 | [diff] [blame] | 200 | 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 Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 207 | return binary |
| 208 | |
| 209 | def _args_to_str(arguments): |
| 210 | return " ".join([_arg_repr(a) for a in arguments]) |
| 211 | |
| 212 | def _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 | |
| 220 | def _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 | |
| 228 | def _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 | |
| 234 | def _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 | |
| 242 | def _timeout_to_str(kwargs): |
| 243 | return kwargs.get("timeout", "<default timeout>") |
| 244 | |
| 245 | def _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 | |
| 265 | repo_utils = struct( |
| 266 | execute_checked = _execute_checked, |
| 267 | execute_unchecked = _execute_unchecked, |
Ignas Anikevicius | 74d576f | 2024-02-08 09:37:52 +0900 | [diff] [blame] | 268 | execute_checked_stdout = _execute_checked_stdout, |
Richard Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 269 | is_repo_debug_enabled = _is_repo_debug_enabled, |
| 270 | debug_print = _debug_print, |
| 271 | which_checked = _which_checked, |
Ignas Anikevicius | b4b52fc | 2024-06-01 13:36:48 +0900 | [diff] [blame] | 272 | logger = _logger, |
Richard Levasseur | e53b0b7 | 2024-01-31 08:53:05 -0800 | [diff] [blame] | 273 | ) |