| # 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. | |
| See @rules_python//python/runfiles/README.md for usage instructions. | |
| :::{versionadded} 1.7.0 | |
| Support for Bazel's `--incompatible_compact_repo_mapping_manifest` flag was added. | |
| This enables prefix-based repository mappings to reduce memory usage for large | |
| dependency graphs under bzlmod. | |
| ::: | |
| """ | |
| from __future__ import annotations | |
| import inspect | |
| import os | |
| import pathlib | |
| import posixpath | |
| import sys | |
| from collections import defaultdict | |
| from collections.abc import Generator, Iterator | |
| from typing import cast | |
| if sys.version_info >= (3, 12): | |
| from typing import override | |
| else: | |
| from typing import TypeVar | |
| _FuncT = TypeVar("_FuncT") | |
| def override(func: _FuncT) -> _FuncT: | |
| return func | |
| if sys.version_info >= (3, 11): | |
| from typing import Self | |
| elif sys.version_info >= (3, 10): | |
| from typing import TypeAlias | |
| Self: TypeAlias = "Path" # pyrefly: ignore[invalid-type-form] | |
| else: | |
| from typing import Any as Self | |
| class _RepositoryMapping: | |
| """Repository mapping for resolving apparent repository names to canonical ones. | |
| Handles both exact mappings and prefix-based mappings introduced by the | |
| --incompatible_compact_repo_mapping_manifest flag. | |
| """ | |
| def __init__( | |
| self, | |
| exact_mappings: dict[tuple[str, str], str], | |
| prefixed_mappings: dict[tuple[str, str], str], | |
| ) -> None: | |
| """Initialize repository mapping with exact and prefixed mappings. | |
| Args: | |
| exact_mappings: Dict mapping (source_canonical, target_apparent) -> target_canonical | |
| prefixed_mappings: Dict mapping (source_prefix, target_apparent) -> target_canonical | |
| """ | |
| self._exact_mappings = exact_mappings | |
| # Group prefixed mappings by target_apparent for faster lookups | |
| self._grouped_prefixed_mappings = defaultdict(list) | |
| for ( | |
| prefix_source, | |
| target_app, | |
| ), target_canonical in prefixed_mappings.items(): | |
| self._grouped_prefixed_mappings[target_app].append( | |
| (prefix_source, target_canonical) | |
| ) | |
| @staticmethod | |
| def create_from_file(repo_mapping_path: str | None) -> _RepositoryMapping: | |
| """Create RepositoryMapping from a repository mapping manifest file. | |
| Args: | |
| repo_mapping_path: Path to the repository mapping file, or None if not available | |
| Returns: | |
| RepositoryMapping instance with parsed mappings | |
| """ | |
| # If the repository mapping file can't be found, that is not an error: We | |
| # might be running without Bzlmod enabled or there may not be any runfiles. | |
| # In this case, just apply empty repo mappings. | |
| if not repo_mapping_path: | |
| return _RepositoryMapping({}, {}) | |
| try: | |
| with open(repo_mapping_path, "r", encoding="utf-8", newline="\n") as f: | |
| content = f.read() | |
| except FileNotFoundError: | |
| return _RepositoryMapping({}, {}) | |
| exact_mappings = {} | |
| prefixed_mappings = {} | |
| for line in content.splitlines(): | |
| source_canonical, target_apparent, target_canonical = line.split(",") | |
| if source_canonical.endswith("*"): | |
| # This is a prefixed mapping - remove the '*' for prefix matching | |
| prefix = source_canonical[:-1] | |
| prefixed_mappings[(prefix, target_apparent)] = target_canonical | |
| else: | |
| # This is an exact mapping | |
| exact_mappings[(source_canonical, target_apparent)] = target_canonical | |
| return _RepositoryMapping(exact_mappings, prefixed_mappings) | |
| def lookup(self, source_repo: str | None, target_apparent: str) -> str | None: | |
| """Look up repository mapping for the given source and target. | |
| This handles both exact mappings and prefix-based mappings introduced by the | |
| --incompatible_compact_repo_mapping_manifest flag. Exact mappings are tried | |
| first, followed by prefix-based mappings where order matters. | |
| Args: | |
| source_repo: Source canonical repository name | |
| target_apparent: Target apparent repository name | |
| Returns: | |
| target_canonical repository name, or None if no mapping exists | |
| """ | |
| if source_repo is None: | |
| return None | |
| key = (source_repo, target_apparent) | |
| # Try exact mapping first | |
| if key in self._exact_mappings: | |
| return self._exact_mappings[key] | |
| # Try prefixed mapping if no exact match found | |
| if target_apparent in self._grouped_prefixed_mappings: | |
| for prefix_source, target_canonical in self._grouped_prefixed_mappings[ | |
| target_apparent | |
| ]: | |
| if source_repo.startswith(prefix_source): | |
| return target_canonical | |
| # No mapping found | |
| return None | |
| def is_empty(self) -> bool: | |
| """Check if this repository mapping is empty (no exact or prefixed mappings). | |
| Returns: | |
| True if there are no mappings, False otherwise | |
| """ | |
| return ( | |
| len(self._exact_mappings) == 0 and len(self._grouped_prefixed_mappings) == 0 | |
| ) | |
| class Path(pathlib.Path): | |
| """A pathlib-like path object for runfiles. | |
| This class extends `pathlib.Path` and resolves paths | |
| using the associated `Runfiles` instance when converted to a string. | |
| """ | |
| # Static type checkers may not realize `self` in the methods | |
| # refers to our Path class instead of pathlib.Path | |
| _runfiles: Runfiles | None | |
| _source_repo: str | None | |
| # For Python < 3.12 compatibility when subclassing Path directly | |
| _flavour = getattr(type(pathlib.Path()), "_flavour", None) | |
| def __new__( | |
| cls, | |
| *args: str | os.PathLike, | |
| runfiles: Runfiles | None = None, | |
| source_repo: str | None = None, | |
| ) -> Self: | |
| """Private constructor. Use Runfiles.root() to create instances.""" | |
| obj = cast("Path", super().__new__(cls, *args)) | |
| obj._runfiles = runfiles | |
| obj._source_repo = source_repo | |
| return cast(Self, obj) | |
| def __init__( | |
| self, | |
| *args: str | os.PathLike, | |
| runfiles: Runfiles | None = None, | |
| source_repo: str | None = None, | |
| ) -> None: | |
| # In Python 3.12+, pathlib was refactored and Path.__init__ now accepts | |
| # *args. Prior to 3.12, Path did not define __init__, so | |
| # super().__init__(*args) would fall through to object.__init__, which | |
| # raises a TypeError because it takes no arguments. | |
| if sys.version_info >= (3, 12): | |
| super().__init__(*args) | |
| else: | |
| super().__init__() | |
| # We override resolve() and absolute() to ensure that in Python < 3.12, | |
| # where pathlib internally uses object.__new__ instead of our custom | |
| # __new__ or with_segments(), the runfiles state is preserved. We delegate | |
| # to self._as_path() because super().resolve() creates intermediate objects | |
| # that would otherwise crash during internal stat() calls. | |
| @override | |
| def resolve(self, strict: bool = False) -> Self: | |
| return type(self)( | |
| self._as_path().resolve(strict=strict), | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| @override | |
| def absolute(self) -> Self: | |
| return type(self)( | |
| self._as_path().absolute(), | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| @override | |
| def with_segments(self, *pathsegments: str | os.PathLike) -> Self: | |
| """Used by Python 3.12+ pathlib to create new path objects.""" | |
| return type(self)( | |
| *pathsegments, | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| # For Python < 3.12 | |
| def _make_child(self, args: tuple[str, ...]) -> Self: | |
| # _make_child is an internal CPython method in Python < 3.12 omitted from | |
| # typeshed stubs. We ignore [missing-attribute] for pyrefly. | |
| obj = cast("Path", super()._make_child(args)) # pyrefly: ignore[missing-attribute] | |
| obj._runfiles = self._runfiles | |
| obj._source_repo = self._source_repo | |
| return cast(Self, obj) | |
| @property | |
| @override | |
| def parents(self) -> tuple[Self, ...]: | |
| return tuple( | |
| type(self)( | |
| p, | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| for p in super().parents | |
| ) | |
| @property | |
| @override | |
| def parent(self) -> Self: | |
| return type(self)( | |
| super().parent, | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| @property | |
| def runfile_path(self) -> str: | |
| """Returns the runfiles-root relative path.""" | |
| path_posix = super().__str__().replace("\\", "/") | |
| if path_posix == ".": | |
| return "" | |
| return path_posix | |
| @override | |
| def with_name(self, name: str) -> Self: | |
| return type(self)( | |
| super().with_name(name), | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| @override | |
| def with_suffix(self, suffix: str) -> Self: | |
| return type(self)( | |
| super().with_suffix(suffix), | |
| runfiles=self._runfiles, | |
| source_repo=self._source_repo, | |
| ) | |
| def _as_path(self) -> pathlib.Path: | |
| return pathlib.Path(str(self)) | |
| @override | |
| def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: | |
| return self._as_path().stat(follow_symlinks=follow_symlinks) | |
| @override | |
| def lstat(self) -> os.stat_result: | |
| return self._as_path().lstat() | |
| @override | |
| def exists(self, *, follow_symlinks: bool = True) -> bool: | |
| if not follow_symlinks and sys.version_info >= (3, 12): | |
| return self._as_path().exists(follow_symlinks=follow_symlinks) | |
| return self._as_path().exists() | |
| @override | |
| def is_dir(self, *, follow_symlinks: bool = True) -> bool: | |
| if not follow_symlinks and sys.version_info >= (3, 13): | |
| return self._as_path().is_dir(follow_symlinks=follow_symlinks) | |
| return self._as_path().is_dir() | |
| @override | |
| def is_file(self, *, follow_symlinks: bool = True) -> bool: | |
| if not follow_symlinks and sys.version_info >= (3, 13): | |
| return self._as_path().is_file(follow_symlinks=follow_symlinks) | |
| return self._as_path().is_file() | |
| @override | |
| def is_symlink(self) -> bool: | |
| return self._as_path().is_symlink() | |
| @override | |
| def is_block_device(self) -> bool: | |
| return self._as_path().is_block_device() | |
| @override | |
| def is_char_device(self) -> bool: | |
| return self._as_path().is_char_device() | |
| @override | |
| def is_fifo(self) -> bool: | |
| return self._as_path().is_fifo() | |
| @override | |
| def is_socket(self) -> bool: | |
| return self._as_path().is_socket() | |
| # Path.open in pathlib has multiple overloads in typeshed. We use a | |
| # simplified delegation signature here. | |
| @override | |
| def open( # pyrefly: ignore[bad-override] | |
| self, | |
| mode: str = "r", | |
| buffering: int = -1, | |
| encoding: str | None = None, | |
| errors: str | None = None, | |
| newline: str | None = None, | |
| ): | |
| return self._as_path().open( | |
| mode=mode, | |
| buffering=buffering, | |
| encoding=encoding, | |
| errors=errors, | |
| newline=newline, | |
| ) | |
| @override | |
| def read_bytes(self) -> bytes: | |
| return self._as_path().read_bytes() | |
| @override | |
| def read_text( | |
| self, | |
| encoding: str | None = None, | |
| errors: str | None = None, | |
| newline: str | None = None, | |
| ) -> str: | |
| if sys.version_info >= (3, 13) and newline is not None: | |
| return self._as_path().read_text( | |
| encoding=encoding, | |
| errors=errors, | |
| newline=newline, | |
| ) | |
| return self._as_path().read_text(encoding=encoding, errors=errors) | |
| @override | |
| def iterdir(self) -> Generator[Self, None, None]: | |
| resolved = self._as_path() | |
| for p in resolved.iterdir(): | |
| yield self / p.name | |
| # Return types and keyword arguments vary across Python versions in typeshed. | |
| @override | |
| def glob( # pyrefly: ignore[bad-override] | |
| self, | |
| pattern: str, | |
| *, | |
| case_sensitive: bool | None = None, | |
| recurse_symlinks: bool = False, | |
| ) -> Iterator[Self]: | |
| resolved = self._as_path() | |
| if sys.version_info >= (3, 13): | |
| it = resolved.glob( | |
| pattern, | |
| case_sensitive=case_sensitive, | |
| recurse_symlinks=recurse_symlinks, | |
| ) | |
| elif sys.version_info >= (3, 12): | |
| it = resolved.glob(pattern, case_sensitive=case_sensitive) | |
| else: | |
| it = resolved.glob(pattern) | |
| for p in it: | |
| yield self / p.relative_to(resolved) | |
| # Return types and keyword arguments vary across Python versions in typeshed. | |
| @override | |
| def rglob( # pyrefly: ignore[bad-override] | |
| self, | |
| pattern: str, | |
| *, | |
| case_sensitive: bool | None = None, | |
| recurse_symlinks: bool = False, | |
| ) -> Iterator[Self]: | |
| resolved = self._as_path() | |
| if sys.version_info >= (3, 13): | |
| it = resolved.rglob( | |
| pattern, | |
| case_sensitive=case_sensitive, | |
| recurse_symlinks=recurse_symlinks, | |
| ) | |
| elif sys.version_info >= (3, 12): | |
| it = resolved.rglob(pattern, case_sensitive=case_sensitive) | |
| else: | |
| it = resolved.rglob(pattern) | |
| for p in it: | |
| yield self / p.relative_to(resolved) | |
| @override | |
| def match( | |
| self, | |
| path_pattern: str, | |
| *, | |
| case_sensitive: bool | None = None, | |
| ) -> bool: | |
| if sys.version_info >= (3, 12): | |
| return self._as_path().match(path_pattern, case_sensitive=case_sensitive) | |
| return self._as_path().match(path_pattern) | |
| def __repr__(self) -> str: | |
| return "runfiles.Path({!r})".format(self.runfile_path) | |
| def __str__(self) -> str: | |
| assert self._runfiles is not None # type assert | |
| path_posix = super().__str__().replace("\\", "/") | |
| if not path_posix or path_posix == ".": | |
| # pylint: disable=protected-access | |
| return self._runfiles._python_runfiles_root # pyrefly: ignore[missing-attribute] | |
| resolved = self._runfiles.Rlocation(path_posix, source_repo=self._source_repo) | |
| if resolved is not None: | |
| return resolved | |
| # pylint: disable=protected-access | |
| return posixpath.join(self._runfiles._python_runfiles_root, path_posix) # pyrefly: ignore[missing-attribute] | |
| def __fspath__(self) -> str: | |
| return str(self) | |
| def runfiles_root(self) -> "Path": | |
| """Returns a Path object representing the runfiles root.""" | |
| assert self._runfiles is not None # type assert | |
| return self._runfiles.root(source_repo=self._source_repo) | |
| class _ManifestBased: | |
| """`Runfiles` strategy that parses a runfiles-manifest to look up runfiles.""" | |
| def __init__(self, path: str) -> None: | |
| if not path: | |
| raise ValueError() | |
| if not isinstance(path, str): | |
| raise TypeError() | |
| self._path = path | |
| self._runfiles = _ManifestBased._LoadRunfiles(path) | |
| def RlocationChecked(self, path: str) -> str | None: | |
| """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: str) -> dict[str, str]: | |
| """Loads the runfiles manifest.""" | |
| result = {} | |
| with open(path, "r", encoding="utf-8", newline="\n") as f: | |
| for line in f: | |
| line = line.rstrip("\n") | |
| if line.startswith(" "): | |
| # In lines that start with a space, spaces, newlines, and backslashes are escaped as \s, \n, and \b in | |
| # link and newlines and backslashes are escaped in target. | |
| escaped_link, escaped_target = line[1:].split(" ", maxsplit=1) | |
| link = ( | |
| escaped_link.replace(r"\s", " ") | |
| .replace(r"\n", "\n") | |
| .replace(r"\b", "\\") | |
| ) | |
| target = escaped_target.replace(r"\n", "\n").replace(r"\b", "\\") | |
| else: | |
| link, target = line.split(" ", maxsplit=1) | |
| if target: | |
| result[link] = target | |
| else: | |
| result[link] = link | |
| return result | |
| def _GetRunfilesDir(self) -> str: | |
| if self._path.endswith("/MANIFEST") or self._path.endswith("\\MANIFEST"): | |
| return self._path[: -len("/MANIFEST")] | |
| if self._path.endswith(".runfiles_manifest"): | |
| return self._path[: -len("_manifest")] | |
| return "" | |
| def EnvVars(self) -> dict[str, str]: | |
| 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: | |
| """`Runfiles` strategy that appends runfiles paths to the runfiles root.""" | |
| def __init__(self, path: str) -> None: | |
| if not path: | |
| raise ValueError() | |
| if not isinstance(path, str): | |
| raise TypeError() | |
| self._runfiles_root = path | |
| def RlocationChecked(self, path: str) -> str: | |
| # 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 _GetRunfilesDir(self) -> str: | |
| return self._runfiles_root | |
| def EnvVars(self) -> dict[str, str]: | |
| 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, | |
| } | |
| class Runfiles: | |
| """Returns the runtime location of runfiles. | |
| Runfiles are data-dependencies of Bazel-built binaries and tests. | |
| """ | |
| def __init__(self, strategy: _ManifestBased | _DirectoryBased) -> None: | |
| self._strategy = strategy | |
| self._python_runfiles_root = strategy._GetRunfilesDir() | |
| self._repo_mapping = _RepositoryMapping.create_from_file( | |
| strategy.RlocationChecked("_repo_mapping") | |
| ) | |
| def root(self, source_repo: str | None = None) -> Path: | |
| """Returns a Path object representing the runfiles root. | |
| The repository mapping used by the returned Path object is that of the | |
| caller of this method. | |
| """ | |
| if source_repo is None and not self._repo_mapping.is_empty(): | |
| source_repo = self.CurrentRepository(frame=2) | |
| return Path(runfiles=self, source_repo=source_repo) | |
| def Rlocation(self, path: str, source_repo: str | None = None) -> str | None: | |
| """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 | |
| source_repo: string; optional; the canonical name of the repository | |
| whose repository mapping should be used to resolve apparent to | |
| canonical repository names in `path`. If `None` (default), the | |
| repository mapping of the repository containing the caller of this | |
| method is used. Explicitly setting this parameter should only be | |
| necessary for libraries that want to wrap the runfiles library. Use | |
| `CurrentRepository` to obtain canonical repository names. | |
| 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 | |
| if source_repo is None and not self._repo_mapping.is_empty(): | |
| # Look up runfiles using the repository mapping of the caller of the | |
| # current method. If the repo mapping is empty, determining this | |
| # name is not necessary. | |
| source_repo = self.CurrentRepository(frame=2) | |
| # Split off the first path component, which contains the repository | |
| # name (apparent or canonical). | |
| target_repo, _, remainder = path.partition("/") | |
| target_canonical = self._repo_mapping.lookup(source_repo, target_repo) | |
| if not remainder or target_canonical is None: | |
| # One of the following is the case: | |
| # - not using Bzlmod, so the repository mapping is empty and | |
| # apparent and canonical repository names are the same | |
| # - target_repo is already a canonical repository name and does not | |
| # have to be mapped. | |
| # - path did not contain a slash and referred to a root symlink, | |
| # which also should not be mapped. | |
| return self._strategy.RlocationChecked(path) | |
| assert source_repo is not None, ( | |
| "BUG: if the `source_repo` is None, we should never go past the `if` statement above" | |
| ) | |
| # Look up the target repository using the repository mapping | |
| if target_canonical is not None: | |
| return self._strategy.RlocationChecked(target_canonical + "/" + remainder) | |
| # No mapping found - assume target_repo is already canonical or | |
| # we're not using Bzlmod | |
| return self._strategy.RlocationChecked(path) | |
| def EnvVars(self) -> dict[str, str]: | |
| """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: int = 1) -> str: | |
| """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 | |
| """ | |
| try: | |
| # pylint: disable-next=protected-access | |
| caller_path = inspect.getfile(sys._getframe(frame)) | |
| except (TypeError, ValueError) as exc: | |
| raise ValueError("failed to determine caller's file path") from exc | |
| caller_runfiles_path = os.path.relpath(caller_path, self._python_runfiles_root) | |
| if caller_runfiles_path.startswith(".." + os.path.sep): | |
| # With Python 3.10 and earlier, sys.path contains the directory | |
| # of the script, which can result in a module being loaded from | |
| # outside the runfiles tree. In this case, assume that the module is | |
| # located in the main repository. | |
| # With Python 3.11 and higher, the Python launcher sets | |
| # PYTHONSAFEPATH, which prevents this behavior. | |
| # On Windows, the current toolchain being used has a buggy zip file | |
| # bootstrap, which leaves RUNFILES_DIR pointing at the first stage | |
| # path and not the module path. In this case too, assume that the | |
| # module is located in the main repository. | |
| # TODO: This doesn't cover the case of a script being run from an | |
| # external repository, which could be heuristically detected | |
| # by parsing the script's path. | |
| if (sys.version_info.minor <= 10 or sys.platform == "win32") and sys.path[ | |
| 0 | |
| ] != self._python_runfiles_root: | |
| return "" | |
| 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) | |
| ] | |
| # With Bzlmod, the runfiles directory of the main repository is always | |
| # named "_main". Without Bzlmod, the value returned by this function is | |
| # never used, so we just assume Bzlmod is enabled. | |
| if caller_runfiles_directory == "_main": | |
| # 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 | |
| # TODO: Update return type to Self when 3.11 is the min version | |
| # https://peps.python.org/pep-0673/ | |
| @staticmethod | |
| def CreateManifestBased(manifest_path: str) -> "Runfiles": | |
| return Runfiles(_ManifestBased(manifest_path)) | |
| # TODO: Update return type to Self when 3.11 is the min version | |
| # https://peps.python.org/pep-0673/ | |
| @staticmethod | |
| def CreateDirectoryBased(runfiles_dir_path: str) -> "Runfiles": | |
| return Runfiles(_DirectoryBased(runfiles_dir_path)) | |
| # TODO: Update return type to Self when 3.11 is the min version | |
| # https://peps.python.org/pep-0673/ | |
| @staticmethod | |
| def Create(env: dict[str, str] | None = None) -> Runfiles | 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 | |
| # TODO: Update return type to Self when 3.11 is the min version | |
| # https://peps.python.org/pep-0673/ | |
| @staticmethod | |
| def CreateOrRaise(env: dict[str, str] | None = None) -> Runfiles: | |
| """Returns a new `Runfiles` instance, or raises an error. | |
| 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 raises a `RuntimeError`. | |
| Args: | |
| env: {string: string}; optional; the map of environment variables. If | |
| None, this function uses the environment variable map of this | |
| process. | |
| Raises: | |
| RuntimeError: if runfiles cannot be found. | |
| :::{versionadded} 2.4.0 | |
| ::: | |
| """ | |
| runfiles = Runfiles.Create(env=env) | |
| if runfiles is None: | |
| raise RuntimeError( | |
| "Cannot create Runfiles: $RUNFILES_MANIFEST_FILE and $RUNFILES_DIR are both unset or empty" | |
| ) | |
| return runfiles | |
| # Support legacy imports by defining a private symbol. | |
| _Runfiles = Runfiles | |
| def CreateManifestBased(manifest_path: str) -> Runfiles: | |
| return Runfiles.CreateManifestBased(manifest_path) | |
| def CreateDirectoryBased(runfiles_dir_path: str) -> Runfiles: | |
| return Runfiles.CreateDirectoryBased(runfiles_dir_path) | |
| def Create(env: dict[str, str] | None = None) -> Runfiles | None: | |
| return Runfiles.Create(env) | |
| def CreateOrRaise(env: dict[str, str] | None = None) -> Runfiles: | |
| """Refer to `Runfiles.CreateOrRaise`. | |
| :::{versionadded} 2.4.0 | |
| ::: | |
| """ | |
| return Runfiles.CreateOrRaise(env) |