From 9356c6519d178be0d748f8c0da5e6f8362765246 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 13 Nov 2020 12:00:08 -0300 Subject: [PATCH] [arch] fix -> not able to replace an installed package for a new one that replaces it during conflict resolutions --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 71 ++++++++++++++++++++++++------------ bauh/gems/arch/pacman.py | 65 ++++++++++++++++++++++++++++----- 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ca27f9..3f5bc053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - Arch: - search: not able to find installed packages that were renamed on the repositories (e.g: xapps -> xapp) + - not able to replace an installed package for a new one that replaces it during conflict resolutions (e.g: xapp replaces xapps) - AUR: not able to find some repository dependencies when their names are not an exact match (e.g: sc-controller [0.4.7-1] relies on "pylibacl". This dependency now is called "python-pylibacl") ## [0.9.8] 2020-10-02 diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 3d768ca2..56f9d042 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1095,15 +1095,20 @@ class ArchManager(SoftwareManager): watcher.change_substatus('') return True - def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: str, handler: ProcessHandler) -> bool: + def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: str, handler: ProcessHandler, ignore_dependencies: bool = False) -> bool: status_handler = TransactionStatusHandler(watcher=handler.watcher, i18n=self.i18n, names={*pkgs}, logger=self.logger, pkgs_to_remove=len(pkgs)) + cmd = ['pacman', '-R', *pkgs, '--noconfirm'] + + if ignore_dependencies: + cmd.append('-dd') + status_handler.start() - all_uninstalled, _ = handler.handle_simple(SimpleProcess(cmd=['pacman', '-R', *pkgs, '--noconfirm'], + all_uninstalled, _ = handler.handle_simple(SimpleProcess(cmd=cmd, root_password=root_password, error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction'}, @@ -1167,24 +1172,25 @@ class ArchManager(SoftwareManager): return True - def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneeded: bool = False, disk_loader: DiskCacheLoader = None): + def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneeded: bool = False, disk_loader: Optional[DiskCacheLoader] = None, skip_requirements: bool = False): self._update_progress(context, 10) net_available = internet.is_available() if disk_loader else True hard_requirements = set() - for n in names: - try: - pkg_reqs = pacman.list_hard_requirements(n, self.logger) + if not skip_requirements: + for n in names: + try: + pkg_reqs = pacman.list_hard_requirements(n, self.logger) - if pkg_reqs: - hard_requirements.update(pkg_reqs) - except PackageInHoldException: - context.watcher.show_message(title=self.i18n['error'].capitalize(), - body=self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(n)), - type_=MessageType.ERROR) - return False + if pkg_reqs: + hard_requirements.update(pkg_reqs) + except PackageInHoldException: + context.watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(n)), + type_=MessageType.ERROR) + return False self._update_progress(context, 25) @@ -1199,7 +1205,7 @@ class ArchManager(SoftwareManager): watcher=context.watcher): return False - if remove_unneeded: + if not skip_requirements and remove_unneeded: unnecessary_packages = pacman.list_post_uninstall_unneeded_packages(to_uninstall) self.logger.info("Checking unnecessary optdeps") @@ -1222,7 +1228,7 @@ class ArchManager(SoftwareManager): else: instances = None - uninstalled = self._uninstall_pkgs(to_uninstall, context.root_password, context.handler) + uninstalled = self._uninstall_pkgs(to_uninstall, context.root_password, context.handler, ignore_dependencies=skip_requirements) if uninstalled: if disk_loader: # loading package instances in case the uninstall succeeds @@ -1521,7 +1527,7 @@ class ArchManager(SoftwareManager): else: return self._get_history_repo_pkg(pkg) - def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: TransactionContext) -> bool: + def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: TransactionContext, skip_requirements: bool = False) -> bool: conflict_msg = '{} {} {}'.format(bold(pkg), self.i18n['and'], bold(conflicting_pkg)) if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)): @@ -1534,7 +1540,8 @@ class ArchManager(SoftwareManager): if context.removed is None: context.removed = {} - res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader, remove_unneeded=False) + res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader, + remove_unneeded=False, skip_requirements=skip_requirements) context.restabilish_progress() return res @@ -1572,16 +1579,17 @@ class ArchManager(SoftwareManager): all_provided = context.get_provided_map() - for dep, conflicts in pacman.map_conflicts_with(repo_dep_names, remote=True).items(): - if conflicts: - for c in conflicts: + for dep, data in pacman.map_conflicts_with(repo_dep_names, remote=True).items(): + if data and data['c']: + for c in data['c']: source_conflict = all_provided.get(c) if source_conflict: conflict_pkg = [*source_conflict][0] if dep != conflict_pkg: - if not self._request_conflict_resolution(dep, conflict_pkg , context): + if not self._request_conflict_resolution(dep, conflict_pkg, context, + skip_requirements=data['r'] and conflict_pkg in data['r']): return {dep} downloaded = 0 @@ -1592,7 +1600,7 @@ class ArchManager(SoftwareManager): except ArchDownloadException: return False - status_handler = TransactionStatusHandler(watcher=context.watcher, i18n=self.i18n, names=repo_dep_names, + status_handler = TransactionStatusHandler(watcher=context.watcher, i18n=self.i18n, names={*repo_dep_names}, logger=self.logger, percentage=len(repo_deps) > 1, downloading=downloaded) status_handler.start() installed, _ = context.handler.handle_simple(pacman.install_as_process(pkgpaths=repo_dep_names, @@ -2106,8 +2114,25 @@ class ArchManager(SoftwareManager): if context.removed is None: context.removed = {} + to_install_replacements = pacman.map_replaces(names_to_install) + + skip_requirement_checking = False + if to_install_replacements: # checking if the packages to be installed replace the installed packages + all_replacements = set() + + for replacements in to_install_replacements: + all_replacements.update(replacements) + + if all_replacements: + for pkg in to_uninstall: + if pkg not in all_replacements: + break + + skip_requirement_checking = True + context.disable_progress_if_changing() - if not self._uninstall(names=to_uninstall, context=context, remove_unneeded=False, disk_loader=context.disk_loader): + if not self._uninstall(names=to_uninstall, context=context, remove_unneeded=False, + disk_loader=context.disk_loader, skip_requirements=skip_requirement_checking): context.watcher.show_message(title=self.i18n['error'], body=self.i18n['arch.uninstalling.conflict.fail'].format(', '.join((bold(p) for p in to_uninstall))), type_=MessageType.ERROR) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 0f475477..34d41d98 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -960,12 +960,57 @@ def map_required_by(names: Iterable[str] = None, remote: bool = False) -> Dict[s return {} -def map_conflicts_with(names: Iterable[str], remote: bool) -> Dict[str, Set[str]]: +def map_conflicts_with(names: Iterable[str], remote: bool) -> Dict[str, Dict[str, Set[str]]]: output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(names))) if output: res = {} - latest_name, conflicts = None, None + latest_name, conflicts, replaces, field = None, None, None, None + + 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 == 'Name': + field = 'n' + val = line[field_sep_idx + 1:].strip() + latest_name = val + elif field == 'Conflicts With': + field = 'c' + val = line[field_sep_idx + 1:].strip() + conflicts = set() + if val != 'None': + conflicts.update((d for d in val.split(' ') if d)) + elif field == 'Replaces': + field = 'r' + val = line[field_sep_idx + 1:].strip() + replaces = set() + if val != 'None': + replaces.update((d for d in val.split(' ') if d)) + + elif latest_name and conflicts is not None and replaces is not None: + field = None + res[latest_name] = {'c': conflicts, 'r': replaces} + latest_name, conflicts, replaces = None, None, None + + elif latest_name and field: + if field == 'c': + conflicts.update((d for d in l.strip().split(' ') if d)) + elif field == 'r': + replaces.update((d for d in l.strip().split(' ') if d)) + + return res + + +def map_replaces(names: Iterable[str], remote: bool = False) -> Dict[str, Set[str]]: + output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(names))) + + if output: + res = {} + latest_name, replaces = None, None for l in output.split('\n'): if l: @@ -977,18 +1022,18 @@ def map_conflicts_with(names: Iterable[str], remote: bool) -> Dict[str, Set[str] if field == 'Name': val = line[field_sep_idx + 1:].strip() latest_name = val - elif field == 'Conflicts With': + elif field == 'Replaces': val = line[field_sep_idx + 1:].strip() - conflicts = set() + replaces = set() if val != 'None': - conflicts.update((d for d in val.split(' ') if d)) + replaces.update((d for d in val.split(' ') if d)) - elif latest_name and conflicts is not None: - res[latest_name] = conflicts - latest_name, conflicts = None, None + elif latest_name and replaces is not None: + res[latest_name] = replaces + latest_name, replaces = None, None, None - elif latest_name and conflicts is not None: - conflicts.update(conflicts.update((d for d in l.strip().split(' ') if d))) + elif latest_name and replaces is not None: + replaces.update((d for d in l.strip().split(' ') if d)) return res