Skip to content

Commit c724645

Browse files
committed
Verify built wheel contains valid metadata
1 parent 89d50dd commit c724645

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
@@ -347,6 +347,7 @@ def run(self, options, args):
347347
_, build_failures = build(
348348
reqs_to_build,
349349
wheel_cache=wheel_cache,
350+
verify=True,
350351
build_options=[],
351352
global_options=[],
352353
)

src/pip/_internal/commands/wheel.py

+9
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ def add_options(self):
8181
self.cmd_opts.add_option(cmdoptions.build_dir())
8282
self.cmd_opts.add_option(cmdoptions.progress_bar())
8383

84+
self.cmd_opts.add_option(
85+
'--no-verify',
86+
dest='no_verify',
87+
action='store_true',
88+
default=False,
89+
help="Don't verify if built wheel is valid.",
90+
)
91+
8492
self.cmd_opts.add_option(
8593
'--global-option',
8694
dest='global_options',
@@ -166,6 +174,7 @@ def run(self, options, args):
166174
build_successes, build_failures = build(
167175
reqs_to_build,
168176
wheel_cache=wheel_cache,
177+
verify=(not options.no_verify),
169178
build_options=options.build_options or [],
170179
global_options=options.global_options or [],
171180
)

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:
@@ -160,9 +168,49 @@ def _always_true(_):
160168
return True
161169

162170

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

183231
# Install build deps into temporary directory (PEP 518)
184232
with req.build_env:
185-
return _build_one_inside_env(
233+
wheel_path = _build_one_inside_env(
186234
req, output_dir, build_options, global_options
187235
)
236+
if wheel_path and verify:
237+
try:
238+
_verify_one(req, wheel_path)
239+
except (InvalidWheelFilename, UnsupportedWheel) as e:
240+
logger.warning("Built wheel for %s is invalid: %s", req.name, e)
241+
return None
242+
return wheel_path
188243

189244

190245
def _build_one_inside_env(
@@ -257,6 +312,7 @@ def _clean_one_legacy(req, global_options):
257312
def build(
258313
requirements, # type: Iterable[InstallRequirement]
259314
wheel_cache, # type: WheelCache
315+
verify, # type: bool
260316
build_options, # type: List[str]
261317
global_options, # type: List[str]
262318
):
@@ -280,7 +336,7 @@ def build(
280336
for req in requirements:
281337
cache_dir = _get_cache_dir(req, wheel_cache)
282338
wheel_file = _build_one(
283-
req, cache_dir, build_options, global_options
339+
req, cache_dir, verify, build_options, global_options
284340
)
285341
if wheel_file:
286342
# Update the link for this.

0 commit comments

Comments
 (0)