From a19174812495be3e0b4733093b12005bc57cc204 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 19 May 2022 16:15:34 -0300 Subject: [PATCH] [arch] fix: conflict resolution: removing hard dependencies that would be satisfied with the inclusion of the new package --- CHANGELOG.md | 5 ++ bauh/gems/arch/aur.py | 4 +- bauh/gems/arch/controller.py | 107 ++++++++++++++++++++++++----------- bauh/gems/arch/pacman.py | 14 ++++- 4 files changed, 92 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0642ac8d..61fd00c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - version: the limit for displaying both the installed and latest versions is 22% (otherwise just the latest version will be displayed) ### Fixes +- Arch + - conflict resolution: removing hard dependencies that would be satisfied with the inclusion of the new package [#268](https://github.com/vinifmor/bauh/issues/268) + - e.g: `pipewire-pulse` conflicts with `pulseaudio`. `pulseaudio-alsa` (a dependency of pulseaudio) should not be removed, since `pipewire-pulse` provides `pulseaudio` + - Flatpak - not all selected runtime partials to upgrade are actually requested to be upgraded + ## [0.10.2] 2022-04-16 ### Improvements - Arch diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 24cab409..fa1b02de 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -294,7 +294,7 @@ class AURClient: provided.add(pkgname) if info: - provided.add('{}={}'.format(pkgname, info['pkgver'])) + provided.add(f"{pkgname}={info['pkgver']}") if info.get('provides'): provided.update(info.get('provides')) @@ -303,7 +303,7 @@ class AURClient: 'b': info.get('pkgbase', pkgname)} else: if latest_version: - provided.add('{}={}'.format(pkgname, latest_version)) + provided.add(f'{pkgname}={latest_version}') return {'c': None, 's': None, 'p': provided, 'r': 'aur', 'v': latest_version, 'd': set(), 'b': pkgname} diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 231ba3a8..b175cae4 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -8,7 +8,6 @@ import tarfile import time import traceback from datetime import datetime -from math import floor from pathlib import Path from pwd import getpwnam from threading import Thread @@ -21,8 +20,8 @@ from bauh.api.abstract.controller import SearchResult, SoftwareManager, Applicat TransactionResult, SoftwareAction, SettingsView, SettingsController from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher, TaskManager -from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ - SuggestionPriority, CustomSoftwareAction +from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageStatus, \ + CustomSoftwareAction from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextInputType, \ FileChooserComponent, TextComponent @@ -1194,7 +1193,10 @@ class ArchManager(SoftwareManager, SettingsController): watcher.change_substatus('') return True - def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: Optional[str], handler: ProcessHandler, ignore_dependencies: bool = False) -> bool: + def _uninstall_pkgs(self, pkgs: Collection[str], root_password: Optional[str], + handler: ProcessHandler, ignore_dependencies: bool = False, + replacers: Optional[Set[str]] = None) -> bool: + status_handler = TransactionStatusHandler(watcher=handler.watcher, i18n=self.i18n, names={*pkgs}, @@ -1206,6 +1208,9 @@ class ArchManager(SoftwareManager, SettingsController): if ignore_dependencies: cmd.append('-dd') + if replacers: + cmd.extend(f'--assume-installed={r}' for r in replacers) + status_handler.start() all_uninstalled, _ = handler.handle_simple(SimpleProcess(cmd=cmd, root_password=root_password, @@ -1299,9 +1304,53 @@ class ArchManager(SoftwareManager, SettingsController): return True - def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneeded: bool = False, disk_loader: Optional[DiskCacheLoader] = None, skip_requirements: bool = False): + def _fill_aur_providers(self, names: str, output: Set[str]): + for _, data in self.aur_client.gen_updates_data(names): + providers = data.get('p') + + if providers: + output.update(providers) + + def _map_actual_replacers(self, names: Set[str], context: TransactionContext) -> Optional[Set[str]]: + if not names: + return + + repo_replacers, aur_replacers = set(), set() + + for r in names: + repo = context.remote_repo_map.get(r) + + if repo and repo != 'aur': + repo_replacers.add(r) + elif repo == 'aur' or (context.aur_idx and r in context.aur_idx): + aur_replacers.add(r) + + actual_replacers = set() + + thread_fill_aur = None + if aur_replacers: + thread_fill_aur = Thread(target=self._fill_aur_providers, args=(aur_replacers, actual_replacers)) + thread_fill_aur.start() + + if repo_replacers: + repo_replace_providers = pacman.map_provided(remote=True, pkgs=repo_replacers) + + if repo_replace_providers: + actual_replacers.update(repo_replace_providers) + + if thread_fill_aur: + thread_fill_aur.join() + + return actual_replacers + + def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneeded: bool = False, + disk_loader: Optional[DiskCacheLoader] = None, skip_requirements: bool = False, + replacers: Optional[Set[str]] = None): + self._update_progress(context, 10) + actual_replacers = self._map_actual_replacers(replacers, context) if replacers else None + net_available = self.context.internet_checker.is_available() if disk_loader else True hard_requirements = set() @@ -1309,13 +1358,14 @@ class ArchManager(SoftwareManager, SettingsController): if not skip_requirements: for n in names: try: - pkg_reqs = pacman.list_hard_requirements(n, self.logger) + pkg_reqs = pacman.list_hard_requirements(name=n, logger=self.logger, assume_installed=actual_replacers) 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)), + error_msg = self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(n)) + context.watcher.show_message(title=self.i18n['error'].capitalize(), body=error_msg, type_=MessageType.ERROR) return False @@ -1355,7 +1405,8 @@ class ArchManager(SoftwareManager, SettingsController): provided_by_uninstalled = pacman.map_provided(pkgs=to_uninstall) - uninstalled = self._uninstall_pkgs(to_uninstall, context.root_password, context.handler, ignore_dependencies=skip_requirements) + uninstalled = self._uninstall_pkgs(to_uninstall, context.root_password, context.handler, + ignore_dependencies=skip_requirements, replacers=actual_replacers) if uninstalled: self._remove_uninstalled_from_context(provided_by_uninstalled, context) @@ -1398,7 +1449,8 @@ class ArchManager(SoftwareManager, SettingsController): else: unnecessary_instances = None - unneded_uninstalled = self._uninstall_pkgs(all_unnecessary_to_uninstall, context.root_password, context.handler) + unneded_uninstalled = self._uninstall_pkgs(all_unnecessary_to_uninstall, context.root_password, + context.handler, replacers=actual_replacers) if unneded_uninstalled: to_uninstall.update(all_unnecessary_to_uninstall) @@ -1769,10 +1821,13 @@ class ArchManager(SoftwareManager, SettingsController): else: return self._get_history_repo_pkg(pkg) - 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)): + def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: TransactionContext, + skip_requirements: bool = False) -> bool: + + conflict_msg = f"{bold(pkg)} {self.i18n['and']} {bold(conflicting_pkg)}" + msg_body = self.i18n['arch.install.conflict.popup.body'].format(conflict_msg) + + if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], body=msg_body): context.watcher.print(self.i18n['action.cancelled']) return False else: @@ -1783,8 +1838,7 @@ class ArchManager(SoftwareManager, SettingsController): context.removed = {} res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader, - remove_unneeded=False, skip_requirements=skip_requirements) - + remove_unneeded=False, skip_requirements=skip_requirements, replacers={pkg}) context.restabilish_progress() return res @@ -2389,25 +2443,12 @@ class ArchManager(SoftwareManager, SettingsController): 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, skip_requirements=skip_requirement_checking): + disk_loader=context.disk_loader, + replacers=names_to_install): + 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 5f27b1bb..c2f73c23 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -2,6 +2,7 @@ import logging import os import re import shutil +from io import StringIO from threading import Thread from typing import List, Set, Tuple, Dict, Iterable, Optional @@ -514,7 +515,7 @@ def fill_provided_map(key: str, val: str, output: dict): current_val.add(val) -def map_provided(remote: bool = False, pkgs: Iterable[str] = None) -> Dict[str, Set[str]]: +def map_provided(remote: bool = False, pkgs: Iterable[str] = None) -> Optional[Dict[str, Set[str]]]: output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(pkgs) if pkgs else '')) if output: @@ -982,8 +983,15 @@ def is_snapd_installed() -> bool: return bool(run_cmd('pacman -Qq snapd', print_error=False)) -def list_hard_requirements(name: str, logger: Optional[logging.Logger] = None) -> Optional[Set[str]]: - code, output = system.execute('pacman -Rc {} --print-format=%n'.format(name), shell=True) +def list_hard_requirements(name: str, logger: Optional[logging.Logger] = None, + assume_installed: Optional[Set[str]] = None) -> Optional[Set[str]]: + cmd = StringIO() + cmd.write(f'pacman -Rc {name} --print-format=%n ') + + if assume_installed: + cmd.write(' '.join(f'--assume-installed={provider}' for provider in assume_installed)) + + code, output = system.execute(cmd.getvalue(), shell=True) if code != 0: if 'HoldPkg' in output: