Skip to content

Commit

Permalink
Merge pull request #9320 from uranusjr/wheel-check-valid
Browse files Browse the repository at this point in the history
Verify built wheel contains valid metadata
  • Loading branch information
pradyunsg authored Jan 5, 2021
2 parents 47493d8 + 6902269 commit c7419b2
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 2 deletions.
3 changes: 3 additions & 0 deletions news/9206.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
``pip wheel`` now verifies the built wheel contains valid metadata, and can be
installed by a subsequent ``pip install``. This can be disabled with
``--no-verify``.
1 change: 1 addition & 0 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ def run(self, options, args):
_, build_failures = build(
reqs_to_build,
wheel_cache=wheel_cache,
verify=True,
build_options=[],
global_options=[],
)
Expand Down
9 changes: 9 additions & 0 deletions src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ def add_options(self):
self.cmd_opts.add_option(cmdoptions.build_dir())
self.cmd_opts.add_option(cmdoptions.progress_bar())

self.cmd_opts.add_option(
'--no-verify',
dest='no_verify',
action='store_true',
default=False,
help="Don't verify if built wheel is valid.",
)

self.cmd_opts.add_option(
'--global-option',
dest='global_options',
Expand Down Expand Up @@ -162,6 +170,7 @@ def run(self, options, args):
build_successes, build_failures = build(
reqs_to_build,
wheel_cache=wheel_cache,
verify=(not options.no_verify),
build_options=options.build_options or [],
global_options=options.global_options or [],
)
Expand Down
60 changes: 58 additions & 2 deletions src/pip/_internal/wheel_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@
import os.path
import re
import shutil
import zipfile

from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
from pip._vendor.packaging.version import InvalidVersion, Version
from pip._vendor.pkg_resources import Distribution

from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
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_legacy import build_wheel_legacy
from pip._internal.utils.logging import indent_log
Expand All @@ -16,6 +23,7 @@
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.utils.urls import path_to_url
from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
from pip._internal.vcs import vcs

if MYPY_CHECK_RUNNING:
Expand Down Expand Up @@ -163,9 +171,49 @@ def _always_true(_):
return True


def _get_metadata_version(dist):
# type: (Distribution) -> Optional[Version]
for line in dist.get_metadata_lines(dist.PKG_INFO):
if line.lower().startswith("metadata-version:"):
value = line.split(":", 1)[-1].strip()
try:
return Version(value)
except InvalidVersion:
msg = "Invalid Metadata-Version: {}".format(value)
raise UnsupportedWheel(msg)
raise UnsupportedWheel("Missing Metadata-Version")


def _verify_one(req, wheel_path):
# type: (InstallRequirement, str) -> None
canonical_name = canonicalize_name(req.name)
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),
)
with zipfile.ZipFile(wheel_path, allowZip64=True) as zf:
dist = pkg_resources_distribution_for_wheel(
zf, canonical_name, wheel_path,
)
if canonicalize_version(dist.version) != canonicalize_version(w.version):
raise InvalidWheelFilename(
"Wheel has unexpected file name: expected {!r}, "
"got {!r}".format(dist.version, w.version),
)
if (_get_metadata_version(dist) >= Version("1.2")
and not isinstance(dist.parsed_version, Version)):
raise UnsupportedWheel(
"Metadata 1.2 mandates PEP 440 version, "
"but {!r} is not".format(dist.version)
)


def _build_one(
req, # type: InstallRequirement
output_dir, # type: str
verify, # type: bool
build_options, # type: List[str]
global_options, # type: List[str]
):
Expand All @@ -185,9 +233,16 @@ def _build_one(

# Install build deps into temporary directory (PEP 518)
with req.build_env:
return _build_one_inside_env(
wheel_path = _build_one_inside_env(
req, output_dir, build_options, global_options
)
if wheel_path and verify:
try:
_verify_one(req, wheel_path)
except (InvalidWheelFilename, UnsupportedWheel) as e:
logger.warning("Built wheel for %s is invalid: %s", req.name, e)
return None
return wheel_path


def _build_one_inside_env(
Expand Down Expand Up @@ -260,6 +315,7 @@ def _clean_one_legacy(req, global_options):
def build(
requirements, # type: Iterable[InstallRequirement]
wheel_cache, # type: WheelCache
verify, # type: bool
build_options, # type: List[str]
global_options, # type: List[str]
):
Expand All @@ -283,7 +339,7 @@ def build(
for req in requirements:
cache_dir = _get_cache_dir(req, wheel_cache)
wheel_file = _build_one(
req, cache_dir, build_options, global_options
req, cache_dir, verify, build_options, global_options
)
if wheel_file:
# Update the link for this.
Expand Down

0 comments on commit c7419b2

Please sign in to comment.