xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/repo_utils.bzl (revision 60517a1edbc8ecf509223e9af94a7adec7d736b8)
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