req_tracker.py 4.6 KB

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