build_tracker.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import contextlib
  2. import hashlib
  3. import logging
  4. import os
  5. from types import TracebackType
  6. from typing import Dict, Generator, Optional, Set, Type, Union
  7. from pip._internal.models.link import Link
  8. from pip._internal.req.req_install import InstallRequirement
  9. from pip._internal.utils.temp_dir import TempDirectory
  10. logger = logging.getLogger(__name__)
  11. @contextlib.contextmanager
  12. def update_env_context_manager(**changes: str) -> Generator[None, None, None]:
  13. target = os.environ
  14. # Save values from the target and change them.
  15. non_existent_marker = object()
  16. saved_values: Dict[str, Union[object, str]] = {}
  17. for name, new_value in changes.items():
  18. try:
  19. saved_values[name] = target[name]
  20. except KeyError:
  21. saved_values[name] = non_existent_marker
  22. target[name] = new_value
  23. try:
  24. yield
  25. finally:
  26. # Restore original values in the target.
  27. for name, original_value in saved_values.items():
  28. if original_value is non_existent_marker:
  29. del target[name]
  30. else:
  31. assert isinstance(original_value, str) # for mypy
  32. target[name] = original_value
  33. @contextlib.contextmanager
  34. def get_build_tracker() -> Generator["BuildTracker", None, None]:
  35. root = os.environ.get("PIP_BUILD_TRACKER")
  36. with contextlib.ExitStack() as ctx:
  37. if root is None:
  38. root = ctx.enter_context(TempDirectory(kind="build-tracker")).path
  39. ctx.enter_context(update_env_context_manager(PIP_BUILD_TRACKER=root))
  40. logger.debug("Initialized build tracking at %s", root)
  41. with BuildTracker(root) as tracker:
  42. yield tracker
  43. class BuildTracker:
  44. def __init__(self, root: str) -> None:
  45. self._root = root
  46. self._entries: Set[InstallRequirement] = set()
  47. logger.debug("Created build tracker: %s", self._root)
  48. def __enter__(self) -> "BuildTracker":
  49. logger.debug("Entered build tracker: %s", self._root)
  50. return self
  51. def __exit__(
  52. self,
  53. exc_type: Optional[Type[BaseException]],
  54. exc_val: Optional[BaseException],
  55. exc_tb: Optional[TracebackType],
  56. ) -> None:
  57. self.cleanup()
  58. def _entry_path(self, link: Link) -> str:
  59. hashed = hashlib.sha224(link.url_without_fragment.encode()).hexdigest()
  60. return os.path.join(self._root, hashed)
  61. def add(self, req: InstallRequirement) -> None:
  62. """Add an InstallRequirement to build tracking."""
  63. assert req.link
  64. # Get the file to write information about this requirement.
  65. entry_path = self._entry_path(req.link)
  66. # Try reading from the file. If it exists and can be read from, a build
  67. # is already in progress, so a LookupError is raised.
  68. try:
  69. with open(entry_path) as fp:
  70. contents = fp.read()
  71. except FileNotFoundError:
  72. pass
  73. else:
  74. message = "{} is already being built: {}".format(req.link, contents)
  75. raise LookupError(message)
  76. # If we're here, req should really not be building already.
  77. assert req not in self._entries
  78. # Start tracking this requirement.
  79. with open(entry_path, "w", encoding="utf-8") as fp:
  80. fp.write(str(req))
  81. self._entries.add(req)
  82. logger.debug("Added %s to build tracker %r", req, self._root)
  83. def remove(self, req: InstallRequirement) -> None:
  84. """Remove an InstallRequirement from build tracking."""
  85. assert req.link
  86. # Delete the created file and the corresponding entries.
  87. os.unlink(self._entry_path(req.link))
  88. self._entries.remove(req)
  89. logger.debug("Removed %s from build tracker %r", req, self._root)
  90. def cleanup(self) -> None:
  91. for req in set(self._entries):
  92. self.remove(req)
  93. logger.debug("Removed build tracker: %r", self._root)
  94. @contextlib.contextmanager
  95. def track(self, req: InstallRequirement) -> Generator[None, None, None]:
  96. self.add(req)
  97. yield
  98. self.remove(req)