%PDF- %PDF-
| Direktori : /lib/python3/dist-packages/nala/ |
| Current File : //lib/python3/dist-packages/nala/install.py |
# __
# ____ _____ | | _____
# / \\__ \ | | \__ \
# | | \/ __ \| |__/ __ \_
# |___| (____ /____(____ /
# \/ \/ \/
#
# Copyright (C) 2010 - 2021 Tatsuhiro Tsujikawa
# Copyright (C) 2021, 2022 Blake Lee
#
# This file is part of nala
# nala is based upon apt-metalink https://github.com/tatsuhiro-t/apt-metalink
#
# 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 the Nala Install command."""
from __future__ import annotations
import contextlib
import fnmatch
import sys
from pathlib import Path
from shutil import which
from typing import Iterable, Sequence, cast
import apt_pkg
from apt.cache import FetchFailedException, LockFailedException
from apt.package import BaseDependency, Dependency, Package
from apt_pkg import DepCache, Error as AptError, get_architectures
from nala import _, color, color_version
from nala.cache import Cache
from nala.constants import (
ARCHIVE_DIR,
DPKG_LOG,
ERROR_PREFIX,
NALA_DIR,
NALA_TERM_LOG,
NEED_RESTART,
NOTICE_PREFIX,
REBOOT_PKGS,
REBOOT_REQUIRED,
WARNING_PREFIX,
CurrentState,
)
from nala.debfile import NalaBaseDep, NalaDebPackage, NalaDep
from nala.downloader import check_pkg, download
from nala.dpkg import DpkgLive, InstallProgress, OpProgress, UpdateProgress, notice
from nala.error import (
BrokenError,
ExitCode,
apt_error,
essential_error,
local_deb_error,
print_dpkg_errors,
)
from nala.history import write_history
from nala.options import arguments
from nala.rich import Text, dpkg_progress, from_ansi
from nala.summary import print_update_summary
from nala.utils import (
DelayedKeyboardInterrupt,
NalaPackage,
PackageHandler,
ask,
dprint,
eprint,
get_date,
get_pkg_version,
pkg_installed,
term,
vprint,
)
def auto_remover(cache: Cache, nala_pkgs: PackageHandler, config: bool = False) -> None:
"""Handle auto removal of packages."""
dprint("Starting Auto Remover")
if not arguments.auto_remove and arguments.command not in (
"autoremove",
"autopurge",
):
dprint("Packages will not be autoremoved")
nala_pkgs.not_needed = [
NalaPackage(pkg.name, pkg.installed.version, pkg.installed.installed_size)
for pkg in cache
if pkg.installed and not pkg.marked_delete and pkg.is_auto_removable
]
return
dprint("Auto-Removing")
with cache.actiongroup(): # type: ignore[attr-defined]
# Recurse 10 levels if we're installing .debs to make sure that all depends are safe
deps = recurse_deps(nala_pkgs.local_debs, levels=10, installed=False)
for pkg in cache:
if pkg.is_installed and not pkg.marked_delete and pkg.is_auto_removable:
if pkg in deps:
dprint(f"Dependency: ['{pkg.name}'] Protected from removal")
continue
# We don't have to autofix while autoremoving
pkg.mark_delete(auto_fix=False, purge=arguments.is_purge())
nala_pkgs.autoremoved.add(pkg.name)
elif config and not pkg.is_installed and pkg.has_config_files:
vprint(
_("Purging configuration files for {package}").format(
package=color(pkg.name, "RED")
)
)
pkg.mark_delete(auto_fix=False, purge=arguments.is_purge())
nala_pkgs.autoremoved.add(pkg.name)
dprint(f"Pkgs marked by autoremove: {nala_pkgs.autoremoved}")
def recurse_deps(
pkgs: Iterable[NalaDebPackage] | Iterable[Package],
levels: int = 1,
installed: bool = False,
) -> set[Package]:
"""Return the installed dependency packages.
This function can be used recursively.
Example for recursing deps 2 levels, returning only those installed::
deps = installed_deps(list[Package], 2, installed=True)
Args::
pkgs (list[NalaDebPackage] | list[Package]): list of package objects.
recurse (int): How many levels to traverse dependencies. Default is 1.
installed (bool): Whether to grab dependencies that are installed or all. Default False.
"""
if not pkgs:
# Return an empty list if we didn't get packages passed.
return set()
total_deps: set[Package] = set()
for _ in range(levels):
dep_list: set[Package] = set()
for dpkg in pkgs:
dependencies = get_dep_type(dpkg, installed)
for deps in dependencies:
# deps len greater than 1 are or_deps
if len(deps) > 1:
for ndep in deps:
dep_list |= get_dep_pkgs(ndep, installed)
continue
dep_list |= get_dep_pkgs(deps[0], installed)
total_deps |= dep_list
pkgs = dep_list
dprint(
f"Recurse Levels: {levels}, Recursive List Size: {len(total_deps)}, "
f"Recurse Type: {'Installed' if installed else 'All Packages'}"
)
return total_deps
def get_dep_pkgs(
ndep: NalaBaseDep | BaseDependency, installed: bool = False
) -> set[Package]:
"""Return the packages of the specified dep that we are to use."""
target_versions = (
ndep.installed_target_versions if installed else ndep.target_versions
)
return {version.package for version in target_versions}
def get_dep_type(
dpkg: NalaDebPackage | Package, installed: bool = False
) -> list[Dependency] | list[NalaDep]:
"""Return the installed or candidate dependencies."""
if isinstance(dpkg, Package):
# We know it is installed as we check outside this function
if installed and dpkg.installed:
return dpkg.installed.dependencies
if not installed and dpkg.candidate:
return dpkg.candidate.dependencies
return cast(list[Dependency], [])
return dpkg.dependencies
def fix_excluded(protected: set[Package], is_upgrade: Iterable[Package]) -> list[str]:
"""Find and optionally fix packages that need protecting."""
eprint(
_("{notice} Selected packages cannot be excluded from upgrade safely.").format(
notice=NOTICE_PREFIX
)
)
new_pkg = set()
old_pkg = set()
for pkg in protected:
old_pkg.add(pkg.name)
if not pkg.candidate:
continue
for deps in pkg.candidate.dependencies:
for base_dep in deps:
if base_dep.target_versions:
dep_pkg = base_dep.target_versions[0].package
if (
dep_pkg in is_upgrade
and dep_pkg.marked_install
or dep_pkg.marked_upgrade
):
new_pkg.add(dep_pkg.name)
if not new_pkg:
eprint(
_(
"{error} Unable to calculate how to protect the selected packages"
).format(error=ERROR_PREFIX)
)
sys.exit(_("{error} You have held broken packages").format(error=ERROR_PREFIX))
eprint(
_("{notice} The following packages need to be protected as well:").format(
notice=NOTICE_PREFIX
)
)
eprint(
f" {' '.join(color(name, 'YELLOW') for name in sorted(new_pkg) if name not in old_pkg)}\n"
)
return sorted(new_pkg | old_pkg)
def hook_exists(key: str, pkg_names: set[str]) -> str:
"""Return True if the hook file exists on the system."""
if "*" in key and (globbed := fnmatch.filter(pkg_names, key)):
return globbed[0]
if key == "hook" or key in pkg_names:
return key
return ""
def parse_hook_args(
pkg: str, hook: dict[str, str | list[str]], cache: Cache
) -> list[str]:
"""Parse the arguments for the advanced hook."""
invalid: list[str] = []
cmd = cast(str, hook.get("hook", "")).split()
if args := cast(list[str], hook.get("args", [])):
arg_pkg = cache[pkg]
for arg in args:
# See if they are valid base package attributes
if arg in ("name", "fullname"):
cmd.append(getattr(arg_pkg, arg))
continue
# Convert simple args to candidate args
if arg in ("version", "architecture"):
arg = f"candidate.{arg}"
# Otherwise they could be a specific version argument
# arg = "candidate.arch"
if (
arg.startswith(("candidate.", "installed."))
and len(arg_split := arg.split(".")) > 1
and arg_split[1] in ("version", "architecture")
):
version = (
arg_pkg.candidate
if arg_split[0] == "candidate"
else arg_pkg.installed
)
cmd.append(getattr(version, arg_split[1]) if version else "None")
continue
# If none of these matched then the requested argument is invalid.
invalid.append(color(arg, "YELLOW"))
if invalid:
sys.exit(
_("{error} The following hook arguments are invalid: {args}").format(
error=ERROR_PREFIX, args=", ".join(invalid)
)
)
return cmd
def check_hooks(pkg_names: set[str], cache: Cache) -> None:
"""Check that the hook paths exist before trying to run anything."""
bad_hooks: dict[str, list[str]] = {
"PreInstall": [],
"PostInstall": [],
}
for hook_type, hook_list in bad_hooks.items():
for key, hook in arguments.config.get_hook(hook_type).items():
if pkg := hook_exists(key, pkg_names):
if isinstance(hook, dict):
# Print a pretty debug message for the hooks
pretty = [(f"{key} = {value},\n") for key, value in hook.items()]
dprint(
f"{hook_type} {{\n"
f"{(indent := ' ')}Key: {key}, Hook: {{\n"
f"{indent*2}{f'{indent*2}'.join(pretty)}{indent}}}\n}}"
)
cmd = parse_hook_args(pkg, hook, cache)
else:
dprint(f"{hook_type} {{ Key: {key}, Hook: {hook} }}")
cmd = hook.split()
# Check to make sure we can even run the hook
if not which(cmd[0]):
hook_list.append(color(cmd, "YELLOW"))
continue
dprint(f"Hook Command: {' '.join(cmd)}")
if hook_type == "PreInstall":
arguments.config.apt.set("DPkg::Pre-Invoke::", " ".join(cmd))
elif hook_type == "PostInstall":
arguments.config.apt.set("DPkg::Post-Invoke::", " ".join(cmd))
# If there are no bad hooks we can continue with the installation
if not bad_hooks["PreInstall"] + bad_hooks["PostInstall"]:
return
# There are bad hooks, so we should exit as to not mess anything up
for hook_type, hook_list in bad_hooks.items():
if hook_list:
eprint(
_("{error} The following {hook_type} commands cannot be found.").format(
error=ERROR_PREFIX, hook_type=hook_type
)
)
eprint(f" {', '.join(hook_list)}")
sys.exit(1)
def commit_pkgs(cache: Cache, nala_pkgs: PackageHandler) -> None:
"""Commit the package changes to the cache."""
dprint("Commit Pkgs")
task = dpkg_progress.add_task("", total=nala_pkgs.dpkg_progress_total())
check_hooks({pkg.name for pkg in nala_pkgs.all_pkgs()}, cache)
with DpkgLive(install=True) as live:
with open(DPKG_LOG, "w", encoding="utf-8") as dpkg_log:
with open(NALA_TERM_LOG, "a", encoding="utf-8") as term_log:
term_log.write(
_("Log Started: [{date}]").format(date=get_date()) + "\n"
)
if arguments.raw_dpkg:
live.stop()
config_purge = tuple(
pkg.name
for pkg in nala_pkgs.autoremove_config + nala_pkgs.delete_config
)
install = InstallProgress(dpkg_log, term_log, live, task, config_purge)
update = UpdateProgress(live)
cache.commit_pkgs(install, update)
if nala_pkgs.local_debs:
cache.commit_pkgs(install, update, nala_pkgs.local_debs)
term_log.write(
_("Log Ended: [{date}]").format(date=get_date()) + "\n\n"
)
# If we made it this far just set the total to 100%.
dpkg_progress.reset(task)
dpkg_progress.advance(task, advance=nala_pkgs.dpkg_progress_total())
live.scroll_bar(rerender=True)
def get_changes(cache: Cache, nala_pkgs: PackageHandler, operation: str) -> None:
"""Get packages requiring changes and process them."""
cache.purge_removed()
pkgs = sorted(cache.get_changes(), key=sort_pkg_name)
if not NALA_DIR.exists():
NALA_DIR.mkdir()
if operation not in ("upgrade", "remove"):
if not arguments.install_recommends:
get_extra_pkgs(
"Recommends", pkgs + nala_pkgs.local_debs, nala_pkgs.recommend_pkgs # type: ignore[operator]
)
if not arguments.install_suggests:
get_extra_pkgs(
"Suggests", pkgs + nala_pkgs.local_debs, nala_pkgs.suggest_pkgs # type: ignore[operator]
)
check_work(pkgs, nala_pkgs, operation)
if pkgs or nala_pkgs.local_debs or nala_pkgs.configure_pkgs:
check_essential(pkgs)
sort_pkg_changes(pkgs, nala_pkgs)
print_update_summary(nala_pkgs, cache)
check_term_ask()
pkgs = [
# Don't download packages that already exist
pkg
for pkg in pkgs
if not pkg.marked_delete and not check_pkg(ARCHIVE_DIR, pkg)
]
# Enable verbose and raw_dpkg if we're piped.
if not term.can_format():
arguments.verbose = True
arguments.raw_dpkg = True
# If we're in Raw_Dpkg we can restore the locale as Nala doesn't handle the output
if arguments.raw_dpkg:
term.restore_locale()
download(pkgs)
write_history(cache, nala_pkgs, operation)
start_dpkg(cache, nala_pkgs)
def start_dpkg(cache: Cache, nala_pkgs: PackageHandler) -> None:
"""Start dpkg."""
try:
# Set Use-Pty to False. This makes Sigwinch signals accepted by dpkg.
apt_pkg.config.set("Dpkg::Use-Pty", "0")
commit_pkgs(cache, nala_pkgs)
# Catch system error because if dpkg fails it'll throw this
except (apt_pkg.Error, SystemError) as error:
apt_error(error)
except FetchFailedException as error:
# We have already printed this error likely. but just in case
# We write it to the dpkg_log so at least we'll know about it.
with open(DPKG_LOG, "a", encoding="utf-8") as file:
file.write("FetchedFailedException:\n")
file.write(f"{error}")
eprint(_("{error} Fetching packages has failed!").format(error=ERROR_PREFIX))
sys.exit(1)
except KeyboardInterrupt:
eprint(_("Exiting due to SIGINT"))
sys.exit(ExitCode.SIGINT)
finally:
term.restore_mode()
# If dpkg quits for any reason we lose the cursor
if term.can_format():
term.write(term.SHOW_CURSOR)
print_dpkg_errors()
print_notices(notice)
if need_reboot():
print(_("{notice} A reboot is required.").format(notice=NOTICE_PREFIX))
print(color(_("Finished Successfully"), "GREEN"))
def install_local(nala_pkgs: PackageHandler, cache: Cache) -> None:
"""Mark the depends for local debs to be installed.
Dependencies that are marked will be marked auto installed.
Returns local_names to be printed in the transaction summary.
"""
failed: list[NalaDebPackage] = []
for pkg in nala_pkgs.local_debs[:]:
if not pkg.check(allow_downgrade=True):
failed.append(pkg)
nala_pkgs.local_debs.remove(pkg)
continue
if not check_local_version(pkg, nala_pkgs):
size = 0
with contextlib.suppress(KeyError):
size = int(pkg._sections["Installed-Size"])
nala_pkgs.install_pkgs.append(
NalaPackage(
pkg.pkgname,
pkg._sections["Version"],
size,
)
)
depends = pkg.dependencies
extra_pkgs = (
(arguments.install_recommends, pkg.get_dependencies("Recommends")),
(arguments.install_suggests, pkg.get_dependencies("Suggests")),
)
# Check to see if we need to install any recommends or suggests.
for check, extra_deps in extra_pkgs:
if not check:
continue
for dep in extra_deps:
if dep[0].name in cache:
cache[dep[0].name].mark_install(auto_fix=arguments.fix_broken)
depends.append(dep)
satisfy_notice(pkg, depends)
if failed:
BrokenError(cache, failed).broken_install()
def satisfy_notice(pkg: NalaDebPackage, depends: list[NalaDep]) -> None:
"""Print a notice of how to satisfy the packages dependencies."""
fixer: list[str] = []
for dep in depends:
fixer.extend(
color(ppkg.name, "GREEN")
for base_dep in dep
if (target := list(base_dep.target_versions))
and (ppkg := target[0].package).marked_install
)
if fixer:
print(
_("{notice} The following will be installed to satisfy {package}:").format(
notice=NOTICE_PREFIX, package=color(pkg.name, "GREEN")
)
)
print(f" {', '.join(fixer)}")
def check_local_version(pkg: NalaDebPackage, nala_pkgs: PackageHandler) -> bool:
"""Check if the version installed is better than the .deb.
Return True if we've added to a package list, False if not.
VERSION_NONE = 0
VERSION_OUTDATED = 1
VERSION_SAME = 2
VERSION_NEWER = 3
"""
if pkg_compare := pkg.compare_to_version_in_cache():
cache_pkg = pkg._cache[pkg.pkgname]
dprint(f"Comparing cache versions of: {pkg.pkgname}")
# Example filename ../scar-archive/nala_0.5.0_amd64.deb
assert pkg.filename
dprint(f"Filename: {(filename := pkg.filename.split('/')[-1])}")
dprint(f"Cache pkg: {cache_pkg.fullname}")
if pkg_compare == pkg.VERSION_SAME and cache_pkg.is_installed:
dprint(f"{filename} is the same version as the installed pkg")
nala_pkgs.reinstall_pkgs.append(
NalaPackage(
pkg.pkgname,
pkg._sections["Version"],
int(pkg._sections["Installed-Size"]),
pkg_installed(cache_pkg).version,
)
)
if (
pkg.compare_to_version_in_cache(use_installed=False)
== pkg.VERSION_OUTDATED
):
if not cache_pkg.candidate:
return True
color_name = color(cache_pkg.name, "GREEN")
print(
_(
"{notice} Newer version {package} {version} exists in the cache.\n"
"You should consider using `{command}`"
).format(
notice=NOTICE_PREFIX,
package=color_name,
version=color_version(cache_pkg.candidate.version),
command=f"{color('nala install')} {color_name}",
)
)
return True
if pkg_compare == pkg.VERSION_OUTDATED:
dprint(f"{pkg.filename} is an older version than the installed pkg")
nala_pkgs.downgrade_pkgs.append(
NalaPackage(
pkg.pkgname,
pkg._sections["Version"],
int(pkg._sections["Installed-Size"]),
pkg_installed(cache_pkg).version,
)
)
return True
if pkg_compare == pkg.VERSION_NEWER and cache_pkg.is_installed:
dprint(f"{pkg.filename} is a newer version than the installed pkg")
nala_pkgs.upgrade_pkgs.append(
NalaPackage(
pkg.pkgname,
pkg._sections["Version"],
int(pkg._sections["Installed-Size"]),
pkg_installed(cache_pkg).version,
)
)
return True
return False
def prioritize_local(
deb_pkg: NalaDebPackage, cache_name: str, pkg_names: list[str]
) -> None:
"""Print a notice of prioritization and remove the pkg name from list."""
assert deb_pkg.filename
print(
_("{notice} {deb} has taken priority over {package} from the cache.").format(
notice=NOTICE_PREFIX,
deb=color(deb_pkg.filename.split("/")[-1], "GREEN"),
package=color(cache_name, "YELLOW"),
)
)
pkg_names.remove(cache_name)
def split_local(
pkg_names: list[str], cache: Cache, local_debs: list[NalaDebPackage]
) -> list[str]:
"""Split pkg_names into either Local debs, regular install or they don't exist."""
not_exist: list[str] = []
for name in pkg_names[:]:
if ".deb" in name or "/" in name:
if not Path(name).exists():
not_exist.append(name)
pkg_names.remove(name)
continue
try:
local_debs.append(deb_pkg := NalaDebPackage(name, cache))
except AptError as error:
local_deb_error(error, name)
if deb_pkg.pkgname in pkg_names and deb_pkg.pkgname in cache:
prioritize_local(deb_pkg, deb_pkg.pkgname, pkg_names)
for arch in get_architectures():
if (
arch_pkg := f"{deb_pkg.pkgname}:{arch}"
) in pkg_names and arch_pkg in cache:
prioritize_local(deb_pkg, arch_pkg, pkg_names)
pkg_names.remove(name)
continue
return not_exist
def package_manager(pkg_names: list[str], cache: Cache, remove: bool = False) -> bool:
"""Manage installation or removal of packages."""
with cache.actiongroup(): # type: ignore[attr-defined]
for pkg_name in pkg_names:
if pkg_name in cache:
pkg = cache[pkg_name]
try:
if remove:
if pkg.installed or (
pkg.has_config_files and arguments.is_purge()
):
pkg.mark_delete(
auto_fix=arguments.fix_broken,
purge=arguments.is_purge(),
)
dprint(f"Marked Remove: {pkg.name}")
continue
if not pkg.installed or pkg.marked_downgrade:
pkg.mark_install(auto_fix=arguments.fix_broken)
dprint(f"Marked Install: {pkg.name}")
elif pkg.is_upgradable:
pkg.mark_upgrade()
dprint(f"Marked upgrade: {pkg.name}")
except AptError as error:
if (
"broken packages" not in f"{error}"
and "held packages" not in f"{error}"
):
raise error from error
return False
return True
def set_candidate_versions(
pkg_names: list[str], cache: Cache
) -> tuple[list[str], bool]:
"""Set the version to be installed."""
not_found: list[str] = []
failed = False
for name in pkg_names[:]:
if "=" not in name:
continue
pkg_name, version = name.split("=")
if pkg_name not in cache:
not_found.append(name)
pkg_names.remove(name)
continue
pkg = cache[pkg_name]
found = False
for ver in pkg.versions:
if ver.version == version:
pkg.candidate = ver
pkg_names.remove(name)
pkg_names.append(pkg_name)
found = True
continue
if found:
continue
failed = True
eprint(
_("{error} Version {version} not found for package {package}").format(
error=ERROR_PREFIX,
version=color_version(version),
package=color(pkg_name, "GREEN"),
)
)
return not_found, failed
# pylint: disable=import-outside-toplevel, cyclic-import
def check_state(cache: Cache, nala_pkgs: PackageHandler) -> None:
"""Check if pkg needs to be configured so we can show it."""
from nala.nala import _fix_broken
if cache.broken_count and arguments.fix_broken:
_fix_broken(cache)
sys.exit()
for raw_pkg in cache._cache.packages:
if raw_pkg.current_state in (
CurrentState.HALF_CONFIGURED,
CurrentState.UNPACKED,
):
pkg = cache[raw_pkg.name]
if pkg.installed:
nala_pkgs.configure_pkgs.append(
NalaPackage(
pkg.name, pkg.installed.version, pkg.installed.installed_size
)
)
def get_extra_pkgs( # pylint: disable=too-many-branches
extra_type: str,
pkgs: Sequence[Package | NalaDebPackage],
npkg_list: list[NalaPackage | list[NalaPackage]],
) -> None:
"""Get Recommended or Suggested Packages."""
dprint(f"Getting `{extra_type}` Packages")
or_name = []
for pkg in pkgs:
if isinstance(pkg, Package):
recommends: list[Dependency] | list[NalaDep]
if not pkg.marked_install or not pkg.candidate:
continue
if not (recommends := pkg.candidate.get_dependencies(extra_type)):
continue
# Then The package must be a NalaDebPackage
elif not (recommends := pkg.get_dependencies(extra_type)):
continue
for dep in recommends:
# We don't need to show this if the extra is satisfied
if dep.installed_target_versions:
continue
if len(dep) == 1:
if not dep.target_versions:
npkg_list.append(NalaPackage(dep[0].name, _("Virtual Package"), 0))
continue
ver = dep.target_versions[0]
# We don't need to show if it's to be installed
if ver.package.marked_install:
continue
npkg_list.append(NalaPackage(ver.package.name, ver.version, ver.size))
continue
or_deps = []
for base_dep in dep:
if not base_dep.target_versions:
if base_dep.name in or_name:
continue
or_name.append(base_dep.name)
or_deps.append(NalaPackage(base_dep.name, _("Virtual Package"), 0))
continue
ver = base_dep.target_versions[0]
# We don't need to show if it's to be installed
if ver.package.name in or_name or ver.package.marked_install:
continue
or_name.append(ver.package.name)
or_deps.append(NalaPackage(ver.package.name, ver.version, ver.size))
if len(or_deps) == 1:
npkg_list.extend(or_deps)
continue
if or_deps:
npkg_list.append(or_deps)
def check_broken(
pkg_names: list[str], cache: Cache, remove: bool = False
) -> tuple[list[Package], list[str], bool]:
"""Check if packages will be broken."""
broken_count = 0
broken: list[Package] = []
depcache = cache._depcache
not_found, failed = set_candidate_versions(pkg_names, cache)
with cache.actiongroup(): # type: ignore[attr-defined]
for pkg_name in pkg_names[:]:
if pkg_name not in cache:
not_found.append(pkg_name)
continue
pkg = cache[pkg_name]
mark_pkg(pkg, depcache, remove=remove)
if depcache.broken_count > broken_count and arguments.fix_broken:
broken.append(pkg)
broken_count += 1
return broken, not_found, failed
def mark_pkg(pkg: Package, depcache: DepCache, remove: bool = False) -> bool:
"""Mark Packages in depcache for broken checks."""
if remove:
if not pkg.installed and not (pkg.has_config_files and arguments.is_purge()):
eprint(
_("{notice} {package} is not installed").format(
notice=NOTICE_PREFIX, package=color(pkg.name, "YELLOW")
)
)
return False
depcache.mark_delete(pkg._pkg, arguments.is_purge())
return True
# Check the installed version against the candidate version in case we're downgrading or upgrading.
if (
pkg.installed
and pkg.candidate
and pkg.installed.version == pkg.candidate.version
):
print(
_("{package} is already at the latest version {version}").format(
package=color(pkg.name, "GREEN"),
version=color(pkg.installed.version, "BLUE"),
)
)
return False
depcache.mark_install(pkg._pkg, False, True)
return True
def sort_pkg_changes(pkgs: list[Package], nala_pkgs: PackageHandler) -> None:
"""Sort a list of packages and splits them based on the action to take."""
dprint("Sorting Package Changes")
for pkg in pkgs:
installed = get_pkg_version(pkg, inst_first=True)
if pkg.marked_delete:
delete, autoremove = (
(nala_pkgs.delete_pkgs, nala_pkgs.autoremove_pkgs)
if pkg.installed
else (nala_pkgs.delete_config, nala_pkgs.autoremove_config)
)
npkg = NalaPackage(pkg.name, installed.version, installed.installed_size)
if pkg.name in nala_pkgs.autoremoved:
autoremove.append(npkg)
continue
delete.append(npkg)
candidate = get_pkg_version(pkg, cand_first=True)
npkg = NalaPackage(pkg.name, candidate.version, candidate.size)
if pkg.marked_install:
nala_pkgs.install_pkgs.append(npkg)
continue
if pkg.marked_reinstall:
nala_pkgs.reinstall_pkgs.append(npkg)
continue
npkg = NalaPackage(
pkg.name, candidate.version, candidate.size, installed.version
)
if pkg.marked_upgrade:
nala_pkgs.upgrade_pkgs.append(npkg)
continue
if pkg.marked_downgrade:
nala_pkgs.downgrade_pkgs.append(npkg)
continue
def need_reboot() -> bool:
"""Check if the system needs a reboot and notify the user."""
if REBOOT_REQUIRED.exists():
if REBOOT_PKGS.exists():
print(
_("{notice} The following packages require a reboot.").format(
notice=NOTICE_PREFIX
)
)
for pkg in REBOOT_PKGS.read_text(encoding="utf-8").splitlines():
print(f" {color(pkg, 'GREEN')}")
return False
return True
if NEED_RESTART.exists():
return True
return False
def print_notices(notices: Iterable[str]) -> None:
"""Print notices from dpkg."""
if notices:
print("\n" + color(_("Notices:"), "YELLOW"))
for notice_msg in notices:
if "NOTICE:" in notice_msg:
notice_msg = notice_msg.replace("NOTICE:", NOTICE_PREFIX)
if "Warning:" in notice_msg:
notice_msg = notice_msg.replace("Warning:", WARNING_PREFIX)
print(f" {notice_msg}")
def setup_cache() -> Cache:
"""Update the cache if necessary, and then return the Cache."""
try:
if arguments.update:
with DelayedKeyboardInterrupt():
with DpkgLive(install=False) as live:
Cache().update(UpdateProgress(live))
except (LockFailedException, FetchFailedException, apt_pkg.Error) as err:
apt_error(err, arguments.command == "update")
except KeyboardInterrupt:
eprint(_("Exiting due to SIGINT"))
sys.exit(ExitCode.SIGINT)
except BrokenPipeError:
sys.stderr.close()
return Cache(OpProgress())
def sort_pkg_name(pkg: Package) -> str:
"""Sort by package name.
This is to be used as sorted(key=sort_pkg_name)
"""
return f"{pkg.name}"
def check_term_ask() -> None:
"""Check terminal and ask user if they want to continue."""
# If we're piped or something the user should specify --assume-yes
# As They are aware it can be dangerous to continue
if (
not term.console.is_terminal
and not arguments.assume_yes
and not arguments.assume_no
):
sys.exit(
_(
"{error} It can be dangerous to continue without a terminal. Use `--assume-yes`"
).format(error=ERROR_PREFIX)
)
if not arguments.fix_broken:
print(
_("{warning} Using {switch} can be very dangerous!").format(
warning=WARNING_PREFIX, switch=color("--no-fix-broken", "YELLOW")
)
)
if not ask(_("Do you want to continue?")):
eprint(_("Abort."))
sys.exit(0)
def check_work(pkgs: list[Package], nala_pkgs: PackageHandler, operation: str) -> None:
"""Check if there is any work for nala to do.
Returns None if there is work, exit's successful if not.
"""
if nala_pkgs.configure_pkgs:
return
if operation == "upgrade" and not pkgs:
print(color(_("All packages are up to date.")))
sys.exit(0)
elif operation == "install" and not pkgs and not nala_pkgs.local_debs:
print(color(_("Nothing for Nala to do.")))
sys.exit(0)
elif operation == "remove" and not pkgs:
print(color(_("Nothing for Nala to remove.")))
sys.exit(0)
elif operation == "fix-broken" and not pkgs:
print(color(_("Nothing for Nala to fix.")))
sys.exit(0)
def check_essential(pkgs: list[Package]) -> None:
"""Check removal of essential packages."""
dprint(f"Checking Essential: {not arguments.remove_essential}")
if arguments.remove_essential:
return
essential: list[Text] = []
for pkg in pkgs:
if pkg.is_installed:
# do not allow the removal of essential or required packages
if pkg.essential and pkg.marked_delete:
essential.append(from_ansi(color(pkg.name, "RED")))
# do not allow the removal of nala
elif pkg.shortname in "nala" and pkg.marked_delete:
essential.append(from_ansi(color("nala", "RED")))
if essential:
essential_error(essential)