wheel_builder.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. """Orchestrator for building wheels from InstallRequirements.
  2. """
  3. import logging
  4. import os.path
  5. import re
  6. import shutil
  7. from typing import Any, Callable, Iterable, List, Optional, Tuple
  8. from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
  9. from pip._vendor.packaging.version import InvalidVersion, Version
  10. from pip._internal.cache import WheelCache
  11. from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
  12. from pip._internal.metadata import get_wheel_distribution
  13. from pip._internal.models.link import Link
  14. from pip._internal.models.wheel import Wheel
  15. from pip._internal.operations.build.wheel import build_wheel_pep517
  16. from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
  17. from pip._internal.req.req_install import InstallRequirement
  18. from pip._internal.utils.logging import indent_log
  19. from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed
  20. from pip._internal.utils.setuptools_build import make_setuptools_clean_args
  21. from pip._internal.utils.subprocess import call_subprocess
  22. from pip._internal.utils.temp_dir import TempDirectory
  23. from pip._internal.utils.urls import path_to_url
  24. from pip._internal.vcs import vcs
  25. logger = logging.getLogger(__name__)
  26. _egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.IGNORECASE)
  27. BinaryAllowedPredicate = Callable[[InstallRequirement], bool]
  28. BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
  29. def _contains_egg_info(s):
  30. # type: (str) -> bool
  31. """Determine whether the string looks like an egg_info.
  32. :param s: The string to parse. E.g. foo-2.1
  33. """
  34. return bool(_egg_info_re.search(s))
  35. def _should_build(
  36. req, # type: InstallRequirement
  37. need_wheel, # type: bool
  38. check_binary_allowed, # type: BinaryAllowedPredicate
  39. ):
  40. # type: (...) -> bool
  41. """Return whether an InstallRequirement should be built into a wheel."""
  42. if req.constraint:
  43. # never build requirements that are merely constraints
  44. return False
  45. if req.is_wheel:
  46. if need_wheel:
  47. logger.info(
  48. 'Skipping %s, due to already being wheel.', req.name,
  49. )
  50. return False
  51. if need_wheel:
  52. # i.e. pip wheel, not pip install
  53. return True
  54. # From this point, this concerns the pip install command only
  55. # (need_wheel=False).
  56. if req.editable or not req.source_dir:
  57. return False
  58. if req.use_pep517:
  59. return True
  60. if not check_binary_allowed(req):
  61. logger.info(
  62. "Skipping wheel build for %s, due to binaries "
  63. "being disabled for it.", req.name,
  64. )
  65. return False
  66. if not is_wheel_installed():
  67. # we don't build legacy requirements if wheel is not installed
  68. logger.info(
  69. "Using legacy 'setup.py install' for %s, "
  70. "since package 'wheel' is not installed.", req.name,
  71. )
  72. return False
  73. return True
  74. def should_build_for_wheel_command(
  75. req, # type: InstallRequirement
  76. ):
  77. # type: (...) -> bool
  78. return _should_build(
  79. req, need_wheel=True, check_binary_allowed=_always_true
  80. )
  81. def should_build_for_install_command(
  82. req, # type: InstallRequirement
  83. check_binary_allowed, # type: BinaryAllowedPredicate
  84. ):
  85. # type: (...) -> bool
  86. return _should_build(
  87. req, need_wheel=False, check_binary_allowed=check_binary_allowed
  88. )
  89. def _should_cache(
  90. req, # type: InstallRequirement
  91. ):
  92. # type: (...) -> Optional[bool]
  93. """
  94. Return whether a built InstallRequirement can be stored in the persistent
  95. wheel cache, assuming the wheel cache is available, and _should_build()
  96. has determined a wheel needs to be built.
  97. """
  98. if req.editable or not req.source_dir:
  99. # never cache editable requirements
  100. return False
  101. if req.link and req.link.is_vcs:
  102. # VCS checkout. Do not cache
  103. # unless it points to an immutable commit hash.
  104. assert not req.editable
  105. assert req.source_dir
  106. vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)
  107. assert vcs_backend
  108. if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):
  109. return True
  110. return False
  111. assert req.link
  112. base, ext = req.link.splitext()
  113. if _contains_egg_info(base):
  114. return True
  115. # Otherwise, do not cache.
  116. return False
  117. def _get_cache_dir(
  118. req, # type: InstallRequirement
  119. wheel_cache, # type: WheelCache
  120. ):
  121. # type: (...) -> str
  122. """Return the persistent or temporary cache directory where the built
  123. wheel need to be stored.
  124. """
  125. cache_available = bool(wheel_cache.cache_dir)
  126. assert req.link
  127. if cache_available and _should_cache(req):
  128. cache_dir = wheel_cache.get_path_for_link(req.link)
  129. else:
  130. cache_dir = wheel_cache.get_ephem_path_for_link(req.link)
  131. return cache_dir
  132. def _always_true(_):
  133. # type: (Any) -> bool
  134. return True
  135. def _verify_one(req, wheel_path):
  136. # type: (InstallRequirement, str) -> None
  137. canonical_name = canonicalize_name(req.name or "")
  138. w = Wheel(os.path.basename(wheel_path))
  139. if canonicalize_name(w.name) != canonical_name:
  140. raise InvalidWheelFilename(
  141. "Wheel has unexpected file name: expected {!r}, "
  142. "got {!r}".format(canonical_name, w.name),
  143. )
  144. dist = get_wheel_distribution(wheel_path, canonical_name)
  145. dist_verstr = str(dist.version)
  146. if canonicalize_version(dist_verstr) != canonicalize_version(w.version):
  147. raise InvalidWheelFilename(
  148. "Wheel has unexpected file name: expected {!r}, "
  149. "got {!r}".format(dist_verstr, w.version),
  150. )
  151. metadata_version_value = dist.metadata_version
  152. if metadata_version_value is None:
  153. raise UnsupportedWheel("Missing Metadata-Version")
  154. try:
  155. metadata_version = Version(metadata_version_value)
  156. except InvalidVersion:
  157. msg = f"Invalid Metadata-Version: {metadata_version_value}"
  158. raise UnsupportedWheel(msg)
  159. if (metadata_version >= Version("1.2")
  160. and not isinstance(dist.version, Version)):
  161. raise UnsupportedWheel(
  162. "Metadata 1.2 mandates PEP 440 version, "
  163. "but {!r} is not".format(dist_verstr)
  164. )
  165. def _build_one(
  166. req, # type: InstallRequirement
  167. output_dir, # type: str
  168. verify, # type: bool
  169. build_options, # type: List[str]
  170. global_options, # type: List[str]
  171. ):
  172. # type: (...) -> Optional[str]
  173. """Build one wheel.
  174. :return: The filename of the built wheel, or None if the build failed.
  175. """
  176. try:
  177. ensure_dir(output_dir)
  178. except OSError as e:
  179. logger.warning(
  180. "Building wheel for %s failed: %s",
  181. req.name, e,
  182. )
  183. return None
  184. # Install build deps into temporary directory (PEP 518)
  185. with req.build_env:
  186. wheel_path = _build_one_inside_env(
  187. req, output_dir, build_options, global_options
  188. )
  189. if wheel_path and verify:
  190. try:
  191. _verify_one(req, wheel_path)
  192. except (InvalidWheelFilename, UnsupportedWheel) as e:
  193. logger.warning("Built wheel for %s is invalid: %s", req.name, e)
  194. return None
  195. return wheel_path
  196. def _build_one_inside_env(
  197. req, # type: InstallRequirement
  198. output_dir, # type: str
  199. build_options, # type: List[str]
  200. global_options, # type: List[str]
  201. ):
  202. # type: (...) -> Optional[str]
  203. with TempDirectory(kind="wheel") as temp_dir:
  204. assert req.name
  205. if req.use_pep517:
  206. assert req.metadata_directory
  207. assert req.pep517_backend
  208. if global_options:
  209. logger.warning(
  210. 'Ignoring --global-option when building %s using PEP 517', req.name
  211. )
  212. if build_options:
  213. logger.warning(
  214. 'Ignoring --build-option when building %s using PEP 517', req.name
  215. )
  216. wheel_path = build_wheel_pep517(
  217. name=req.name,
  218. backend=req.pep517_backend,
  219. metadata_directory=req.metadata_directory,
  220. tempd=temp_dir.path,
  221. )
  222. else:
  223. wheel_path = build_wheel_legacy(
  224. name=req.name,
  225. setup_py_path=req.setup_py_path,
  226. source_dir=req.unpacked_source_directory,
  227. global_options=global_options,
  228. build_options=build_options,
  229. tempd=temp_dir.path,
  230. )
  231. if wheel_path is not None:
  232. wheel_name = os.path.basename(wheel_path)
  233. dest_path = os.path.join(output_dir, wheel_name)
  234. try:
  235. wheel_hash, length = hash_file(wheel_path)
  236. shutil.move(wheel_path, dest_path)
  237. logger.info('Created wheel for %s: '
  238. 'filename=%s size=%d sha256=%s',
  239. req.name, wheel_name, length,
  240. wheel_hash.hexdigest())
  241. logger.info('Stored in directory: %s', output_dir)
  242. return dest_path
  243. except Exception as e:
  244. logger.warning(
  245. "Building wheel for %s failed: %s",
  246. req.name, e,
  247. )
  248. # Ignore return, we can't do anything else useful.
  249. if not req.use_pep517:
  250. _clean_one_legacy(req, global_options)
  251. return None
  252. def _clean_one_legacy(req, global_options):
  253. # type: (InstallRequirement, List[str]) -> bool
  254. clean_args = make_setuptools_clean_args(
  255. req.setup_py_path,
  256. global_options=global_options,
  257. )
  258. logger.info('Running setup.py clean for %s', req.name)
  259. try:
  260. call_subprocess(clean_args, cwd=req.source_dir)
  261. return True
  262. except Exception:
  263. logger.error('Failed cleaning build dir for %s', req.name)
  264. return False
  265. def build(
  266. requirements, # type: Iterable[InstallRequirement]
  267. wheel_cache, # type: WheelCache
  268. verify, # type: bool
  269. build_options, # type: List[str]
  270. global_options, # type: List[str]
  271. ):
  272. # type: (...) -> BuildResult
  273. """Build wheels.
  274. :return: The list of InstallRequirement that succeeded to build and
  275. the list of InstallRequirement that failed to build.
  276. """
  277. if not requirements:
  278. return [], []
  279. # Build the wheels.
  280. logger.info(
  281. 'Building wheels for collected packages: %s',
  282. ', '.join(req.name for req in requirements), # type: ignore
  283. )
  284. with indent_log():
  285. build_successes, build_failures = [], []
  286. for req in requirements:
  287. cache_dir = _get_cache_dir(req, wheel_cache)
  288. wheel_file = _build_one(
  289. req, cache_dir, verify, build_options, global_options
  290. )
  291. if wheel_file:
  292. # Update the link for this.
  293. req.link = Link(path_to_url(wheel_file))
  294. req.local_file_path = req.link.file_path
  295. assert req.link.is_wheel
  296. build_successes.append(req)
  297. else:
  298. build_failures.append(req)
  299. # notify success/failure
  300. if build_successes:
  301. logger.info(
  302. 'Successfully built %s',
  303. ' '.join([req.name for req in build_successes]), # type: ignore
  304. )
  305. if build_failures:
  306. logger.info(
  307. 'Failed to build %s',
  308. ' '.join([req.name for req in build_failures]), # type: ignore
  309. )
  310. # Return a list of requirements that failed to build
  311. return build_successes, build_failures