Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/wheel_builder.py
4799 views
"""Orchestrator for building wheels from InstallRequirements.1"""23import logging4import os.path5import re6import shutil7from typing import Any, Callable, Iterable, List, Optional, Tuple89from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version10from pip._vendor.packaging.version import InvalidVersion, Version1112from pip._internal.cache import WheelCache13from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel14from pip._internal.metadata import FilesystemWheel, get_wheel_distribution15from pip._internal.models.link import Link16from pip._internal.models.wheel import Wheel17from pip._internal.operations.build.wheel import build_wheel_pep51718from pip._internal.operations.build.wheel_editable import build_wheel_editable19from pip._internal.operations.build.wheel_legacy import build_wheel_legacy20from pip._internal.req.req_install import InstallRequirement21from pip._internal.utils.logging import indent_log22from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed23from pip._internal.utils.setuptools_build import make_setuptools_clean_args24from pip._internal.utils.subprocess import call_subprocess25from pip._internal.utils.temp_dir import TempDirectory26from pip._internal.utils.urls import path_to_url27from pip._internal.vcs import vcs2829logger = logging.getLogger(__name__)3031_egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)3233BinaryAllowedPredicate = Callable[[InstallRequirement], bool]34BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]353637def _contains_egg_info(s: str) -> bool:38"""Determine whether the string looks like an egg_info.3940:param s: The string to parse. E.g. foo-2.141"""42return bool(_egg_info_re.search(s))434445def _should_build(46req: InstallRequirement,47need_wheel: bool,48check_binary_allowed: BinaryAllowedPredicate,49) -> bool:50"""Return whether an InstallRequirement should be built into a wheel."""51if req.constraint:52# never build requirements that are merely constraints53return False54if req.is_wheel:55if need_wheel:56logger.info(57"Skipping %s, due to already being wheel.",58req.name,59)60return False6162if need_wheel:63# i.e. pip wheel, not pip install64return True6566# From this point, this concerns the pip install command only67# (need_wheel=False).6869if not req.source_dir:70return False7172if req.editable:73# we only build PEP 660 editable requirements74return req.supports_pyproject_editable()7576if req.use_pep517:77return True7879if not check_binary_allowed(req):80logger.info(81"Skipping wheel build for %s, due to binaries being disabled for it.",82req.name,83)84return False8586if not is_wheel_installed():87# we don't build legacy requirements if wheel is not installed88logger.info(89"Using legacy 'setup.py install' for %s, "90"since package 'wheel' is not installed.",91req.name,92)93return False9495return True969798def should_build_for_wheel_command(99req: InstallRequirement,100) -> bool:101return _should_build(req, need_wheel=True, check_binary_allowed=_always_true)102103104def should_build_for_install_command(105req: InstallRequirement,106check_binary_allowed: BinaryAllowedPredicate,107) -> bool:108return _should_build(109req, need_wheel=False, check_binary_allowed=check_binary_allowed110)111112113def _should_cache(114req: InstallRequirement,115) -> Optional[bool]:116"""117Return whether a built InstallRequirement can be stored in the persistent118wheel cache, assuming the wheel cache is available, and _should_build()119has determined a wheel needs to be built.120"""121if req.editable or not req.source_dir:122# never cache editable requirements123return False124125if req.link and req.link.is_vcs:126# VCS checkout. Do not cache127# unless it points to an immutable commit hash.128assert not req.editable129assert req.source_dir130vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)131assert vcs_backend132if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):133return True134return False135136assert req.link137base, ext = req.link.splitext()138if _contains_egg_info(base):139return True140141# Otherwise, do not cache.142return False143144145def _get_cache_dir(146req: InstallRequirement,147wheel_cache: WheelCache,148) -> str:149"""Return the persistent or temporary cache directory where the built150wheel need to be stored.151"""152cache_available = bool(wheel_cache.cache_dir)153assert req.link154if cache_available and _should_cache(req):155cache_dir = wheel_cache.get_path_for_link(req.link)156else:157cache_dir = wheel_cache.get_ephem_path_for_link(req.link)158return cache_dir159160161def _always_true(_: Any) -> bool:162return True163164165def _verify_one(req: InstallRequirement, wheel_path: str) -> None:166canonical_name = canonicalize_name(req.name or "")167w = Wheel(os.path.basename(wheel_path))168if canonicalize_name(w.name) != canonical_name:169raise InvalidWheelFilename(170"Wheel has unexpected file name: expected {!r}, "171"got {!r}".format(canonical_name, w.name),172)173dist = get_wheel_distribution(FilesystemWheel(wheel_path), canonical_name)174dist_verstr = str(dist.version)175if canonicalize_version(dist_verstr) != canonicalize_version(w.version):176raise InvalidWheelFilename(177"Wheel has unexpected file name: expected {!r}, "178"got {!r}".format(dist_verstr, w.version),179)180metadata_version_value = dist.metadata_version181if metadata_version_value is None:182raise UnsupportedWheel("Missing Metadata-Version")183try:184metadata_version = Version(metadata_version_value)185except InvalidVersion:186msg = f"Invalid Metadata-Version: {metadata_version_value}"187raise UnsupportedWheel(msg)188if metadata_version >= Version("1.2") and not isinstance(dist.version, Version):189raise UnsupportedWheel(190"Metadata 1.2 mandates PEP 440 version, "191"but {!r} is not".format(dist_verstr)192)193194195def _build_one(196req: InstallRequirement,197output_dir: str,198verify: bool,199build_options: List[str],200global_options: List[str],201editable: bool,202) -> Optional[str]:203"""Build one wheel.204205:return: The filename of the built wheel, or None if the build failed.206"""207artifact = "editable" if editable else "wheel"208try:209ensure_dir(output_dir)210except OSError as e:211logger.warning(212"Building %s for %s failed: %s",213artifact,214req.name,215e,216)217return None218219# Install build deps into temporary directory (PEP 518)220with req.build_env:221wheel_path = _build_one_inside_env(222req, output_dir, build_options, global_options, editable223)224if wheel_path and verify:225try:226_verify_one(req, wheel_path)227except (InvalidWheelFilename, UnsupportedWheel) as e:228logger.warning("Built %s for %s is invalid: %s", artifact, req.name, e)229return None230return wheel_path231232233def _build_one_inside_env(234req: InstallRequirement,235output_dir: str,236build_options: List[str],237global_options: List[str],238editable: bool,239) -> Optional[str]:240with TempDirectory(kind="wheel") as temp_dir:241assert req.name242if req.use_pep517:243assert req.metadata_directory244assert req.pep517_backend245if global_options:246logger.warning(247"Ignoring --global-option when building %s using PEP 517", req.name248)249if build_options:250logger.warning(251"Ignoring --build-option when building %s using PEP 517", req.name252)253if editable:254wheel_path = build_wheel_editable(255name=req.name,256backend=req.pep517_backend,257metadata_directory=req.metadata_directory,258tempd=temp_dir.path,259)260else:261wheel_path = build_wheel_pep517(262name=req.name,263backend=req.pep517_backend,264metadata_directory=req.metadata_directory,265tempd=temp_dir.path,266)267else:268wheel_path = build_wheel_legacy(269name=req.name,270setup_py_path=req.setup_py_path,271source_dir=req.unpacked_source_directory,272global_options=global_options,273build_options=build_options,274tempd=temp_dir.path,275)276277if wheel_path is not None:278wheel_name = os.path.basename(wheel_path)279dest_path = os.path.join(output_dir, wheel_name)280try:281wheel_hash, length = hash_file(wheel_path)282shutil.move(wheel_path, dest_path)283logger.info(284"Created wheel for %s: filename=%s size=%d sha256=%s",285req.name,286wheel_name,287length,288wheel_hash.hexdigest(),289)290logger.info("Stored in directory: %s", output_dir)291return dest_path292except Exception as e:293logger.warning(294"Building wheel for %s failed: %s",295req.name,296e,297)298# Ignore return, we can't do anything else useful.299if not req.use_pep517:300_clean_one_legacy(req, global_options)301return None302303304def _clean_one_legacy(req: InstallRequirement, global_options: List[str]) -> bool:305clean_args = make_setuptools_clean_args(306req.setup_py_path,307global_options=global_options,308)309310logger.info("Running setup.py clean for %s", req.name)311try:312call_subprocess(313clean_args, command_desc="python setup.py clean", cwd=req.source_dir314)315return True316except Exception:317logger.error("Failed cleaning build dir for %s", req.name)318return False319320321def build(322requirements: Iterable[InstallRequirement],323wheel_cache: WheelCache,324verify: bool,325build_options: List[str],326global_options: List[str],327) -> BuildResult:328"""Build wheels.329330:return: The list of InstallRequirement that succeeded to build and331the list of InstallRequirement that failed to build.332"""333if not requirements:334return [], []335336# Build the wheels.337logger.info(338"Building wheels for collected packages: %s",339", ".join(req.name for req in requirements), # type: ignore340)341342with indent_log():343build_successes, build_failures = [], []344for req in requirements:345assert req.name346cache_dir = _get_cache_dir(req, wheel_cache)347wheel_file = _build_one(348req,349cache_dir,350verify,351build_options,352global_options,353req.editable and req.permit_editable_wheels,354)355if wheel_file:356# Update the link for this.357req.link = Link(path_to_url(wheel_file))358req.local_file_path = req.link.file_path359assert req.link.is_wheel360build_successes.append(req)361else:362build_failures.append(req)363364# notify success/failure365if build_successes:366logger.info(367"Successfully built %s",368" ".join([req.name for req in build_successes]), # type: ignore369)370if build_failures:371logger.info(372"Failed to build %s",373" ".join([req.name for req in build_failures]), # type: ignore374)375# Return a list of requirements that failed to build376return build_successes, build_failures377378379