| # Copyright 2018 The Bazel Authors. All rights reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| """Runfiles lookup library for Bazel-built Python binaries and tests. | |
| USAGE: | |
| 1. Depend on this runfiles library from your build rule: | |
| py_binary( | |
| name = "my_binary", | |
| ... | |
| deps = ["@rules_python//python/runfiles"], | |
| ) | |
| 2. Import the runfiles library. | |
| from rules_python.python.runfiles import runfiles | |
| 3. Create a Runfiles object and use rlocation to look up runfile paths: | |
| r = runfiles.Create() | |
| ... | |
| with open(r.Rlocation("my_workspace/path/to/my/data.txt"), "r") as f: | |
| contents = f.readlines() | |
| ... | |
| The code above creates a manifest- or directory-based implementations based | |
| on the environment variables in os.environ. See `Create()` for more info. | |
| If you want to explicitly create a manifest- or directory-based | |
| implementations, you can do so as follows: | |
| r1 = runfiles.CreateManifestBased("path/to/foo.runfiles_manifest") | |
| r2 = runfiles.CreateDirectoryBased("path/to/foo.runfiles/") | |
| If you want to start subprocesses that also need runfiles, you need to set | |
| the right environment variables for them: | |
| import subprocess | |
| from rules_python.python.runfiles import runfiles | |
| r = runfiles.Create() | |
| env = {} | |
| ... | |
| env.update(r.EnvVars()) | |
| p = subprocess.Popen([r.Rlocation("path/to/binary")], env, ...) | |
| """ | |
| import inspect | |
| import os | |
| import posixpath | |
| import sys | |
| from ._runfiles_constants import MAIN_REPOSITORY_RUNFILES_DIRECTORY | |
| def CreateManifestBased(manifest_path): | |
| return _Runfiles(_ManifestBased(manifest_path)) | |
| def CreateDirectoryBased(runfiles_dir_path): | |
| return _Runfiles(_DirectoryBased(runfiles_dir_path)) | |
| def Create(env=None): | |
| """Returns a new `Runfiles` instance. | |
| The returned object is either: | |
| - manifest-based, meaning it looks up runfile paths from a manifest file, or | |
| - directory-based, meaning it looks up runfile paths under a given directory | |
| path | |
| If `env` contains "RUNFILES_MANIFEST_FILE" with non-empty value, this method | |
| returns a manifest-based implementation. The object eagerly reads and caches | |
| the whole manifest file upon instantiation; this may be relevant for | |
| performance consideration. | |
| Otherwise, if `env` contains "RUNFILES_DIR" with non-empty value (checked in | |
| this priority order), this method returns a directory-based implementation. | |
| If neither cases apply, this method returns null. | |
| Args: | |
| env: {string: string}; optional; the map of environment variables. If None, | |
| this function uses the environment variable map of this process. | |
| Raises: | |
| IOError: if some IO error occurs. | |
| """ | |
| env_map = os.environ if env is None else env | |
| manifest = env_map.get("RUNFILES_MANIFEST_FILE") | |
| if manifest: | |
| return CreateManifestBased(manifest) | |
| directory = env_map.get("RUNFILES_DIR") | |
| if not directory: | |
| directory = env_map.get("PYTHON_RUNFILES") | |
| if directory: | |
| return CreateDirectoryBased(directory) | |
| return None | |
| class _Runfiles(object): | |
| """Returns the runtime location of runfiles. | |
| Runfiles are data-dependencies of Bazel-built binaries and tests. | |
| """ | |
| def __init__(self, strategy): | |
| self._strategy = strategy | |
| self._python_runfiles_root = _FindPythonRunfilesRoot() | |
| def Rlocation(self, path): | |
| """Returns the runtime path of a runfile. | |
| Runfiles are data-dependencies of Bazel-built binaries and tests. | |
| The returned path may not be valid. The caller should check the path's | |
| validity and that the path exists. | |
| The function may return None. In that case the caller can be sure that the | |
| rule does not know about this data-dependency. | |
| Args: | |
| path: string; runfiles-root-relative path of the runfile | |
| Returns: | |
| the path to the runfile, which the caller should check for existence, or | |
| None if the method doesn't know about this runfile | |
| Raises: | |
| TypeError: if `path` is not a string | |
| ValueError: if `path` is None or empty, or it's absolute or not normalized | |
| """ | |
| if not path: | |
| raise ValueError() | |
| if not isinstance(path, str): | |
| raise TypeError() | |
| if (path.startswith("../") or "/.." in path or path.startswith("./") or | |
| "/./" in path or path.endswith("/.") or "//" in path): | |
| raise ValueError("path is not normalized: \"%s\"" % path) | |
| if path[0] == "\\": | |
| raise ValueError("path is absolute without a drive letter: \"%s\"" % path) | |
| if os.path.isabs(path): | |
| return path | |
| return self._strategy.RlocationChecked(path) | |
| def EnvVars(self): | |
| """Returns environment variables for subprocesses. | |
| The caller should set the returned key-value pairs in the environment of | |
| subprocesses in case those subprocesses are also Bazel-built binaries that | |
| need to use runfiles. | |
| Returns: | |
| {string: string}; a dict; keys are environment variable names, values are | |
| the values for these environment variables | |
| """ | |
| return self._strategy.EnvVars() | |
| def CurrentRepository(self, frame=1): | |
| """Returns the canonical name of the caller's Bazel repository. | |
| For example, this function returns '' (the empty string) when called from | |
| the main repository and a string of the form 'rules_python+0.13.0` when | |
| called from code in the repository corresponding to the rules_python Bazel | |
| module. | |
| More information about the difference between canonical repository names and | |
| the `@repo` part of labels is available at: | |
| https://bazel.build/build/bzlmod#repository-names | |
| NOTE: This function inspects the callstack to determine where in the | |
| runfiles the caller is located to determine which repository it came from. | |
| This may fail or produce incorrect results depending on who the caller is, | |
| for example if it is not represented by a Python source file. Use the | |
| `frame` argument to control the stack lookup. | |
| Args: | |
| frame: int; the stack frame to return the repository name for. Defaults to | |
| 1, the caller of the CurrentRepository function. | |
| Returns: | |
| The canonical name of the Bazel repository containing the file containing | |
| the frame-th caller of this function | |
| Raises: | |
| ValueError: if the caller cannot be determined or the caller's file path | |
| is not contained in the Python runfiles tree | |
| """ | |
| # pylint:disable=protected-access # for sys._getframe | |
| # pylint:disable=raise-missing-from # we're still supporting Python 2... | |
| try: | |
| caller_path = inspect.getfile(sys._getframe(frame)) | |
| except (TypeError, ValueError): | |
| raise ValueError("failed to determine caller's file path") | |
| caller_runfiles_path = os.path.relpath(caller_path, | |
| self._python_runfiles_root) | |
| if caller_runfiles_path.startswith(".." + os.path.sep): | |
| raise ValueError("{} does not lie under the runfiles root {}".format( | |
| caller_path, self._python_runfiles_root)) | |
| caller_runfiles_directory = caller_runfiles_path[:caller_runfiles_path | |
| .find(os.path.sep)] | |
| if caller_runfiles_directory == MAIN_REPOSITORY_RUNFILES_DIRECTORY: | |
| # The canonical name of the main repository (also known as the workspace) | |
| # is the empty string. | |
| return "" | |
| # For all other repositories, the name of the runfiles directory is the | |
| # canonical name. | |
| return caller_runfiles_directory | |
| def _FindPythonRunfilesRoot(): | |
| """Finds the root of the Python runfiles tree.""" | |
| root = __file__ | |
| # Walk up our own runfiles path to the root of the runfiles tree from which | |
| # the current file is being run. This path coincides with what the Bazel | |
| # Python stub sets up as sys.path[0]. Since that entry can be changed at | |
| # runtime, we rederive it here. | |
| for _ in range( | |
| "bazel_tools/tools/python/runfiles/runfiles.py".count("/") + | |
| 1): | |
| root = os.path.dirname(root) | |
| return root | |
| class _ManifestBased(object): | |
| """`Runfiles` strategy that parses a runfiles-manifest to look up runfiles.""" | |
| def __init__(self, path): | |
| if not path: | |
| raise ValueError() | |
| if not isinstance(path, str): | |
| raise TypeError() | |
| self._path = path | |
| self._runfiles = _ManifestBased._LoadRunfiles(path) | |
| def RlocationChecked(self, path): | |
| """Returns the runtime path of a runfile.""" | |
| exact_match = self._runfiles.get(path) | |
| if exact_match: | |
| return exact_match | |
| # If path references a runfile that lies under a directory that itself is a | |
| # runfile, then only the directory is listed in the manifest. Look up all | |
| # prefixes of path in the manifest and append the relative path from the | |
| # prefix to the looked up path. | |
| prefix_end = len(path) | |
| while True: | |
| prefix_end = path.rfind("/", 0, prefix_end - 1) | |
| if prefix_end == -1: | |
| return None | |
| prefix_match = self._runfiles.get(path[0:prefix_end]) | |
| if prefix_match: | |
| return prefix_match + "/" + path[prefix_end + 1:] | |
| @staticmethod | |
| def _LoadRunfiles(path): | |
| """Loads the runfiles manifest.""" | |
| result = {} | |
| with open(path, "r") as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| tokens = line.split(" ", 1) | |
| if len(tokens) == 1: | |
| result[line] = line | |
| else: | |
| result[tokens[0]] = tokens[1] | |
| return result | |
| def _GetRunfilesDir(self): | |
| if self._path.endswith("/MANIFEST") or self._path.endswith("\\MANIFEST"): | |
| return self._path[:-len("/MANIFEST")] | |
| elif self._path.endswith(".runfiles_manifest"): | |
| return self._path[:-len("_manifest")] | |
| else: | |
| return "" | |
| def EnvVars(self): | |
| directory = self._GetRunfilesDir() | |
| return { | |
| "RUNFILES_MANIFEST_FILE": self._path, | |
| "RUNFILES_DIR": directory, | |
| # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can | |
| # pick up RUNFILES_DIR. | |
| "JAVA_RUNFILES": directory, | |
| } | |
| class _DirectoryBased(object): | |
| """`Runfiles` strategy that appends runfiles paths to the runfiles root.""" | |
| def __init__(self, path): | |
| if not path: | |
| raise ValueError() | |
| if not isinstance(path, str): | |
| raise TypeError() | |
| self._runfiles_root = path | |
| def RlocationChecked(self, path): | |
| # Use posixpath instead of os.path, because Bazel only creates a runfiles | |
| # tree on Unix platforms, so `Create()` will only create a directory-based | |
| # runfiles strategy on those platforms. | |
| return posixpath.join(self._runfiles_root, path) | |
| def EnvVars(self): | |
| return { | |
| "RUNFILES_DIR": self._runfiles_root, | |
| # TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can | |
| # pick up RUNFILES_DIR. | |
| "JAVA_RUNFILES": self._runfiles_root, | |
| } | |
| def _PathsFrom(argv0, runfiles_mf, runfiles_dir, is_runfiles_manifest, | |
| is_runfiles_directory): | |
| """Discover runfiles manifest and runfiles directory paths. | |
| Args: | |
| argv0: string; the value of sys.argv[0] | |
| runfiles_mf: string; the value of the RUNFILES_MANIFEST_FILE environment | |
| variable | |
| runfiles_dir: string; the value of the RUNFILES_DIR environment variable | |
| is_runfiles_manifest: lambda(string):bool; returns true if the argument is | |
| the path of a runfiles manifest file | |
| is_runfiles_directory: lambda(string):bool; returns true if the argument is | |
| the path of a runfiles directory | |
| Returns: | |
| (string, string) pair, first element is the path to the runfiles manifest, | |
| second element is the path to the runfiles directory. If the first element | |
| is non-empty, then is_runfiles_manifest returns true for it. Same goes for | |
| the second element and is_runfiles_directory respectively. If both elements | |
| are empty, then this function could not find a manifest or directory for | |
| which is_runfiles_manifest or is_runfiles_directory returns true. | |
| """ | |
| mf_alid = is_runfiles_manifest(runfiles_mf) | |
| dir_valid = is_runfiles_directory(runfiles_dir) | |
| if not mf_alid and not dir_valid: | |
| runfiles_mf = argv0 + ".runfiles/MANIFEST" | |
| runfiles_dir = argv0 + ".runfiles" | |
| mf_alid = is_runfiles_manifest(runfiles_mf) | |
| dir_valid = is_runfiles_directory(runfiles_dir) | |
| if not mf_alid: | |
| runfiles_mf = argv0 + ".runfiles_manifest" | |
| mf_alid = is_runfiles_manifest(runfiles_mf) | |
| if not mf_alid and not dir_valid: | |
| return ("", "") | |
| if not mf_alid: | |
| runfiles_mf = runfiles_dir + "/MANIFEST" | |
| mf_alid = is_runfiles_manifest(runfiles_mf) | |
| if not mf_alid: | |
| runfiles_mf = runfiles_dir + "_manifest" | |
| mf_alid = is_runfiles_manifest(runfiles_mf) | |
| if not dir_valid: | |
| runfiles_dir = runfiles_mf[:-9] # "_manifest" or "/MANIFEST" | |
| dir_valid = is_runfiles_directory(runfiles_dir) | |
| return (runfiles_mf if mf_alid else "", runfiles_dir if dir_valid else "") |