Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify built wheel contains valid metadata #9320

Merged
merged 1 commit into from
Jan 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -347,6 +347,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 @@ -81,6 +81,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 @@ -166,6 +174,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 @@ -160,9 +168,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}, "
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unexpected file version no ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The technically correct expression is “the version field in the file name” but that’s probably too long and not comprehensible to typical users.

"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 @@ -182,9 +230,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 @@ -257,6 +312,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 @@ -280,7 +336,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