|  | # 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 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 "") |