diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py
index 6aae9bc5072..08c7a7918b6 100644
--- a/src/poetry/inspection/info.py
+++ b/src/poetry/inspection/info.py
@@ -12,7 +12,6 @@
import pkginfo
-from build import BuildBackendException
from poetry.core.constraints.version import Version
from poetry.core.factory import Factory
from poetry.core.packages.dependency import Dependency
@@ -24,6 +23,7 @@
from poetry.core.version.requirements import InvalidRequirementError
from poetry.utils.helpers import extractall
+from poetry.utils.isolated_build import IsolatedBuildBackendError
from poetry.utils.isolated_build import isolated_builder
@@ -540,9 +540,8 @@ def get_pep517_metadata(path: Path) -> PackageInfo:
builder.metadata_path(dest)
info = PackageInfo.from_metadata_directory(dest)
- except BuildBackendException as e:
- logger.debug("PEP517 build failed: %s", e)
- raise PackageInfoError(path, e, "PEP517 build failed")
+ except IsolatedBuildBackendError as e:
+ raise PackageInfoError(path, str(e)) from None
if info:
return info
diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py
index 7341678a9df..17cfb82d187 100644
--- a/src/poetry/installation/chef.py
+++ b/src/poetry/installation/chef.py
@@ -5,12 +5,9 @@
from pathlib import Path
from typing import TYPE_CHECKING
-from build import BuildBackendException
from poetry.core.utils.helpers import temporary_directory
-from poetry.utils._compat import decode
from poetry.utils.helpers import extractall
-from poetry.utils.isolated_build import IsolatedBuildError
from poetry.utils.isolated_build import isolated_builder
@@ -48,38 +45,19 @@ def prepare(
def _prepare(
self, directory: Path, destination: Path, *, editable: bool = False
) -> Path:
- from subprocess import CalledProcessError
-
distribution: DistributionType = "editable" if editable else "wheel"
- error: Exception | None = None
-
- try:
- with isolated_builder(
- source=directory,
- distribution=distribution,
- python_executable=self._env.python,
- pool=self._pool,
- ) as builder:
- return Path(
- builder.build(
- distribution,
- destination.as_posix(),
- )
+ with isolated_builder(
+ source=directory,
+ distribution=distribution,
+ python_executable=self._env.python,
+ pool=self._pool,
+ ) as builder:
+ return Path(
+ builder.build(
+ distribution,
+ destination.as_posix(),
)
- except BuildBackendException as e:
- message_parts = [str(e)]
-
- if isinstance(e.exception, CalledProcessError):
- text = e.exception.stderr or e.exception.stdout
- if text is not None:
- message_parts.append(decode(text))
- else:
- message_parts.append(str(e.exception))
-
- error = IsolatedBuildError("\n\n".join(message_parts))
-
- if error is not None:
- raise error from None
+ )
def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path:
from poetry.core.packages.utils.link import Link
diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py
index 4423911eb40..8e4ff0f31d4 100644
--- a/src/poetry/installation/executor.py
+++ b/src/poetry/installation/executor.py
@@ -31,7 +31,7 @@
from poetry.utils.helpers import get_highest_priority_hash_type
from poetry.utils.helpers import pluralize
from poetry.utils.helpers import remove_directory
-from poetry.utils.isolated_build import IsolatedBuildError
+from poetry.utils.isolated_build import IsolatedBuildBackendError
from poetry.utils.isolated_build import IsolatedBuildInstallError
from poetry.vcs.git import Git
@@ -300,10 +300,13 @@ def _execute_operation(self, operation: Operation) -> None:
io = self._sections.get(id(operation), self._io)
with self._lock:
- trace = ExceptionTrace(e)
- trace.render(io)
pkg = operation.package
- if isinstance(e, IsolatedBuildError):
+ with_trace = True
+
+ if isinstance(e, IsolatedBuildBackendError):
+ # TODO: Revisit once upstream fix is available https://github.com/python-poetry/cleo/issues/454
+ # we disable trace here explicitly to workaround incorrect context detection by crashtest
+ with_trace = False
pip_command = "pip wheel --no-cache-dir --use-pep517"
if pkg.develop:
requirement = pkg.source_url
@@ -312,14 +315,9 @@ def _execute_operation(self, operation: Operation) -> None:
requirement = (
pkg.to_dependency().to_pep_508().split(";")[0].strip()
)
- message = (
- ""
- "Note: This error originates from the build backend,"
- " and is likely not a problem with poetry"
- f" but with {pkg.pretty_name} ({pkg.full_pretty_version})"
- " not supporting PEP 517 builds. You can verify this by"
- f" running '{pip_command} \"{requirement}\"'."
- ""
+ message = e.generate_message(
+ source_string=f"{pkg.pretty_name} ({pkg.full_pretty_version})",
+ build_command=f'{pip_command} "{requirement}"',
)
elif isinstance(e, IsolatedBuildInstallError):
message = (
@@ -338,6 +336,9 @@ def _execute_operation(self, operation: Operation) -> None:
else:
message = f"Cannot install {pkg.pretty_name}."
+ if with_trace:
+ ExceptionTrace(e).render(io)
+
io.write_line("")
io.write_line(message)
io.write_line("")
diff --git a/src/poetry/utils/isolated_build.py b/src/poetry/utils/isolated_build.py
index abbd468f991..803360df183 100644
--- a/src/poetry/utils/isolated_build.py
+++ b/src/poetry/utils/isolated_build.py
@@ -1,14 +1,17 @@
from __future__ import annotations
import os
+import subprocess
from contextlib import contextmanager
from contextlib import redirect_stdout
from io import StringIO
from typing import TYPE_CHECKING
+from build import BuildBackendException
from build.env import IsolatedEnv as BaseIsolatedEnv
+from poetry.utils._compat import decode
from poetry.utils.env import Env
from poetry.utils.env import EnvManager
from poetry.utils.env import ephemeral_environment
@@ -28,7 +31,47 @@
class IsolatedBuildBaseError(Exception): ...
-class IsolatedBuildError(IsolatedBuildBaseError): ...
+class IsolatedBuildBackendError(IsolatedBuildBaseError):
+ def __init__(self, source: Path, exception: BuildBackendException) -> None:
+ super().__init__()
+ self.source = source
+ self.exception = exception
+
+ def generate_message(
+ self, source_string: str | None = None, build_command: str | None = None
+ ) -> str:
+ e = self.exception.exception
+ source_string = source_string or self.source.as_posix()
+ build_command = (
+ build_command
+ or f'pip wheel --no-cache-dir --use-pep517 "{self.source.as_posix()}"'
+ )
+
+ reasons = ["PEP517 build of a dependency failed", str(self.exception)]
+
+ if isinstance(e, subprocess.CalledProcessError):
+ inner_traceback = decode(e.stderr or e.stdout or e.output).strip()
+ inner_reason = "\n | ".join(
+ ["", str(e), "", *inner_traceback.split("\n")]
+ ).lstrip("\n")
+ reasons.append(f"{inner_reason}")
+
+ reasons.append(
+ ""
+ "Note:> This error originates from the build backend, and is likely not a "
+ f"problem with poetry but one of the following issues with {source_string}\n\n"
+ " - not supporting PEP 517 builds\n"
+ " - not specifying PEP 517 build requirements correctly\n"
+ " - the build requirements are incompatible with your operating system or Python version\n"
+ " - the build requirements are missing system dependencies (eg: compilers, libraries, headers).\n\n"
+ f"You can verify this by running {build_command}."
+ ""
+ )
+
+ return "\n\n".join(reasons)
+
+ def __str__(self) -> str:
+ return self.generate_message()
class IsolatedBuildInstallError(IsolatedBuildBaseError):
@@ -140,18 +183,20 @@ def isolated_builder(
) as venv:
env = IsolatedEnv(venv, pool)
stdout = StringIO()
+ try:
+ builder = ProjectBuilder.from_isolated_env(
+ env, source, runner=quiet_subprocess_runner
+ )
- builder = ProjectBuilder.from_isolated_env(
- env, source, runner=quiet_subprocess_runner
- )
-
- with redirect_stdout(stdout):
- env.install(builder.build_system_requires)
+ with redirect_stdout(stdout):
+ env.install(builder.build_system_requires)
- # we repeat the build system requirements to avoid poetry installer from removing them
- env.install(
- builder.build_system_requires
- | builder.get_requires_for_build(distribution)
- )
+ # we repeat the build system requirements to avoid poetry installer from removing them
+ env.install(
+ builder.build_system_requires
+ | builder.get_requires_for_build(distribution)
+ )
- yield builder
+ yield builder
+ except BuildBackendException as e:
+ raise IsolatedBuildBackendError(source, e) from None
diff --git a/tests/inspection/test_info.py b/tests/inspection/test_info.py
index 62c1f77732a..776cafa60d1 100644
--- a/tests/inspection/test_info.py
+++ b/tests/inspection/test_info.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import shutil
+import uuid
from subprocess import CalledProcessError
from typing import TYPE_CHECKING
@@ -331,15 +332,24 @@ def test_info_setup_complex(demo_setup_complex: Path) -> None:
def test_info_setup_complex_pep517_error(
mocker: MockerFixture, demo_setup_complex: Path
) -> None:
+ output = uuid.uuid4().hex
mocker.patch(
"build.ProjectBuilder.from_isolated_env",
autospec=True,
- side_effect=BuildBackendException(CalledProcessError(1, "mock", output="mock")),
+ side_effect=BuildBackendException(CalledProcessError(1, "mock", output=output)),
)
- with pytest.raises(PackageInfoError):
+ with pytest.raises(PackageInfoError) as exc:
PackageInfo.from_directory(demo_setup_complex)
+ text = str(exc.value)
+ assert "Command 'mock' returned non-zero exit status 1." in text
+ assert output in text
+ assert (
+ "This error originates from the build backend, and is likely not a problem with poetry"
+ in text
+ )
+
def test_info_setup_complex_pep517_legacy(
demo_setup_complex_pep517_legacy: Path,
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index b9905d6af70..a7bff526612 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -1292,18 +1292,6 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess(
assert return_code == 1
package_url = directory_package.source_url
- expected_start = f"""
-Package operations: 1 install, 0 updates, 0 removals
-
- - Installing {package_name} ({package_version} {package_url})
-
- IsolatedBuildError
-
- hide the original error
- \
-
- original error
-"""
assert directory_package.source_url is not None
if editable:
@@ -1313,16 +1301,41 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess(
else:
pip_command = "pip wheel --no-cache-dir --use-pep517"
requirement = f"{package_name} @ {path_to_url(directory_package.source_url)}"
- expected_end = f"""
-Note: This error originates from the build backend, and is likely not a problem with \
-poetry but with {package_name} ({package_version} {package_url}) not supporting \
-PEP 517 builds. You can verify this by running '{pip_command} "{requirement}"'.
+ expected_source_string = f"{package_name} ({package_version} {package_url})"
+ expected_pip_command = f'{pip_command} "{requirement}"'
+
+ expected_output = f"""
+Package operations: 1 install, 0 updates, 0 removals
+
+ - Installing {package_name} ({package_version} {package_url})
+
+PEP517 build of a dependency failed
+
+hide the original error
"""
- output = io.fetch_output()
- assert output.startswith(expected_start)
- assert output.endswith(expected_end)
+ if isinstance(exception, CalledProcessError):
+ expected_output += (
+ "\n | Command '['pip']' returned non-zero exit status 1."
+ "\n | "
+ "\n | original error"
+ "\n"
+ )
+
+ expected_output += f"""
+Note: This error originates from the build backend, and is likely not a problem with poetry but one of the following issues with {expected_source_string}
+
+ - not supporting PEP 517 builds
+ - not specifying PEP 517 build requirements correctly
+ - the build requirements are incompatible with your operating system or Python version
+ - the build requirements are missing system dependencies (eg: compilers, libraries, headers).
+
+You can verify this by running {expected_pip_command}.
+
+"""
+
+ assert io.fetch_output() == expected_output
@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"])