diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 7bb1ee9a..f69f4314 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -27,8 +27,7 @@ from bauh.commons import user from bauh.commons.category import CategoriesDownloader from bauh.commons.config import save_config from bauh.commons.html import bold -from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, new_root_subprocess, \ - SimpleProcess +from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, SimpleProcess from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, message, confirmation, disk, git, \ gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ CONFIG_FILE, get_icon_path, database, mirrors, sorting, cpu_manager, ARCH_CACHE_PATH @@ -786,7 +785,9 @@ class ArchManager(SoftwareManager): return True def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: str, handler: ProcessHandler) -> bool: - res = handler.handle(SystemProcess(new_root_subprocess(['pacman', '-R', *pkgs, '--noconfirm'], root_password), wrong_error_phrase='warning:')) + all_uninstalled, _ = handler.handle_simple(SimpleProcess(cmd=['pacman', '-R', *pkgs, '--noconfirm'], + root_password=root_password, + error_phrases={'error: failed to prepare transaction'})) installed = pacman.list_installed_names() @@ -796,7 +797,7 @@ class ArchManager(SoftwareManager): if os.path.exists(cache_path): shutil.rmtree(cache_path) - return res + return all_uninstalled def _request_uninstall_confirmation(self, pkgs: Iterable[str], context: TransactionContext) -> bool: reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in pkgs] @@ -809,7 +810,8 @@ class ArchManager(SoftwareManager): body=msg, components=[reqs_select], confirmation_label=self.i18n['proceed'].capitalize(), - deny_label=self.i18n['cancel'].capitalize()): + deny_label=self.i18n['cancel'].capitalize(), + window_cancel=False): context.watcher.print("Aborted") return False @@ -834,13 +836,14 @@ class ArchManager(SoftwareManager): def _request_all_unncessary_uninstall_confirmation(self, pkgs: Iterable[str], context: TransactionContext): reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in pkgs] - reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3) + reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=1) if not context.watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), - body=self.i18n['arch.uninstall.unnecessary.all'].format(len(pkgs)), + body=self.i18n['arch.uninstall.unnecessary.all'].format(bold(str(len(pkgs)))), components=[reqs_select], confirmation_label=self.i18n['proceed'].capitalize(), - deny_label=self.i18n['cancel'].capitalize()): + deny_label=self.i18n['cancel'].capitalize(), + window_cancel=False): context.watcher.print("Aborted") return False @@ -886,27 +889,27 @@ class ArchManager(SoftwareManager): if all_deps: self.logger.info("Mapping dependencies required packages of uninstalled packages") - optdeps_reqs = pacman.map_required_by(all_deps) + alldeps_reqs = pacman.map_required_by(all_deps) no_longer_needed = set() - if optdeps_reqs: - for dep, reqs in optdeps_reqs.items(): + if alldeps_reqs: + for dep, reqs in alldeps_reqs.items(): if not reqs: no_longer_needed.add(dep) if no_longer_needed: self.logger.info("{} packages no longer needed found".format(len(no_longer_needed))) - opt_to_uninstall = self._request_unncessary_uninstall_confirmation(no_longer_needed, context) + unnecessary_to_uninstall = self._request_unncessary_uninstall_confirmation(no_longer_needed, context) - if opt_to_uninstall: - opt_required = self.deps_analyser.map_all_required_by(opt_to_uninstall, set()) - unnecessary_to_uninstall = {*opt_to_uninstall, *opt_required} + if unnecessary_to_uninstall: + unnecessary_to_uninstall_deps = pacman.list_unnecessary_deps(unnecessary_to_uninstall, all_provided) + all_unnecessary_to_uninstall = {*unnecessary_to_uninstall, *unnecessary_to_uninstall_deps} - if len(opt_to_uninstall) == len(unnecessary_to_uninstall) or self._request_all_unncessary_uninstall_confirmation(unnecessary_to_uninstall, context): - unneded_uninstalled = self._uninstall_pkgs(unnecessary_to_uninstall, context.root_password, context.handler) + if not unnecessary_to_uninstall_deps or self._request_all_unncessary_uninstall_confirmation(all_unnecessary_to_uninstall, context): + unneded_uninstalled = self._uninstall_pkgs(all_unnecessary_to_uninstall, context.root_password, context.handler) if unneded_uninstalled: - to_uninstall.update(unnecessary_to_uninstall) + to_uninstall.update(all_unnecessary_to_uninstall) else: self.logger.error("Could not uninstall some unnecessary packages") context.watcher.print("Could not uninstall some unnecessary packages") diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index ced56a8f..47f3ff05 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -901,3 +901,111 @@ def map_conflicts_with(names: Iterable[str], remote: bool) -> Dict[str, Set[str] conflicts.update(conflicts.update((d for d in l.strip().split(' ') if d))) return res + + +def _list_unnecessary_deps(pkgs: Iterable[str], already_checked: Set[str], all_provided: Dict[str, Set[str]], recursive: bool = False) -> Set[str]: + output = run_cmd('pacman -Qi {}'.format(' '.join(pkgs))) + + if output: + res = set() + deps_field = False + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + + if field == 'Depends On': + deps_field = True + val = line[field_sep_idx + 1:].strip() + + if val != 'None': + if ':' in val: + dep_info = val.split(':') + + real_deps = all_provided.get(dep_info[0].strip()) + + if real_deps: + res.update(real_deps) + else: + for dep in val.split(' '): + if dep: + real_deps = all_provided.get(dep.strip()) + + if real_deps: + res.update(real_deps) + + elif deps_field: + latest_field = False + + elif deps_field: + if ':' in l: + dep_info = l.split(':') + + real_deps = all_provided.get(dep_info[0].strip()) + + if real_deps: + res.update(real_deps) + else: + for dep in l.split(' '): + if dep: + real_deps = all_provided.get(dep.strip()) + + if real_deps: + res.update(real_deps) + + if res: + res = {dep for dep in res if dep not in already_checked} + already_checked.update(res) + + if recursive and res: + subdeps = _list_unnecessary_deps(res, already_checked, all_provided) + + if subdeps: + res.update(subdeps) + + return res + + +def list_unnecessary_deps(pkgs: Iterable[str], all_provided: Dict[str, Set[str]] = None) -> Set[str]: + all_checked = set(pkgs) + all_deps = _list_unnecessary_deps(pkgs, all_checked, map_provided(remote=False) if not all_provided else all_provided, recursive=True) + + unnecessary = set(pkgs) + if all_deps: + requirements_map = map_required_by(all_deps) + + to_clean = set() + for dep, required_by in requirements_map.items(): + if not required_by or not required_by.difference(unnecessary): + unnecessary.add(dep) + to_clean.add(dep) + elif required_by.difference(all_checked): # checking if there are requirements outside the context + to_clean.add(dep) + + if to_clean: + for dep in to_clean: + del requirements_map[dep] + + if requirements_map: + while True: + to_clean = set() + for dep, required_by in requirements_map.items(): + if not required_by.difference(unnecessary): + unnecessary.add(dep) + to_clean.add(dep) + + if to_clean: + for dep in to_clean: + del requirements_map[dep] + else: + break + + if requirements_map: # if it reaches this points it is possible to exist mutual dependent packages + for dep, required_by in requirements_map.items(): + if not required_by.difference({*requirements_map.keys(), *unnecessary}): + unnecessary.add(dep) + + return unnecessary.difference(pkgs)