python (3.11.7)

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