%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)