wheel_builder.py 12 KB

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