xref: /aosp_15_r20/external/bazelbuild-rules_python/python/private/pypi/pypi_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""
16
17load("@bazel_skylib//lib:types.bzl", "types")
18load("//python/private:repo_utils.bzl", "repo_utils")
19
20def _get_python_interpreter_attr(mrctx, *, python_interpreter = None):
21    """A helper function for getting the `python_interpreter` attribute or it's default
22
23    Args:
24        mrctx (module_ctx or repository_ctx): Handle to the rule repository context.
25        python_interpreter (str): The python interpreter override.
26
27    Returns:
28        str: The attribute value or it's default
29    """
30    if python_interpreter:
31        return python_interpreter
32
33    os = repo_utils.get_platforms_os_name(mrctx)
34    if "windows" in os:
35        return "python.exe"
36    else:
37        return "python3"
38
39def _resolve_python_interpreter(mrctx, *, python_interpreter = None, python_interpreter_target = None):
40    """Helper function to find the python interpreter from the common attributes
41
42    Args:
43        mrctx: Handle to the module_ctx or repository_ctx.
44        python_interpreter: str, the python interpreter to use.
45        python_interpreter_target: Label, the python interpreter to use after
46            downloading the label.
47
48    Returns:
49        `path` object, for the resolved path to the Python interpreter.
50    """
51    python_interpreter = _get_python_interpreter_attr(mrctx, python_interpreter = python_interpreter)
52
53    if python_interpreter_target != None:
54        # The following line would make the MODULE.bazel.lock platform
55        # independent, because the lock file will then contain a hash of the
56        # file so that the lock file can be recalculated, hence the best way is
57        # to add this directory to PATH.
58        #
59        # hence we add the root BUILD.bazel file and get the directory of that
60        # and construct the path differently. At the end of the day we don't
61        # want the hash of the interpreter to end up in the lock file.
62        if hasattr(python_interpreter_target, "same_package_label"):
63            root_build_bazel = python_interpreter_target.same_package_label("BUILD.bazel")
64        else:
65            root_build_bazel = python_interpreter_target.relative(":BUILD.bazel")
66
67        python_interpreter = mrctx.path(root_build_bazel).dirname.get_child(python_interpreter_target.name)
68
69        os = repo_utils.get_platforms_os_name(mrctx)
70
71        # On Windows, the symlink doesn't work because Windows attempts to find
72        # Python DLLs where the symlink is, not where the symlink points.
73        if "windows" in os:
74            python_interpreter = python_interpreter.realpath
75    elif "/" not in python_interpreter:
76        # It's a plain command, e.g. "python3", to look up in the environment.
77        python_interpreter = repo_utils.which_checked(mrctx, python_interpreter)
78    else:
79        python_interpreter = mrctx.path(python_interpreter)
80    return python_interpreter
81
82def _construct_pypath(mrctx, *, entries):
83    """Helper function to construct a PYTHONPATH.
84
85    Contains entries for code in this repo as well as packages downloaded from //python/pip_install:repositories.bzl.
86    This allows us to run python code inside repository rule implementations.
87
88    Args:
89        mrctx: Handle to the module_ctx or repository_ctx.
90        entries: The list of entries to add to PYTHONPATH.
91
92    Returns: String of the PYTHONPATH.
93    """
94
95    if not entries:
96        return None
97
98    os = repo_utils.get_platforms_os_name(mrctx)
99    separator = ";" if "windows" in os else ":"
100    pypath = separator.join([
101        str(mrctx.path(entry).dirname)
102        # Use a dict as a way to remove duplicates and then sort it.
103        for entry in sorted({x: None for x in entries})
104    ])
105    return pypath
106
107def _execute_checked(mrctx, *, srcs, **kwargs):
108    """Helper function to run a python script and modify the PYTHONPATH to include external deps.
109
110    Args:
111        mrctx: Handle to the module_ctx or repository_ctx.
112        srcs: The src files that the script depends on. This is important to
113            ensure that the Bazel repository cache or the bzlmod lock file gets
114            invalidated when any one file changes. It is advisable to use
115            `RECORD` files for external deps and the list of srcs from the
116            rules_python repo for any scripts.
117        **kwargs: Arguments forwarded to `repo_utils.execute_checked`. If
118            the `environment` has a value `PYTHONPATH` and it is a list, then
119            it will be passed to `construct_pythonpath` function.
120    """
121
122    for src in srcs:
123        # This will ensure that we will re-evaluate the bzlmod extension or
124        # refetch the repository_rule when the srcs change. This should work on
125        # Bazel versions without `mrctx.watch` as well.
126        repo_utils.watch(mrctx, mrctx.path(src))
127
128    env = kwargs.pop("environment", {})
129    pythonpath = env.get("PYTHONPATH", "")
130    if pythonpath and not types.is_string(pythonpath):
131        env["PYTHONPATH"] = _construct_pypath(mrctx, entries = pythonpath)
132
133    return repo_utils.execute_checked(
134        mrctx,
135        environment = env,
136        **kwargs
137    )
138
139pypi_repo_utils = struct(
140    construct_pythonpath = _construct_pypath,
141    execute_checked = _execute_checked,
142    resolve_python_interpreter = _resolve_python_interpreter,
143)
144