123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- """Orchestrator for building wheels from InstallRequirements.
- """
- import logging
- import os.path
- import re
- import shutil
- from typing import Iterable, List, Optional, Tuple
- from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
- from pip._vendor.packaging.version import InvalidVersion, Version
- from pip._internal.cache import WheelCache
- from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
- from pip._internal.metadata import FilesystemWheel, get_wheel_distribution
- from pip._internal.models.link import Link
- from pip._internal.models.wheel import Wheel
- from pip._internal.operations.build.wheel import build_wheel_pep517
- from pip._internal.operations.build.wheel_editable import build_wheel_editable
- from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
- from pip._internal.req.req_install import InstallRequirement
- from pip._internal.utils.logging import indent_log
- from pip._internal.utils.misc import ensure_dir, hash_file
- from pip._internal.utils.setuptools_build import make_setuptools_clean_args
- from pip._internal.utils.subprocess import call_subprocess
- from pip._internal.utils.temp_dir import TempDirectory
- from pip._internal.utils.urls import path_to_url
- from pip._internal.vcs import vcs
- logger = logging.getLogger(__name__)
- _egg_info_re = re.compile(r"([a-z0-9_.]+)-([a-z0-9_.!+-]+)", re.IGNORECASE)
- BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]]
- def _contains_egg_info(s: str) -> bool:
- """Determine whether the string looks like an egg_info.
- :param s: The string to parse. E.g. foo-2.1
- """
- return bool(_egg_info_re.search(s))
- def _should_build(
- req: InstallRequirement,
- need_wheel: bool,
- ) -> bool:
- """Return whether an InstallRequirement should be built into a wheel."""
- if req.constraint:
- # never build requirements that are merely constraints
- return False
- if req.is_wheel:
- if need_wheel:
- logger.info(
- "Skipping %s, due to already being wheel.",
- req.name,
- )
- return False
- if need_wheel:
- # i.e. pip wheel, not pip install
- return True
- # From this point, this concerns the pip install command only
- # (need_wheel=False).
- if not req.source_dir:
- return False
- if req.editable:
- # we only build PEP 660 editable requirements
- return req.supports_pyproject_editable()
- return True
- def should_build_for_wheel_command(
- req: InstallRequirement,
- ) -> bool:
- return _should_build(req, need_wheel=True)
- def should_build_for_install_command(
- req: InstallRequirement,
- ) -> bool:
- return _should_build(req, need_wheel=False)
- def _should_cache(
- req: InstallRequirement,
- ) -> Optional[bool]:
- """
- Return whether a built InstallRequirement can be stored in the persistent
- wheel cache, assuming the wheel cache is available, and _should_build()
- has determined a wheel needs to be built.
- """
- if req.editable or not req.source_dir:
- # never cache editable requirements
- return False
- if req.link and req.link.is_vcs:
- # VCS checkout. Do not cache
- # unless it points to an immutable commit hash.
- assert not req.editable
- assert req.source_dir
- vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)
- assert vcs_backend
- if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):
- return True
- return False
- assert req.link
- base, ext = req.link.splitext()
- if _contains_egg_info(base):
- return True
- # Otherwise, do not cache.
- return False
- def _get_cache_dir(
- req: InstallRequirement,
- wheel_cache: WheelCache,
- ) -> str:
- """Return the persistent or temporary cache directory where the built
- wheel need to be stored.
- """
- cache_available = bool(wheel_cache.cache_dir)
- assert req.link
- if cache_available and _should_cache(req):
- cache_dir = wheel_cache.get_path_for_link(req.link)
- else:
- cache_dir = wheel_cache.get_ephem_path_for_link(req.link)
- return cache_dir
- def _verify_one(req: InstallRequirement, wheel_path: str) -> None:
- canonical_name = canonicalize_name(req.name or "")
- w = Wheel(os.path.basename(wheel_path))
- if canonicalize_name(w.name) != canonical_name:
- raise InvalidWheelFilename(
- "Wheel has unexpected file name: expected {!r}, "
- "got {!r}".format(canonical_name, w.name),
- )
- dist = get_wheel_distribution(FilesystemWheel(wheel_path), canonical_name)
- dist_verstr = str(dist.version)
- if canonicalize_version(dist_verstr) != canonicalize_version(w.version):
- raise InvalidWheelFilename(
- "Wheel has unexpected file name: expected {!r}, "
- "got {!r}".format(dist_verstr, w.version),
- )
- metadata_version_value = dist.metadata_version
- if metadata_version_value is None:
- raise UnsupportedWheel("Missing Metadata-Version")
- try:
- metadata_version = Version(metadata_version_value)
- except InvalidVersion:
- msg = f"Invalid Metadata-Version: {metadata_version_value}"
- raise UnsupportedWheel(msg)
- if metadata_version >= Version("1.2") and not isinstance(dist.version, Version):
- raise UnsupportedWheel(
- "Metadata 1.2 mandates PEP 440 version, "
- "but {!r} is not".format(dist_verstr)
- )
- def _build_one(
- req: InstallRequirement,
- output_dir: str,
- verify: bool,
- build_options: List[str],
- global_options: List[str],
- editable: bool,
- ) -> Optional[str]:
- """Build one wheel.
- :return: The filename of the built wheel, or None if the build failed.
- """
- artifact = "editable" if editable else "wheel"
- try:
- ensure_dir(output_dir)
- except OSError as e:
- logger.warning(
- "Building %s for %s failed: %s",
- artifact,
- req.name,
- e,
- )
- return None
- # Install build deps into temporary directory (PEP 518)
- with req.build_env:
- wheel_path = _build_one_inside_env(
- req, output_dir, build_options, global_options, editable
- )
- if wheel_path and verify:
- try:
- _verify_one(req, wheel_path)
- except (InvalidWheelFilename, UnsupportedWheel) as e:
- logger.warning("Built %s for %s is invalid: %s", artifact, req.name, e)
- return None
- return wheel_path
- def _build_one_inside_env(
- req: InstallRequirement,
- output_dir: str,
- build_options: List[str],
- global_options: List[str],
- editable: bool,
- ) -> Optional[str]:
- with TempDirectory(kind="wheel") as temp_dir:
- assert req.name
- if req.use_pep517:
- assert req.metadata_directory
- assert req.pep517_backend
- if global_options:
- logger.warning(
- "Ignoring --global-option when building %s using PEP 517", req.name
- )
- if build_options:
- logger.warning(
- "Ignoring --build-option when building %s using PEP 517", req.name
- )
- if editable:
- wheel_path = build_wheel_editable(
- name=req.name,
- backend=req.pep517_backend,
- metadata_directory=req.metadata_directory,
- tempd=temp_dir.path,
- )
- else:
- wheel_path = build_wheel_pep517(
- name=req.name,
- backend=req.pep517_backend,
- metadata_directory=req.metadata_directory,
- tempd=temp_dir.path,
- )
- else:
- wheel_path = build_wheel_legacy(
- name=req.name,
- setup_py_path=req.setup_py_path,
- source_dir=req.unpacked_source_directory,
- global_options=global_options,
- build_options=build_options,
- tempd=temp_dir.path,
- )
- if wheel_path is not None:
- wheel_name = os.path.basename(wheel_path)
- dest_path = os.path.join(output_dir, wheel_name)
- try:
- wheel_hash, length = hash_file(wheel_path)
- shutil.move(wheel_path, dest_path)
- logger.info(
- "Created wheel for %s: filename=%s size=%d sha256=%s",
- req.name,
- wheel_name,
- length,
- wheel_hash.hexdigest(),
- )
- logger.info("Stored in directory: %s", output_dir)
- return dest_path
- except Exception as e:
- logger.warning(
- "Building wheel for %s failed: %s",
- req.name,
- e,
- )
- # Ignore return, we can't do anything else useful.
- if not req.use_pep517:
- _clean_one_legacy(req, global_options)
- return None
- def _clean_one_legacy(req: InstallRequirement, global_options: List[str]) -> bool:
- clean_args = make_setuptools_clean_args(
- req.setup_py_path,
- global_options=global_options,
- )
- logger.info("Running setup.py clean for %s", req.name)
- try:
- call_subprocess(
- clean_args, command_desc="python setup.py clean", cwd=req.source_dir
- )
- return True
- except Exception:
- logger.error("Failed cleaning build dir for %s", req.name)
- return False
- def build(
- requirements: Iterable[InstallRequirement],
- wheel_cache: WheelCache,
- verify: bool,
- build_options: List[str],
- global_options: List[str],
- ) -> BuildResult:
- """Build wheels.
- :return: The list of InstallRequirement that succeeded to build and
- the list of InstallRequirement that failed to build.
- """
- if not requirements:
- return [], []
- # Build the wheels.
- logger.info(
- "Building wheels for collected packages: %s",
- ", ".join(req.name for req in requirements), # type: ignore
- )
- with indent_log():
- build_successes, build_failures = [], []
- for req in requirements:
- assert req.name
- cache_dir = _get_cache_dir(req, wheel_cache)
- wheel_file = _build_one(
- req,
- cache_dir,
- verify,
- build_options,
- global_options,
- req.editable and req.permit_editable_wheels,
- )
- if wheel_file:
- # Record the download origin in the cache
- if req.download_info is not None:
- # download_info is guaranteed to be set because when we build an
- # InstallRequirement it has been through the preparer before, but
- # let's be cautious.
- wheel_cache.record_download_origin(cache_dir, req.download_info)
- # Update the link for this.
- req.link = Link(path_to_url(wheel_file))
- req.local_file_path = req.link.file_path
- assert req.link.is_wheel
- build_successes.append(req)
- else:
- build_failures.append(req)
- # notify success/failure
- if build_successes:
- logger.info(
- "Successfully built %s",
- " ".join([req.name for req in build_successes]), # type: ignore
- )
- if build_failures:
- logger.info(
- "Failed to build %s",
- " ".join([req.name for req in build_failures]), # type: ignore
- )
- # Return a list of requirements that failed to build
- return build_successes, build_failures
|