pip/src/pip/_internal/operations/build/build_tracker.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

140 lines
4.7 KiB
Python
Raw Normal View History

import contextlib
import hashlib
import logging
import os
from types import TracebackType
from typing import Dict, Generator, Optional, Set, Type, Union
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.temp_dir import TempDirectory
logger = logging.getLogger(__name__)
@contextlib.contextmanager
def update_env_context_manager(**changes: str) -> Generator[None, None, None]:
target = os.environ
# Save values from the target and change them.
non_existent_marker = object()
2021-07-23 16:38:27 +02:00
saved_values: Dict[str, Union[object, str]] = {}
2019-10-30 18:05:12 +01:00
for name, new_value in changes.items():
try:
saved_values[name] = target[name]
except KeyError:
saved_values[name] = non_existent_marker
2019-10-30 18:05:12 +01:00
target[name] = new_value
try:
yield
finally:
# Restore original values in the target.
2019-10-30 18:05:12 +01:00
for name, original_value in saved_values.items():
if original_value is non_existent_marker:
del target[name]
else:
2019-10-30 18:05:12 +01:00
assert isinstance(original_value, str) # for mypy
target[name] = original_value
2019-10-27 09:31:14 +01:00
@contextlib.contextmanager
def get_build_tracker() -> Generator["BuildTracker", None, None]:
2022-03-26 11:04:54 +01:00
root = os.environ.get("PIP_BUILD_TRACKER")
with contextlib.ExitStack() as ctx:
2019-10-27 09:31:14 +01:00
if root is None:
2022-03-26 11:04:54 +01:00
root = ctx.enter_context(TempDirectory(kind="build-tracker")).path
ctx.enter_context(update_env_context_manager(PIP_BUILD_TRACKER=root))
2020-01-29 18:24:26 +01:00
logger.debug("Initialized build tracking at %s", root)
2019-10-27 09:31:14 +01:00
with BuildTracker(root) as tracker:
yield tracker
2019-10-27 09:31:14 +01:00
class TrackerId(str):
"""Uniquely identifying string provided to the build tracker."""
class BuildTracker:
"""Ensure that an sdist cannot request itself as a setup requirement.
When an sdist is prepared, it identifies its setup requirements in the
context of ``BuildTracker.track()``. If a requirement shows up recursively, this
raises an exception.
This stops fork bombs embedded in malicious packages."""
2021-07-23 16:38:27 +02:00
def __init__(self, root: str) -> None:
self._root = root
self._entries: Dict[TrackerId, InstallRequirement] = {}
2019-11-10 08:12:22 +01:00
logger.debug("Created build tracker: %s", self._root)
def __enter__(self) -> "BuildTracker":
2019-11-10 08:12:22 +01:00
logger.debug("Entered build tracker: %s", self._root)
return self
def __exit__(
self,
2021-07-23 16:38:27 +02:00
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
self.cleanup()
def _entry_path(self, key: TrackerId) -> str:
hashed = hashlib.sha224(key.encode()).hexdigest()
return os.path.join(self._root, hashed)
def add(self, req: InstallRequirement, key: TrackerId) -> None:
"""Add an InstallRequirement to build tracking."""
2020-05-21 22:57:09 +02:00
# Get the file to write information about this requirement.
entry_path = self._entry_path(key)
# Try reading from the file. If it exists and can be read from, a build
# is already in progress, so a LookupError is raised.
try:
with open(entry_path) as fp:
contents = fp.read()
except FileNotFoundError:
pass
else:
2020-01-29 18:24:26 +01:00
message = "{} is already being built: {}".format(req.link, contents)
raise LookupError(message)
# If we're here, req should really not be building already.
assert key not in self._entries
# Start tracking this requirement.
with open(entry_path, "w", encoding="utf-8") as fp:
fp.write(str(req))
self._entries[key] = req
logger.debug("Added %s to build tracker %r", req, self._root)
def remove(self, req: InstallRequirement, key: TrackerId) -> None:
"""Remove an InstallRequirement from build tracking."""
# Delete the created file and the corresponding entry.
os.unlink(self._entry_path(key))
del self._entries[key]
logger.debug("Removed %s from build tracker %r", req, self._root)
2021-07-23 16:38:27 +02:00
def cleanup(self) -> None:
for key, req in list(self._entries.items()):
self.remove(req, key)
logger.debug("Removed build tracker: %r", self._root)
@contextlib.contextmanager
def track(self, req: InstallRequirement, key: str) -> Generator[None, None, None]:
"""Ensure that `key` cannot install itself as a setup requirement.
:raises LookupError: If `key` was already provided in a parent invocation of
the context introduced by this method."""
tracker_id = TrackerId(key)
self.add(req, tracker_id)
yield
self.remove(req, tracker_id)