%PDF- %PDF-
Direktori : /lib/python3/dist-packages/nala/ |
Current File : //lib/python3/dist-packages/nala/nala.py |
# __ # ____ _____ | | _____ # / \\__ \ | | \__ \ # | | \/ __ \| |__/ __ \_ # |___| (____ /____(____ / # \/ \/ \/ # # 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/>. """Main module for Nala which facilitates apt.""" from __future__ import annotations import re import sys from subprocess import run from typing import Generator, Optional, Pattern import apt_pkg import typer from apt.package import Package, Version from nala import _, color from nala.cache import Cache from nala.constants import ( ARCHIVE_DIR, CAT, DPKG_STATE, ERROR_PREFIX, LISTS_DIR, LISTS_PARTIAL_DIR, NALA_SOURCES, PARTIAL_DIR, PKGCACHE, SRCPKGCACHE, ) from nala.error import BrokenError, pkg_error from nala.history import get_history, get_list from nala.install import ( auto_remover, check_broken, check_state, check_term_ask, fix_excluded, get_changes, install_local, package_manager, setup_cache, split_local, ) from nala.options import ( ALL_ARCHES, ALL_VERSIONS, ASSUME_YES, AUTO_REMOVE, CONFIG, DEBUG, DOWNLOAD_ONLY, FETCH, FIX_BROKEN, FULL, INSTALLED, LISTS, MAN_HELP, NALA_INSTALLED, NAMES, OPTION, PURGE, RAW_DPKG, RECOMMENDS, REMOVE_ESSENTIAL, SUGGESTS, UPDATE, UPGRADABLE, UPGRADEABLE, VERBOSE, VIRTUAL, arguments, nala, ) from nala.rich import ELLIPSIS from nala.search import iter_search, list_match, search_name from nala.show import additional_notice, pkg_not_found, show_main from nala.utils import ( PackageHandler, ask, command_help, compile_regex, dedupe_list, eprint, get_version, iter_remove, sudo_check, vprint, ) nala_pkgs = PackageHandler() def _fix_broken(nested_cache: Cache | None = None) -> None: """Attempt to fix broken packages, if any.""" cache = nested_cache or setup_cache() print("Fixing Broken Packages...") cache.fix_broken() if nested_cache: print(color(_("There are broken packages that need to be fixed!"), "YELLOW")) print( _("You can use {switch} if you'd like to try without fixing them.").format( switch=color("--no-fix-broken", "YELLOW") ) ) else: check_state(cache, nala_pkgs) get_changes(cache, nala_pkgs, "fix-broken") def _remove(pkg_names: list[str]) -> None: sudo_check() cache = setup_cache() check_state(cache, nala_pkgs) pkg_names = cache.glob_filter(dedupe_list(pkg_names)) pkg_names = cache.virtual_filter(pkg_names, remove=True) broken, not_found, ver_failed = check_broken( pkg_names, cache, remove=True, ) for pkg_name in not_found[:]: if cache.is_any_virtual(pkg_name): not_found.remove(pkg_name) pkg_names.remove(pkg_name) if not_found or ver_failed: pkg_error(not_found, cache) nala_pkgs.user_explicit = [cache[pkg_name] for pkg_name in pkg_names] if not package_manager(pkg_names, cache, remove=True): BrokenError(cache).broken_remove(broken) auto_remover(cache, nala_pkgs) get_changes(cache, nala_pkgs, "remove") def _install(pkg_names: list[str] | None, ctx: typer.Context) -> None: sudo_check(pkg_names) if not pkg_names: if arguments.fix_broken: _fix_broken() return ctx.fail(_("{error} Missing packages to install").format(error=ERROR_PREFIX)) pkg_names = dedupe_list(pkg_names) # type: ignore[arg-type] check_state(cache := setup_cache(), nala_pkgs) not_exist = split_local(pkg_names, cache, nala_pkgs.local_debs) install_local(nala_pkgs, cache) pkg_names = cache.glob_filter(pkg_names) pkg_names = cache.virtual_filter(pkg_names) broken, not_found, ver_failed = check_broken(pkg_names, cache) not_found.extend(not_exist) if not_found or ver_failed: pkg_error(not_found, cache) nala_pkgs.user_explicit = [cache[pkg_name] for pkg_name in pkg_names] if not package_manager(pkg_names, cache): if not (error := BrokenError(cache, broken)).broken_install(): error.unmarked_error(nala_pkgs.user_explicit) auto_remover(cache, nala_pkgs) get_changes(cache, nala_pkgs, "install") def remove_completion(ctx: typer.Context) -> Generator[str, None, None]: """Complete remove command arguments.""" if not DPKG_STATE.exists(): return regex = r"ok installed|half-installed|unpacked|half-configured" if "purge" in ctx.command_path: regex += r"|config-files" status = re.compile(regex) for package in DPKG_STATE.read_text(encoding="utf-8").split("\n\n"): pkg_name = pkg_status = None for line in package.splitlines(): if len(feilds := line.split(": ")) == 1: continue if "Package" in feilds: pkg_name = feilds[1] if "Status" in feilds and status.findall(feilds[1]): pkg_status = feilds[1] if pkg_name and pkg_status: yield pkg_name def package_completion(cur: str) -> Generator[str, None, None]: """Complete install command arguments.""" yield from run( ["apt-cache", "--no-generate", "pkgnames", cur] if PKGCACHE.exists() else ["apt-cache", "pkgnames", cur], capture_output=True, check=True, text=True, ).stdout.split() @nala.command("update", help=_("Update package list.")) # pylint: disable=unused-argument def _update( debug: bool = DEBUG, raw_dpkg: bool = RAW_DPKG, dpkg_option: list[str] = OPTION, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Update package list.""" sudo_check() arguments.update = True setup_cache().print_upgradable() @nala.command(help=_("Update package list and upgrade the system.")) # pylint: disable=unused-argument,too-many-arguments, too-many-locals def upgrade( exclude: Optional[list[str]] = typer.Option( None, metavar="PKG", help=_("Specify packages to exclude during upgrade. Accepts glob*"), ), purge: bool = PURGE, debug: bool = DEBUG, raw_dpkg: bool = RAW_DPKG, download_only: bool = DOWNLOAD_ONLY, remove_essential: bool = REMOVE_ESSENTIAL, full: bool = typer.Option(True, help=_("Toggle full-upgrade")), update: bool = UPDATE, auto_remove: bool = AUTO_REMOVE, install_recommends: bool = RECOMMENDS, install_suggests: bool = SUGGESTS, fix_broken: bool = FIX_BROKEN, assume_yes: bool = ASSUME_YES, dpkg_option: list[str] = OPTION, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Update package list and upgrade the system.""" sudo_check() def _upgrade( full: bool = True, exclude: list[str] | None = None, nested_cache: Cache | None = None, ) -> None: """Upgrade pkg[s].""" cache = nested_cache or setup_cache() check_state(cache, nala_pkgs) is_upgrade = tuple(cache.upgradable_pkgs()) protected = cache.protect_upgrade_pkgs(exclude) try: cache.upgrade(dist_upgrade=full) except apt_pkg.Error: if exclude: exclude = fix_excluded(protected, is_upgrade) if ask(_("Would you like us to protect these and try again?")): cache.clear() _upgrade(full, exclude, cache) sys.exit() sys.exit( _("{error} You have held broken packages").format( error=ERROR_PREFIX ) ) BrokenError( cache, tuple(pkg for pkg in cache if pkg.is_inst_broken) ).broken_install() if kept_back := tuple( pkg for pkg in is_upgrade if not (pkg.marked_upgrade or pkg.marked_delete or pkg in protected) ): BrokenError(cache, kept_back).held_pkgs(protected) check_term_ask() auto_remover(cache, nala_pkgs) get_changes(cache, nala_pkgs, "upgrade") _upgrade(full, exclude) @nala.command(help=_("Install packages.")) # pylint: disable=unused-argument,too-many-arguments,too-many-locals def install( ctx: typer.Context, pkg_names: Optional[list[str]] = typer.Argument( None, metavar="PKGS ...", help=_("Package(s) to install"), autocompletion=package_completion, ), purge: bool = PURGE, debug: bool = DEBUG, raw_dpkg: bool = RAW_DPKG, download_only: bool = DOWNLOAD_ONLY, remove_essential: bool = REMOVE_ESSENTIAL, update: bool = UPDATE, auto_remove: bool = AUTO_REMOVE, install_recommends: bool = RECOMMENDS, install_suggests: bool = SUGGESTS, fix_broken: bool = FIX_BROKEN, assume_yes: bool = ASSUME_YES, dpkg_option: list[str] = OPTION, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Install packages.""" _install(pkg_names, ctx) @nala.command(help=_("Remove packages.")) @nala.command("purge", help=_("Purge packages.")) @nala.command("uninstall", hidden=True) # pylint: disable=unused-argument,too-many-arguments def remove( pkg_names: list[str] = typer.Argument( ..., metavar="PKGS ...", help=_("Package(s) to remove/purge"), autocompletion=remove_completion, ), purge: bool = PURGE, debug: bool = DEBUG, raw_dpkg: bool = RAW_DPKG, download_only: bool = DOWNLOAD_ONLY, remove_essential: bool = REMOVE_ESSENTIAL, update: bool = UPDATE, auto_remove: bool = AUTO_REMOVE, fix_broken: bool = FIX_BROKEN, assume_yes: bool = ASSUME_YES, dpkg_option: list[str] = OPTION, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Remove or Purge packages.""" command_help("uninstall", "remove", update) _remove(pkg_names) @nala.command("autoremove", help=_("Autoremove packages that are no longer needed.")) @nala.command("autopurge", help=_("Autopurge packages that are no longer needed.")) # pylint: disable=unused-argument,too-many-arguments def _auto_remove( purge: bool = PURGE, debug: bool = DEBUG, config: bool = CONFIG, raw_dpkg: bool = RAW_DPKG, download_only: bool = DOWNLOAD_ONLY, remove_essential: bool = REMOVE_ESSENTIAL, update: bool = UPDATE, fix_broken: bool = FIX_BROKEN, assume_yes: bool = ASSUME_YES, dpkg_option: list[str] = OPTION, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Command for autoremove.""" sudo_check() if config and not arguments.is_purge(): sys.exit( _( "{error} {config} must be used with either {autoremove} or {autopurge}." ).format( error=ERROR_PREFIX, config=color("--config", "YELLOW"), autoremove=color("autoremove --purge", "YELLOW"), autopurge=color("autopurge", "YELLOW"), ) ) cache = setup_cache() check_state(cache, nala_pkgs) auto_remover(cache, nala_pkgs, config) get_changes(cache, nala_pkgs, "remove") @nala.command(help=_("Show package details.")) @nala.command("info", hidden=True) # pylint: disable=unused-argument def show( pkg_names: list[str] = typer.Argument( ..., help=_("Package(s) to show"), autocompletion=package_completion, ), debug: bool = DEBUG, verbose: bool = VERBOSE, all_versions: bool = ALL_VERSIONS, man_help: bool = MAN_HELP, ) -> None: """Show package details.""" command_help("info", "show", None) cache = Cache() not_found: list[str] = [] pkg_names = cache.glob_filter(pkg_names, show=True) pkg_names = cache.virtual_filter(pkg_names) additional_records = 0 for num, pkg_name in enumerate(pkg_names): if pkg_name in cache: pkg = cache[pkg_name] additional_records += show_main(num, pkg) continue pkg_not_found(pkg_name, cache, not_found) if additional_records and not arguments.all_versions: additional_notice(additional_records) if not_found: for error in not_found: eprint(error) sys.exit(1) @nala.command(help=_("Search package names and descriptions.")) # pylint: disable=unused-argument,too-many-arguments,too-many-locals def search( word: str = typer.Argument( ..., help=_("Regex or word to search for"), autocompletion=package_completion, ), debug: bool = DEBUG, full: bool = FULL, names: bool = NAMES, installed: bool = INSTALLED, nala_installed: bool = NALA_INSTALLED, upgradable: bool = UPGRADABLE, upgradeable: bool = UPGRADEABLE, all_versions: bool = ALL_VERSIONS, all_arches: bool = ALL_ARCHES, virtual: bool = VIRTUAL, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Search package names and descriptions.""" cache = Cache() found: list[tuple[Package, Version]] = [] user_installed = ( get_list(get_history("Nala"), "User-Installed") if nala_installed else [] ) search_pattern: tuple[str, Pattern[str] | None] if word.startswith("g/"): search_pattern = (word, None) elif word.startswith("r/"): search_pattern = (word, compile_regex(word[2:])) else: search_pattern = (word, compile_regex(word)) arches = apt_pkg.get_architectures() for pkg in cache: if nala_installed and pkg.name not in user_installed: continue if arguments.installed and not pkg.installed: continue if arguments.upgradable and not pkg.is_upgradable: continue if arguments.virtual and not cache.is_virtual_package(pkg.name): continue if arguments.all_arches or pkg.architecture() in arches: found.extend(search_name(pkg, search_pattern)) if not found: sys.exit(_("{error} {regex} not found.").format(error=ERROR_PREFIX, regex=word)) iter_search(found) @nala.command("list", help=_("List packages based on package names.")) # pylint: disable=unused-argument,too-many-arguments,too-many-locals def list_pkgs( pkg_names: Optional[list[str]] = typer.Argument( None, help=_("Package(s) to list."), autocompletion=package_completion, ), debug: bool = DEBUG, full: bool = FULL, installed: bool = INSTALLED, nala_installed: bool = NALA_INSTALLED, upgradable: bool = UPGRADABLE, upgradeable: bool = UPGRADEABLE, all_versions: bool = ALL_VERSIONS, virtual: bool = VIRTUAL, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """List packages based on package names.""" cache = Cache() user_installed = ( get_list(get_history("Nala"), "User-Installed") if nala_installed else [] ) patterns: dict[str, Pattern[str] | None] = {} if pkg_names: for name in pkg_names: if name.startswith("r/"): # Take out the prefix when we compile the regex patterns[name] = compile_regex(name[2:]) continue if name.startswith("g/"): # We won't be using regex here. patterns[name] = None continue # Otherwise we can just compile it patterns[name] = compile_regex(name) def _list_gen() -> Generator[ tuple[Package, Version | tuple[Version, ...]], None, None ]: """Generate to speed things up.""" for pkg in cache: if nala_installed and pkg.name not in user_installed: continue if arguments.installed and not pkg.installed: continue if arguments.upgradable and not pkg.is_upgradable: continue if arguments.virtual and not cache.is_virtual_package(pkg.name): continue if pkg_names: for name, regex in patterns.items(): if list_match(pkg.fullname, name, regex): yield (pkg, get_version(pkg, inst_first=True)) continue # If names were supplied and no matches # we don't want to grab everything continue # In this case no names were supplied so we list everything yield (pkg, get_version(pkg, inst_first=True)) if not iter_search(_list_gen()): sys.exit(_("Nothing was found to list.")) @nala.command(help=_("Clear out the local archive of downloaded package files.")) # pylint: disable=unused-argument def clean( lists: bool = LISTS, fetch: bool = FETCH, debug: bool = DEBUG, verbose: bool = VERBOSE, man_help: bool = MAN_HELP, ) -> None: """Clear out the local archive of downloaded package files.""" sudo_check() if lists: iter_remove(LISTS_DIR) iter_remove(LISTS_PARTIAL_DIR) print(_("Package lists have been cleaned")) return if fetch: NALA_SOURCES.unlink(missing_ok=True) print(_("Nala sources.list has been cleaned")) return iter_remove(ARCHIVE_DIR) iter_remove(PARTIAL_DIR) iter_remove(LISTS_PARTIAL_DIR) vprint( _("Removing {cache}\nRemoving {src_cache}").format( cache=PKGCACHE, src_cache=SRCPKGCACHE ) ) PKGCACHE.unlink(missing_ok=True) SRCPKGCACHE.unlink(missing_ok=True) print(_("Cache has been cleaned")) @nala.command(hidden=True, help=_("I beg, pls moo")) # pylint: disable=unused-argument def moo( moos: Optional[list[str]] = typer.Argument(None, hidden=True), update: bool = typer.Option(None, hidden=True), ) -> None: """I beg, pls moo.""" print(CAT) can_no_moo = _("I can't moo for I'm a cat") print(f'{ELLIPSIS}"{can_no_moo}"{ELLIPSIS}') if update: what_did_you_expect = _("What did you expect to update?") print(f'{ELLIPSIS}"{what_did_you_expect}"{ELLIPSIS}') return if update is not None: what_did_you_expect = _("What did you expect no-update to do?") print(f'{ELLIPSIS}"{what_did_you_expect}"{ELLIPSIS}')