diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py
index ebbba0a4d6c..d9a30de9175 100644
--- a/src/poetry/installation/executor.py
+++ b/src/poetry/installation/executor.py
@@ -207,6 +207,14 @@ def execute(self, operations: list[Operation]) -> int:
for warning in self._yanked_warnings:
self._io.write_error_line(f"Warning: {warning}")
+ for path, issues in self._wheel_installer.invalid_wheels.items():
+ formatted_issues = "\n".join(issues)
+ warning = (
+ f"Validation of the RECORD file of {path.name} failed."
+ " Please report to the maintainers of that package so they can fix"
+ f" their build process. Details:\n{formatted_issues}\n"
+ )
+ self._io.write_error_line(f"Warning: {warning}")
return 1 if self._shutdown else 0
diff --git a/src/poetry/installation/wheel_installer.py b/src/poetry/installation/wheel_installer.py
index ab2e0a82f3e..ad4f0fec2e7 100644
--- a/src/poetry/installation/wheel_installer.py
+++ b/src/poetry/installation/wheel_installer.py
@@ -10,6 +10,7 @@
from installer import install
from installer.destinations import SchemeDictionaryDestination
from installer.sources import WheelFile
+from installer.sources import _WheelFileValidationError
from poetry.__version__ import __version__
from poetry.utils._compat import WINDOWS
@@ -93,12 +94,17 @@ def __init__(self, env: Env) -> None:
schemes, interpreter=self._env.python, script_kind=script_kind
)
+ self.invalid_wheels: dict[Path, list[str]] = {}
+
def enable_bytecode_compilation(self, enable: bool = True) -> None:
self._destination.bytecode_optimization_levels = (-1,) if enable else ()
def install(self, wheel: Path) -> None:
with WheelFile.open(wheel) as source:
- source.validate_record()
+ try:
+ source.validate_record()
+ except _WheelFileValidationError as e:
+ self.invalid_wheels[wheel] = e.issues
install(
source=source,
destination=self._destination.for_source(source),
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 474aafab500..98412529977 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -148,9 +148,9 @@ def callback(
)
if not fixture.exists():
- if name == "demo-0.1.0.tar.gz":
- fixture = fixture_dir("distributions") / "demo-0.1.0.tar.gz"
- else:
+ fixture = fixture_dir("distributions") / name
+
+ if not fixture.exists():
fixture = (
fixture_dir("distributions") / "demo-0.1.0-py2.py3-none-any.whl"
)
@@ -337,6 +337,66 @@ def test_execute_prints_warning_for_yanked_package(
assert error.count("yanked") == 0
+def test_execute_prints_warning_for_invalid_wheels(
+ config: Config,
+ pool: RepositoryPool,
+ io: BufferedIO,
+ tmp_dir: str,
+ mock_file_downloads: None,
+ env: MockEnv,
+):
+ config.merge({"cache-dir": tmp_dir})
+
+ executor = Executor(env, pool, config, io)
+
+ base_url = "https://files.pythonhosted.org/"
+ wheel1 = "demo_invalid_record-0.1.0-py2.py3-none-any.whl"
+ wheel2 = "demo_invalid_record2-0.1.0-py2.py3-none-any.whl"
+ return_code = executor.execute(
+ [
+ Install(
+ Package(
+ "demo-invalid-record",
+ "0.1.0",
+ source_type="url",
+ source_url=f"{base_url}/{wheel1}",
+ )
+ ),
+ Install(
+ Package(
+ "demo-invalid-record2",
+ "0.1.0",
+ source_type="url",
+ source_url=f"{base_url}/{wheel2}",
+ )
+ ),
+ ]
+ )
+
+ warning1 = f"""\
+Warning: Validation of the RECORD file of {wheel1} failed.\
+ Please report to the maintainers of that package so they can fix their build process.\
+ Details:
+In .*?{wheel1}, demo/__init__.py is not mentioned in RECORD
+In .*?{wheel1}, demo_invalid_record-0.1.0.dist-info/WHEEL is not mentioned in RECORD
+"""
+
+ warning2 = f"""\
+Warning: Validation of the RECORD file of {wheel2} failed.\
+ Please report to the maintainers of that package so they can fix their build process.\
+ Details:
+In .*?{wheel2}, hash / size of demo_invalid_record2-0.1.0.dist-info/METADATA didn't\
+ match RECORD
+"""
+
+ output = io.fetch_output()
+ error = io.fetch_error()
+ assert return_code == 0, f"\noutput: {output}\nerror: {error}\n"
+ assert re.match(f"{warning1}\n{warning2}", error) or re.match(
+ f"{warning2}\n{warning1}", error
+ ), error
+
+
def test_execute_shows_skipped_operations_if_verbose(
config: Config,
pool: RepositoryPool,
diff --git a/tests/utils/test_cache.py b/tests/utils/test_cache.py
index 8cdbc93a284..a9a09fff9cb 100644
--- a/tests/utils/test_cache.py
+++ b/tests/utils/test_cache.py
@@ -228,7 +228,9 @@ def test_get_cached_archives_for_link(
)
assert archives
- assert set(archives) == set(distributions.glob("demo-0.1.*"))
+ assert set(archives) == set(distributions.glob("*.whl")) | set(
+ distributions.glob("*.tar.gz")
+ )
@pytest.mark.parametrize(