%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/nala/ |
| Current File : //lib/python3/dist-packages/nala/error.py |
# __
# ____ _____ | | _____
# / \\__ \ | | \__ \
# | | \/ __ \| |__/ __ \_
# |___| (____ /____(____ /
# \/ \/ \/
#
# Copyright (C) 2021, 2022 Blake Lee
#
# This file is part of nala
#
# nala is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# nala is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with nala. If not, see <https://www.gnu.org/licenses/>.
"""Functions for Nala errors."""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Iterable, NoReturn, Union
import apt_pkg
from apt.cache import FetchFailedException, LockFailedException
from apt.package import BaseDependency, Dependency, Package, Version
from nala import ROOT, _, color, color_version
from nala.cache import Cache
from nala.constants import ERROR_PREFIX, NOTICE_PREFIX, WARNING_PREFIX
from nala.debfile import NalaBaseDep, NalaDebPackage, NalaDep
from nala.dpkg import dpkg_error, update_error
from nala.rich import Columns, Text
from nala.search import BOT_LINE, LINE, TOP_LINE
from nala.show import format_dep
from nala.utils import dprint, eprint, term
DEPENDS = color(_("Depends:"))
"""'Depends:'"""
OR_DEPENDS = color(_("Either:"))
"""'Either:'"""
BREAKS = color(_("Breaks:"))
"""'Breaks:'"""
CONFLICTS = color(_("Conflicts:"))
"""'Conflicts:'"""
SECRET_VIRTUAL = _("{package} is only referenced by name, no packages provides it")
"""'{package} is a secret virtual package, nothing provides it'"""
BREAKS_MSG = _("{dependency} will break {package} {version}")
"""'{dependency} will break {package} {version}'"""
CONFLICTS_MSG = _("{dependency} conflicts with {package} {version}")
"""'{dependency} conflicts with {package} {version}'"""
NO_PROPER_ERR = _(
"{error} python-apt gave us {apt_err} This isn't a proper error as it's empty"
)
AptErrorTypes = Union[
FetchFailedException, LockFailedException, apt_pkg.Error, SystemError
]
class ExitCode: # pylint: disable=too-few-public-methods
"""Constants for Exit Codes."""
SIGINT = 130
SIGTERM = 143
class FileDownloadError(Exception):
"""Exception class for passing errors.
ERRHASH: 1 'Hash Sum is mismatched.'
ENOENT: 2 'No such file or directory.'
ERRSIZE: 3 'Size is mismatched.'
"""
ERRHASH = 1
ENOENT = 2
ERRSIZE = 3
def __init__( # pylint: disable=too-many-arguments
self,
error_str: str = "",
errno: int = 0,
filename: str = "",
expected: str = "",
received: str = "",
) -> None:
"""Define error properties."""
super().__init__(error_str)
self.error_str = error_str
self.errno = errno
self.filename = filename
self.expected = expected
self.received = received
# Should probably refactor this in the future. For now just disable the warning.
# pylint: disable=too-many-branches
def apt_error(apt_err: AptErrorTypes, update: bool = False) -> NoReturn | None:
"""Take an error message from python-apt and formats it."""
msg = f"{apt_err}"
if not msg:
if update_error:
print()
bad_mirror = False
for line in update_error:
bad_mirror = "Connection failed" in line
eprint(line)
if bad_mirror:
eprint(
_(
"{notice} Some index files failed to download. "
"They have been ignored, or old ones used instead."
).format(notice=NOTICE_PREFIX)
)
if update:
sys.exit(1)
# Sometimes python apt gives us literally nothing to work with.
# Probably an issue with sources.list. Needs further testing.
if update:
sys.exit(NO_PROPER_ERR.format(error=ERROR_PREFIX, apt_err=repr(apt_err)))
return None
if "installArchives() failed" in msg:
eprint(_("{error} Installation has failed.").format(error=ERROR_PREFIX))
eprint(
_(
"If you'd like to file a bug report please include '{debug_file}'"
).format(debug_file=f"{ROOT}/var/log/nala/dpkg-debug.log")
)
sys.exit(1)
if "," in msg:
err_list = set(msg.split(","))
for err in err_list:
if "E:" in err:
eprint(f"{ERROR_PREFIX} {err.replace('E:', '').strip()}")
continue
if "W:" in err:
eprint(f"{WARNING_PREFIX} {err.replace('W:', '').strip()}")
continue
if update:
sys.exit(1)
return None
eprint(f"{ERROR_PREFIX} {msg.replace('E:', '').strip()}")
if not term.is_su():
sys.exit(_("Are you root?"))
if update:
sys.exit(1)
return None
def essential_error(pkg_list: list[Text]) -> NoReturn:
"""Print error message for essential packages and exit."""
print("=" * term.columns)
print(_("{error} The following packages are essential!").format(error=ERROR_PREFIX))
print("=" * term.columns)
term.console.print(Columns(pkg_list, padding=(0, 2), equal=True))
print("=" * term.columns)
eprint(
_("{error} You have attempted to remove essential packages").format(
error=ERROR_PREFIX
)
)
eprint(
_("{error} Please use {switch} if you are sure you want to.").format(
error=ERROR_PREFIX, switch=color("--remove-essential", "YELLOW")
)
)
sys.exit(1)
def pkg_error(pkg_list: list[str], cache: Cache) -> NoReturn:
"""Print error for package in list."""
for pkg_name in pkg_list:
if cache.is_any_virtual(pkg_name):
eprint(
_("{error} {package} has no installation candidate.").format(
error=ERROR_PREFIX, package=color(pkg_name, "YELLOW")
)
)
continue
eprint(
_("{error} {package} not found").format(
error=ERROR_PREFIX, package=color(pkg_name, "YELLOW")
)
)
sys.exit(1)
def print_dpkg_errors() -> None:
"""Format and print dpkg errors if there are any."""
if not dpkg_error:
return
# for line in dedupe_list(dpkg_error):
for line in dpkg_error:
if "dpkg:" in line:
line = line.replace("dpkg:", "")
if "warning:" in line:
line = line.replace("warning:", "")
if "downgrading" in line:
line = line.replace("downgrading", "Downgraded")
eprint(f"\n{WARNING_PREFIX} {line.strip()}")
continue
eprint(f"\n{ERROR_PREFIX} {line.strip()}")
continue
if "Errors were encountered" in line or "Processing was halted" in line:
eprint(f"\n{line}")
continue
eprint(line)
def local_deb_error(error: apt_pkg.Error, name: str) -> NoReturn:
"""Print what is wrong with the .deb and exit."""
msg = f"{error}"
if "Invalid archive signature" in msg:
eprint(
_("{error} {apt_error}\n Unsupported File: {filename}").format(
error=ERROR_PREFIX,
apt_error=msg.replace("E:", "").strip(),
filename=Path(name).resolve(),
)
)
sys.exit(1)
eprint(
_("{error} {apt_error}\n Could not read meta data from {filename}").format(
error=ERROR_PREFIX, apt_error=msg, filename=Path(name).resolve()
)
)
sys.exit(1)
class BrokenError:
"""Calculate and print broken dependencies."""
def __init__(
self,
cache: Cache,
broken_list: Iterable[Package] | list[NalaDebPackage] | None = None,
) -> None:
"""Calculate and print broken install dependencies."""
self.cache = cache
self.broken_list = broken_list
self.provides = (
{
ppkg
for pkg in broken_list
if isinstance(pkg, Package) and pkg.candidate
for ppkg in pkg.candidate.provides
}
if broken_list
else set()
)
def broken_install(self) -> int | NoReturn:
"""Handle printing of errors due to broken packages."""
# We have to clear the changes from the cache
# before we can calculate why the packages are broken.
if not self.broken_list:
return 0
self.cache.clear()
ret_count = sum(self.broken_pkg(pkg) for pkg in self.broken_list)
if not ret_count:
return ret_count
self._print_held_error()
def broken_remove(self, broken_list: list[Package]) -> NoReturn:
"""Calculate and print broken remove dependencies."""
installed = tuple(
pkg for pkg in self.cache if pkg.installed and pkg.installed.dependencies
)
self.cache.clear()
for pkg in (
pkg.name
for pkg in broken_list
if pkg.name in self._installed_dep_names(installed)
):
self._print_rdeps(pkg, installed)
self._print_held_error()
def held_pkgs(self, protected: set[Package]) -> None:
"""Print packages that have been held back."""
if not self.broken_list:
# There is really not a chance of this happening
return
print(color(_("The following packages were kept back:"), "YELLOW"))
if undetermined := [
pkg for pkg in self.broken_list if not self.broken_pkg(pkg)
]:
print(
color(
_("The following were held due to exclusions:")
if protected
else _("Nala was unable to determine why these were held:")
)
)
print(f" {', '.join(color(pkg.name, 'YELLOW') for pkg in undetermined)}")
def broken_pkg(self, pkg: Package | NalaDebPackage) -> int:
"""Calculate and print broken Dependencies."""
ret_count = 0
version: NalaDebPackage | Version | None
if isinstance(pkg, NalaDebPackage):
version = pkg
elif not (version := pkg.candidate):
# We do this in case a broken package is locally installed.
if not pkg.installed:
return ret_count
version = pkg.installed
tree: list[str] = []
dep_tree: list[str] = []
arch = self._arch(pkg.name)
indent = " "
if formatted_break := self.breaks_conflicts(pkg.name, version, arch):
indent = LINE
for dep in version.dependencies:
dep_tree.extend(self._dep_tree(dep, arch, indent))
if dep_tree:
dep_tree.insert(0, f"{TOP_LINE if formatted_break else BOT_LINE} {DEPENDS}")
dep_tree.append(dep_tree.pop().replace(TOP_LINE, BOT_LINE))
tree.extend(dep_tree)
tree.extend(f"{TOP_LINE} {_break}" for _break in formatted_break)
if tree:
tree.append(tree.pop().replace(TOP_LINE, BOT_LINE))
tree.insert(0, color(pkg.name, "GREEN"))
print("\n".join(tree), end="\n\n")
ret_count += 1
return ret_count
def _dep_tree(
self,
dep: NalaDep | Dependency,
arch: str,
indent: str,
) -> list[str]:
dep_tree: list[str] = []
if len(dep) > 1:
count = 0
or_tree = []
for base_dep in dep:
if formatted := self.format_broken(base_dep, arch):
count += 1
or_tree.append(formatted)
if count == len(dep):
start = f"{indent}{color(TOP_LINE, 'MAGENTA')}"
for num, msg in enumerate(or_tree):
if not num:
dep_tree.append(f"{start} {msg}")
continue
dep_tree.append(f"{start} or {msg}")
elif formatted := self.format_broken(dep[0], arch):
dep_tree.append(f"{indent}{TOP_LINE} {formatted}")
return dep_tree
def format_broken(self, dep: BaseDependency | NalaBaseDep, arch: str = "") -> str:
"""Format broken dependencies into a Tree, if any."""
formatted_dep = format_dep(dep, 0).strip()
dep_name = dep.name
if arch and ":any" not in dep_name:
dep_name = f"{dep_name}:{arch}"
formatted_dep = formatted_dep.replace(dep.name, dep_name)
# We print nothing on a virtual package
if self.cache.is_virtual_package(dep_name):
return ""
if self.cache.is_secret_virtual(dep_name):
return SECRET_VIRTUAL.format(package=formatted_dep)
if dep_name not in self.cache:
return _("{package} but it isn't in the cache").format(
package=formatted_dep
)
# This means that the dependency doesn't have the right version in the cache
if (
dep.version
and not dep.target_versions
and not dep.installed_target_versions
):
dep_pkg = self.cache[dep.name]
if (candidate := dep_pkg.candidate) and not apt_pkg.check_dep(
candidate.version, dep.relation_deb, dep.version
):
return _("{package} but the cache version is {version}").format(
package=formatted_dep,
version=color_version(candidate.version),
)
# If none of our conditions are met we just fall back to a general error
return _("{package} but it cannont be installed").format(
package=formatted_dep
)
return ""
def breaks_conflicts(
self,
pkg_name: str,
version: Version | NalaDebPackage,
arch: str,
) -> tuple[str, ...]:
"""Generate tree objects for breaks and conflict type deps."""
break_conflict: list[str] = []
for dep_type in ("Breaks", "Conflicts"):
if deps := version.get_dependencies(dep_type):
dprint(f"{pkg_name} {dep_type}:\n{deps}")
break_conflict.extend(
self.format_broken_conflict(
deps,
BREAKS_MSG if dep_type == "Breaks" else CONFLICTS_MSG,
arch,
)
)
return tuple(break_conflict)
def format_broken_conflict(
self,
breaks: list[Dependency] | list[NalaDep],
dep_string: str,
arch: str = "",
) -> list[str]:
"""Format broken conflict/breaks dependency into a Tree."""
break_tree: list[str] = []
for dep in breaks:
if not (target_versions := dep.target_versions):
continue
if installed_versions := tuple(
ver for ver in target_versions if ver.is_installed
):
break_tree.append(
dep_string.format(
dependency=self._dependency_name(dep, arch),
package=color(
self._break_pkg_name(installed_versions[0], arch), "GREEN"
),
version=color_version(installed_versions[0].version),
)
)
if not (target_provides := target_versions[0].provides):
continue
if conflict := self.format_conflict(dep, target_provides, dep_string):
break_tree.append(conflict)
return break_tree
def format_conflict(
self,
dep: Dependency | NalaDep,
target_provides: list[str],
dep_string: str,
) -> str | None:
"""Format a conflicting package that isn't installed."""
return (
next(
(
dep_string.format(
dependency=format_dep(dep[0]).strip(),
package=color(provide_name, "GREEN"),
version="",
)
for provide_name in target_provides
if dep[0].name != provide_name and provide_name in self.provides
),
None,
)
if self.provides
else None
)
@staticmethod
def _break_pkg_name(version: Version, arch: str) -> str:
"""Get the name of the package that is broken."""
return (
f"{version.package.name}:{version.architecture}"
if arch
else version.package.name
)
@staticmethod
def _dependency_name(dep: Dependency | NalaDep, arch: str) -> str:
"""Get the formatted dependency name."""
return (
format_dep(dep[0]).replace(dep[0].name, f"{dep[0].name}:{arch}")
if arch
else format_dep(dep[0])
).strip()
@staticmethod
def _arch(pkg_name: str) -> str:
return (
pkg_name.split(":")[1]
if ":" in pkg_name
and all(substring not in pkg_name for substring in (":all", ":any"))
else ""
)
@staticmethod
def unmarked_error(pkgs: list[Package]) -> None:
"""Print error messages related to the fixer unmarking packages requested for install."""
for pkg in pkgs:
if not pkg.marked_upgrade or pkg.marked_install:
print(
_("{package} has been unmarked.").format(
package=color(pkg.name, "GREEN"),
)
)
print(
_("Try {switch} if you're sure they can be installed.").format(
switch=color("--no-fix-broken", "YELLOW")
)
)
sys.exit(
_("{error} Some packages were unable to be installed.").format(
error=ERROR_PREFIX
)
)
@staticmethod
def _installed_dep_names(installed_pkgs: tuple[Package, ...]) -> tuple[str, ...]:
"""Iterate installed pkgs and return all of their deps in a tuple.
This is so we can reduce iterations when checking reverse depends.
"""
total_deps = set()
for pkg in installed_pkgs:
if not (pkg_installed := pkg.installed):
continue
for deps in pkg_installed.dependencies:
for dep in deps:
total_deps.add(dep.name)
return tuple(total_deps)
@staticmethod
def _print_rdeps(name: str, installed_pkgs: tuple[Package, ...]) -> int:
"""Print the installed reverse depends of a package."""
msg = color(
_("Installed packages that depend on {package}").format(
package=color(name, "GREEN")
)
+ "\n",
"YELLOW",
)
for pkg in installed_pkgs:
if not (pkg_installed := pkg.installed):
continue
for dep in pkg_installed.dependencies:
if name in dep.rawstr:
dep_msg = f" {color(pkg.name, 'GREEN')}"
if pkg.essential:
dep_msg = _("{package} is an Essential package!").format(
package=dep_msg
)
msg += f"{dep_msg}\n"
break
print(msg.strip())
return 1
@staticmethod
def _print_held_error() -> NoReturn:
"""Print the held broken error and exit."""
eprint(
_("{notice} The information above may be able to help").format(
notice=NOTICE_PREFIX
)
)
sys.exit(_("{error} You have held broken packages").format(error=ERROR_PREFIX))