Skip to content

Commit c7419b2

Browse files
authored
Merge pull request #9320 from uranusjr/wheel-check-valid
Verify built wheel contains valid metadata
2 parents 47493d8 + 6902269 commit c7419b2

File tree

4 files changed

+71
-2
lines changed

4 files changed

+71
-2
lines changed

news/9206.feature.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``pip wheel`` now verifies the built wheel contains valid metadata, and can be
2+
installed by a subsequent ``pip install``. This can be disabled with
3+
``--no-verify``.

src/pip/_internal/commands/install.py

+1
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ def run(self, options, args):
343343
_, build_failures = build(
344344
reqs_to_build,
345345
wheel_cache=wheel_cache,
346+
verify=True,
346347
build_options=[],
347348
global_options=[],
348349
)

src/pip/_internal/commands/wheel.py

+9
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ def add_options(self):
7777
self.cmd_opts.add_option(cmdoptions.build_dir())
7878
self.cmd_opts.add_option(cmdoptions.progress_bar())
7979

80+
self.cmd_opts.add_option(
81+
'--no-verify',
82+
dest='no_verify',
83+
action='store_true',
84+
default=False,
85+
help="Don't verify if built wheel is valid.",
86+
)
87+
8088
self.cmd_opts.add_option(
8189
'--global-option',
8290
dest='global_options',
@@ -162,6 +170,7 @@ def run(self, options, args):
162170
build_successes, build_failures = build(
163171
reqs_to_build,
164172
wheel_cache=wheel_cache,
173+
verify=(not options.no_verify),
165174
build_options=options.build_options or [],
166175
global_options=options.global_options or [],
167176
)

src/pip/_internal/wheel_builder.py

+58-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
import os.path
66
import re
77
import shutil
8+
import zipfile
89

10+
from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version
11+
from pip._vendor.packaging.version import InvalidVersion, Version
12+
from pip._vendor.pkg_resources import Distribution
13+
14+
from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel
915
from pip._internal.models.link import Link
16+
from pip._internal.models.wheel import Wheel
1017
from pip._internal.operations.build.wheel import build_wheel_pep517
1118
from pip._internal.operations.build.wheel_legacy import build_wheel_legacy
1219
from pip._internal.utils.logging import indent_log
@@ -16,6 +23,7 @@
1623
from pip._internal.utils.temp_dir import TempDirectory
1724
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
1825
from pip._internal.utils.urls import path_to_url
26+
from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
1927
from pip._internal.vcs import vcs
2028

2129
if MYPY_CHECK_RUNNING:
@@ -163,9 +171,49 @@ def _always_true(_):
163171
return True
164172

165173

174+
def _get_metadata_version(dist):
175+
# type: (Distribution) -> Optional[Version]
176+
for line in dist.get_metadata_lines(dist.PKG_INFO):
177+
if line.lower().startswith("metadata-version:"):
178+
value = line.split(":", 1)[-1].strip()
179+
try:
180+
return Version(value)
181+
except InvalidVersion:
182+
msg = "Invalid Metadata-Version: {}".format(value)
183+
raise UnsupportedWheel(msg)
184+
raise UnsupportedWheel("Missing Metadata-Version")
185+
186+
187+
def _verify_one(req, wheel_path):
188+
# type: (InstallRequirement, str) -> None
189+
canonical_name = canonicalize_name(req.name)
190+
w = Wheel(os.path.basename(wheel_path))
191+
if canonicalize_name(w.name) != canonical_name:
192+
raise InvalidWheelFilename(
193+
"Wheel has unexpected file name: expected {!r}, "
194+
"got {!r}".format(canonical_name, w.name),
195+
)
196+
with zipfile.ZipFile(wheel_path, allowZip64=True) as zf:
197+
dist = pkg_resources_distribution_for_wheel(
198+
zf, canonical_name, wheel_path,
199+
)
200+
if canonicalize_version(dist.version) != canonicalize_version(w.version):
201+
raise InvalidWheelFilename(
202+
"Wheel has unexpected file name: expected {!r}, "
203+
"got {!r}".format(dist.version, w.version),
204+
)
205+
if (_get_metadata_version(dist) >= Version("1.2")
206+
and not isinstance(dist.parsed_version, Version)):
207+
raise UnsupportedWheel(
208+
"Metadata 1.2 mandates PEP 440 version, "
209+
"but {!r} is not".format(dist.version)
210+
)
211+
212+
166213
def _build_one(
167214
req, # type: InstallRequirement
168215
output_dir, # type: str
216+
verify, # type: bool
169217
build_options, # type: List[str]
170218
global_options, # type: List[str]
171219
):
@@ -185,9 +233,16 @@ def _build_one(
185233

186234
# Install build deps into temporary directory (PEP 518)
187235
with req.build_env:
188-
return _build_one_inside_env(
236+
wheel_path = _build_one_inside_env(
189237
req, output_dir, build_options, global_options
190238
)
239+
if wheel_path and verify:
240+
try:
241+
_verify_one(req, wheel_path)
242+
except (InvalidWheelFilename, UnsupportedWheel) as e:
243+
logger.warning("Built wheel for %s is invalid: %s", req.name, e)
244+
return None
245+
return wheel_path
191246

192247

193248
def _build_one_inside_env(
@@ -260,6 +315,7 @@ def _clean_one_legacy(req, global_options):
260315
def build(
261316
requirements, # type: Iterable[InstallRequirement]
262317
wheel_cache, # type: WheelCache
318+
verify, # type: bool
263319
build_options, # type: List[str]
264320
global_options, # type: List[str]
265321
):
@@ -283,7 +339,7 @@ def build(
283339
for req in requirements:
284340
cache_dir = _get_cache_dir(req, wheel_cache)
285341
wheel_file = _build_one(
286-
req, cache_dir, build_options, global_options
342+
req, cache_dir, verify, build_options, global_options
287343
)
288344
if wheel_file:
289345
# Update the link for this.

0 commit comments

Comments
 (0)