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