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 17This code should only be loaded and used during the repository phase. 18""" 19 20REPO_DEBUG_ENV_VAR = "RULES_PYTHON_REPO_DEBUG" 21REPO_VERBOSITY_ENV_VAR = "RULES_PYTHON_REPO_DEBUG_VERBOSITY" 22 23def _is_repo_debug_enabled(mrctx): 24 """Tells if debbugging output is requested during repo operatiosn. 25 26 Args: 27 mrctx: repository_ctx or module_ctx object 28 29 Returns: 30 True if enabled, False if not. 31 """ 32 return _getenv(mrctx, REPO_DEBUG_ENV_VAR) == "1" 33 34def _logger(mrctx, name = None): 35 """Creates a logger instance for printing messages. 36 37 Args: 38 mrctx: repository_ctx or module_ctx object. If the attribute 39 `_rule_name` is present, it will be included in log messages. 40 name: name for the logger. Optional for repository_ctx usage. 41 42 Returns: 43 A struct with attributes logging: trace, debug, info, warn, fail. 44 """ 45 if _is_repo_debug_enabled(mrctx): 46 verbosity_level = "DEBUG" 47 else: 48 verbosity_level = "WARN" 49 50 env_var_verbosity = _getenv(mrctx, REPO_VERBOSITY_ENV_VAR) 51 verbosity_level = env_var_verbosity or verbosity_level 52 53 verbosity = { 54 "DEBUG": 2, 55 "INFO": 1, 56 "TRACE": 3, 57 }.get(verbosity_level, 0) 58 59 if hasattr(mrctx, "attr"): 60 rctx = mrctx # This is `repository_ctx`. 61 name = name or "{}(@@{})".format(getattr(rctx.attr, "_rule_name", "?"), rctx.name) 62 elif not name: 63 fail("The name has to be specified when using the logger with `module_ctx`") 64 65 def _log(enabled_on_verbosity, level, message_cb_or_str, printer = print): 66 if verbosity < enabled_on_verbosity: 67 return 68 69 if type(message_cb_or_str) == "string": 70 message = message_cb_or_str 71 else: 72 message = message_cb_or_str() 73 74 # NOTE: printer may be the `fail` function. 75 printer("\nrules_python:{} {}:".format( 76 name, 77 level.upper(), 78 ), message) # buildifier: disable=print 79 80 return struct( 81 trace = lambda message_cb: _log(3, "TRACE", message_cb), 82 debug = lambda message_cb: _log(2, "DEBUG", message_cb), 83 info = lambda message_cb: _log(1, "INFO", message_cb), 84 warn = lambda message_cb: _log(0, "WARNING", message_cb), 85 fail = lambda message_cb: _log(-1, "FAIL", message_cb, fail), 86 ) 87 88def _execute_internal( 89 mrctx, 90 *, 91 op, 92 fail_on_error = False, 93 arguments, 94 environment = {}, 95 logger = None, 96 **kwargs): 97 """Execute a subprocess with debugging instrumentation. 98 99 Args: 100 mrctx: module_ctx or repository_ctx object 101 op: string, brief description of the operation this command 102 represents. Used to succintly describe it in logging and 103 error messages. 104 fail_on_error: bool, True if fail() should be called if the command 105 fails (non-zero exit code), False if not. 106 arguments: list of arguments; see module_ctx.execute#arguments or 107 repository_ctx#arguments. 108 environment: optional dict of the environment to run the command 109 in; see module_ctx.execute#environment or 110 repository_ctx.execute#environment. 111 logger: optional `Logger` to use for logging execution details. Must be 112 specified when using module_ctx. If not specified, a default will 113 be created. 114 **kwargs: additional kwargs to pass onto rctx.execute 115 116 Returns: 117 exec_result object, see repository_ctx.execute return type. 118 """ 119 if not logger and hasattr(mrctx, "attr"): 120 rctx = mrctx 121 logger = _logger(rctx) 122 elif not logger: 123 fail("logger must be specified when using 'module_ctx'") 124 125 logger.debug(lambda: ( 126 "repo.execute: {op}: start\n" + 127 " command: {cmd}\n" + 128 " working dir: {cwd}\n" + 129 " timeout: {timeout}\n" + 130 " environment:{env_str}\n" 131 ).format( 132 op = op, 133 cmd = _args_to_str(arguments), 134 cwd = _cwd_to_str(mrctx, kwargs), 135 timeout = _timeout_to_str(kwargs), 136 env_str = _env_to_str(environment), 137 )) 138 139 mrctx.report_progress("Running {}".format(op)) 140 result = mrctx.execute(arguments, environment = environment, **kwargs) 141 142 if fail_on_error and result.return_code != 0: 143 logger.fail(( 144 "repo.execute: {op}: end: failure:\n" + 145 " command: {cmd}\n" + 146 " return code: {return_code}\n" + 147 " working dir: {cwd}\n" + 148 " timeout: {timeout}\n" + 149 " environment:{env_str}\n" + 150 "{output}" 151 ).format( 152 op = op, 153 cmd = _args_to_str(arguments), 154 return_code = result.return_code, 155 cwd = _cwd_to_str(mrctx, kwargs), 156 timeout = _timeout_to_str(kwargs), 157 env_str = _env_to_str(environment), 158 output = _outputs_to_str(result), 159 )) 160 elif _is_repo_debug_enabled(mrctx): 161 logger.debug(( 162 "repo.execute: {op}: end: {status}\n" + 163 " return code: {return_code}\n" + 164 "{output}" 165 ).format( 166 op = op, 167 status = "success" if result.return_code == 0 else "failure", 168 return_code = result.return_code, 169 output = _outputs_to_str(result), 170 )) 171 172 result_kwargs = {k: getattr(result, k) for k in dir(result)} 173 return struct( 174 describe_failure = lambda: _execute_describe_failure( 175 op = op, 176 arguments = arguments, 177 result = result, 178 mrctx = mrctx, 179 kwargs = kwargs, 180 environment = environment, 181 ), 182 **result_kwargs 183 ) 184 185def _execute_unchecked(*args, **kwargs): 186 """Execute a subprocess. 187 188 Additional information will be printed if debug output is enabled. 189 190 Args: 191 *args: see _execute_internal 192 **kwargs: see _execute_internal 193 194 Returns: 195 exec_result object, see repository_ctx.execute return type. 196 """ 197 return _execute_internal(fail_on_error = False, *args, **kwargs) 198 199def _execute_checked(*args, **kwargs): 200 """Execute a subprocess, failing for a non-zero exit code. 201 202 If the command fails, then fail() is called with detailed information 203 about the command and its failure. 204 205 Args: 206 *args: see _execute_internal 207 **kwargs: see _execute_internal 208 209 Returns: 210 exec_result object, see repository_ctx.execute return type. 211 """ 212 return _execute_internal(fail_on_error = True, *args, **kwargs) 213 214def _execute_checked_stdout(*args, **kwargs): 215 """Calls execute_checked, but only returns the stdout value.""" 216 return _execute_checked(*args, **kwargs).stdout 217 218def _execute_describe_failure(*, op, arguments, result, mrctx, kwargs, environment): 219 return ( 220 "repo.execute: {op}: failure:\n" + 221 " command: {cmd}\n" + 222 " return code: {return_code}\n" + 223 " working dir: {cwd}\n" + 224 " timeout: {timeout}\n" + 225 " environment:{env_str}\n" + 226 "{output}" 227 ).format( 228 op = op, 229 cmd = _args_to_str(arguments), 230 return_code = result.return_code, 231 cwd = _cwd_to_str(mrctx, kwargs), 232 timeout = _timeout_to_str(kwargs), 233 env_str = _env_to_str(environment), 234 output = _outputs_to_str(result), 235 ) 236 237def _which_checked(mrctx, binary_name): 238 """Tests to see if a binary exists, and otherwise fails with a message. 239 240 Args: 241 binary_name: name of the binary to find. 242 mrctx: module_ctx or repository_ctx. 243 244 Returns: 245 mrctx.Path for the binary. 246 """ 247 result = _which_unchecked(mrctx, binary_name) 248 if result.binary == None: 249 fail(result.describe_failure()) 250 return result.binary 251 252def _which_unchecked(mrctx, binary_name): 253 """Tests to see if a binary exists. 254 255 This is also watch the `PATH` environment variable. 256 257 Args: 258 binary_name: name of the binary to find. 259 mrctx: repository context. 260 261 Returns: 262 `struct` with attributes: 263 * `binary`: `repository_ctx.Path` 264 * `describe_failure`: `Callable | None`; takes no args. If the 265 binary couldn't be found, provides a detailed error description. 266 """ 267 path = _getenv(mrctx, "PATH", "") 268 binary = mrctx.which(binary_name) 269 if binary: 270 _watch(mrctx, binary) 271 describe_failure = None 272 else: 273 describe_failure = lambda: _which_describe_failure(binary_name, path) 274 275 return struct( 276 binary = binary, 277 describe_failure = describe_failure, 278 ) 279 280def _which_describe_failure(binary_name, path): 281 return ( 282 "Unable to find the binary '{binary_name}' on PATH.\n" + 283 " PATH = {path}" 284 ).format( 285 binary_name = binary_name, 286 path = path, 287 ) 288 289def _getenv(mrctx, name, default = None): 290 # Bazel 7+ API has (repository|module)_ctx.getenv 291 return getattr(mrctx, "getenv", mrctx.os.environ.get)(name, default) 292 293def _args_to_str(arguments): 294 return " ".join([_arg_repr(a) for a in arguments]) 295 296def _arg_repr(value): 297 if _arg_should_be_quoted(value): 298 return repr(value) 299 else: 300 return str(value) 301 302_SPECIAL_SHELL_CHARS = [" ", "'", '"', "{", "$", "("] 303 304def _arg_should_be_quoted(value): 305 # `value` may be non-str, such as mrctx.path objects 306 value_str = str(value) 307 for char in _SPECIAL_SHELL_CHARS: 308 if char in value_str: 309 return True 310 return False 311 312def _cwd_to_str(mrctx, kwargs): 313 cwd = kwargs.get("working_directory") 314 if not cwd: 315 cwd = "<default: {}>".format(mrctx.path("")) 316 return cwd 317 318def _env_to_str(environment): 319 if not environment: 320 env_str = " <default environment>" 321 else: 322 env_str = "\n".join(["{}={}".format(k, repr(v)) for k, v in environment.items()]) 323 env_str = "\n" + env_str 324 return env_str 325 326def _timeout_to_str(kwargs): 327 return kwargs.get("timeout", "<default timeout>") 328 329def _outputs_to_str(result): 330 lines = [] 331 items = [ 332 ("stdout", result.stdout), 333 ("stderr", result.stderr), 334 ] 335 for name, content in items: 336 if content: 337 lines.append("===== {} start =====".format(name)) 338 339 # Prevent adding an extra new line, which makes the output look odd. 340 if content.endswith("\n"): 341 lines.append(content[:-1]) 342 else: 343 lines.append(content) 344 lines.append("===== {} end =====".format(name)) 345 else: 346 lines.append("<{} empty>".format(name)) 347 return "\n".join(lines) 348 349# This includes the vendored _translate_cpu and _translate_os from 350# @platforms//host:extension.bzl at version 0.0.9 so that we don't 351# force the users to depend on it. 352 353def _get_platforms_os_name(mrctx): 354 """Return the name in @platforms//os for the host os. 355 356 Args: 357 mrctx: module_ctx or repository_ctx. 358 359 Returns: 360 `str`. The target name. 361 """ 362 os = mrctx.os.name.lower() 363 364 if os.startswith("mac os"): 365 return "osx" 366 if os.startswith("freebsd"): 367 return "freebsd" 368 if os.startswith("openbsd"): 369 return "openbsd" 370 if os.startswith("linux"): 371 return "linux" 372 if os.startswith("windows"): 373 return "windows" 374 return os 375 376def _get_platforms_cpu_name(mrctx): 377 """Return the name in @platforms//cpu for the host arch. 378 379 Args: 380 mrctx: module_ctx or repository_ctx. 381 382 Returns: 383 `str`. The target name. 384 """ 385 arch = mrctx.os.arch.lower() 386 if arch in ["i386", "i486", "i586", "i686", "i786", "x86"]: 387 return "x86_32" 388 if arch in ["amd64", "x86_64", "x64"]: 389 return "x86_64" 390 if arch in ["ppc", "ppc64", "ppc64le"]: 391 return "ppc" 392 if arch in ["arm", "armv7l"]: 393 return "arm" 394 if arch in ["aarch64"]: 395 return "aarch64" 396 if arch in ["s390x", "s390"]: 397 return "s390x" 398 if arch in ["mips64el", "mips64"]: 399 return "mips64" 400 if arch in ["riscv64"]: 401 return "riscv64" 402 return arch 403 404# TODO: Remove after Bazel 6 support dropped 405def _watch(mrctx, *args, **kwargs): 406 """Calls mrctx.watch, if available.""" 407 if not args and not kwargs: 408 fail("'watch' needs at least a single argument.") 409 410 if hasattr(mrctx, "watch"): 411 mrctx.watch(*args, **kwargs) 412 413# TODO: Remove after Bazel 6 support dropped 414def _watch_tree(mrctx, *args, **kwargs): 415 """Calls mrctx.watch_tree, if available.""" 416 if not args and not kwargs: 417 fail("'watch_tree' needs at least a single argument.") 418 419 if hasattr(mrctx, "watch_tree"): 420 mrctx.watch_tree(*args, **kwargs) 421 422repo_utils = struct( 423 # keep sorted 424 execute_checked = _execute_checked, 425 execute_checked_stdout = _execute_checked_stdout, 426 execute_unchecked = _execute_unchecked, 427 get_platforms_cpu_name = _get_platforms_cpu_name, 428 get_platforms_os_name = _get_platforms_os_name, 429 getenv = _getenv, 430 is_repo_debug_enabled = _is_repo_debug_enabled, 431 logger = _logger, 432 watch = _watch, 433 watch_tree = _watch_tree, 434 which_checked = _which_checked, 435 which_unchecked = _which_unchecked, 436) 437