diff --git a/news/90.bugfix b/news/90.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..200585250e77cf0f3589e4ff95db09d565230b2d --- /dev/null +++ b/news/90.bugfix @@ -0,0 +1 @@ +Ensure `wheel` package is available before building packages. diff --git a/pdm/installers.py b/pdm/installers.py index 857f345e326449f845d7b68377ffd58d00802f38..525e3a36c88dd3b711e8a1deff13a06b90fc9b8a 100644 --- a/pdm/installers.py +++ b/pdm/installers.py @@ -433,7 +433,7 @@ class Synchronizer: elif not _is_dist_editable(dist) and dist.version != can.version: # XXX: An editable distribution is always considered as consistent. to_update.append(key) - elif key not in self.all_candidates: + elif key not in self.all_candidates and key != "wheel": # Remove package only if it is not required by any section to_remove.append(key) to_add = list( @@ -596,22 +596,26 @@ class Synchronizer: ) if key in self.SEQUENTIAL_PACKAGES: future.result() - # End installation - self.summarize(result) - if not any(failed.values()): - return - stream.echo(stream.red("[ERROR]", bold=True)) - if failed["add"] + failed["update"]: - stream.echo( - f"Installation failed: {', '.join(failed['add'] + failed['update'])}" - ) - if failed["remove"]: - stream.echo(f"Removal failed: {', '.join(failed['remove'])}") - for error in errors: - stream.echo( - "".join( - traceback.format_exception(type(error), error, error.__traceback__) - ), - verbosity=stream.DEBUG, - ) - raise InstallationError() + # End installation + self.summarize(result) + if not any(failed.values()): + return + stream.echo("\n") + error_msg = [] + if failed["add"] + failed["update"]: + error_msg.append( + "Installation failed: " + f"{', '.join(failed['add'] + failed['update'])}" + ) + if failed["remove"]: + error_msg.append(f"Removal failed: {', '.join(failed['remove'])}") + for error in errors: + stream.echo( + "".join( + traceback.format_exception( + type(error), error, error.__traceback__ + ) + ), + verbosity=stream.DEBUG, + ) + raise InstallationError("\n" + "\n".join(error_msg)) diff --git a/pdm/iostream.py b/pdm/iostream.py index a3c137875f15777eb81d99a5582dde35983a4eb0..3a51f3fd24d0e29f5a9175366cc64251ef280cdc 100644 --- a/pdm/iostream.py +++ b/pdm/iostream.py @@ -91,7 +91,10 @@ class IOStream: logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) - handler = logging.FileHandler(file_name, encoding="utf-8") + if self.verbosity >= self.DETAIL: + handler = logging.StreamHandler() + else: + handler = logging.FileHandler(file_name, encoding="utf-8") handler.setLevel(logging.DEBUG) logger.addHandler(handler) pip_logger = logging.getLogger("pip") @@ -100,7 +103,8 @@ class IOStream: self.logger = logger yield logger except Exception: - self.echo(self.yellow(f"See {file_name} for detailed debug log.")) + if self.verbosity < self.DETAIL: + self.echo(self.yellow(f"See {file_name} for detailed debug log.")) raise else: try: diff --git a/pdm/models/environment.py b/pdm/models/environment.py index a8eed01f658238da6be104ab3851d5db43f1b4aa..3972610af3769bd791ebc562bceddc933dec4fe9 100644 --- a/pdm/models/environment.py +++ b/pdm/models/environment.py @@ -82,6 +82,7 @@ class Environment: """ self.python_requires = project.python_requires self.project = project + self._wheel_ensured = False @cached_property def python_executable(self) -> str: @@ -307,7 +308,11 @@ class Environment: # Now all source is prepared, build it. if ireq.link.is_wheel: return (self.project.cache("wheels") / ireq.link.filename).as_posix() - builder_class = EditableBuilder if ireq.editable else WheelBuilder + if ireq.editable: + builder_class = EditableBuilder + else: + builder_class = WheelBuilder + self.ensure_wheel_package() kwargs["finder"] = finder with builder_class(ireq) as builder, self.activate(): return builder.build(**kwargs) @@ -335,6 +340,19 @@ class Environment: # Fallback to use shutil.which to find the executable return shutil.which(command, path=os.getenv("PATH")) + def ensure_wheel_package(self) -> None: + """Ensure wheel package is available and install if it isn't.""" + from pdm.installers import Installer + from pdm.models.requirements import parse_requirement + from pdm.models.candidates import Candidate + + if self._wheel_ensured or "wheel" in self.get_working_set(): + return + req = parse_requirement("wheel") + candidate = Candidate(req, self, "wheel") + Installer(self).install(candidate) + self._wheel_ensured = True + class GlobalEnvironment(Environment): """Global environment"""