[arch] fix -> some conflict resolution scenarios when upgrading several packages

This commit is contained in:
Vinicius Moreira
2020-08-06 11:53:52 -03:00
parent 4400e60622
commit 78912d5eee
5 changed files with 228 additions and 58 deletions

View File

@@ -16,6 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Arch - Arch
- not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub)
- Downloading some AUR packages sources twice when multi-threaded download is enabled - Downloading some AUR packages sources twice when multi-threaded download is enabled
- upgrade summary:
- not displaying all packages that must be uninstalled
- displaying "required size" for packages that must be uninstalled
- some conflict resolution scenarios when upgrading several packages
- UI - UI
- crashing when nothing can be upgraded - crashing when nothing can be upgraded

View File

@@ -456,7 +456,9 @@ class DependenciesAnalyser:
return missing_deps return missing_deps
def map_all_required_by(self, pkgnames: Iterable[str], to_ignore: Set[str]) -> Set[str]: def map_all_required_by(self, pkgnames: Iterable[str], to_ignore: Set[str]) -> Set[str]:
to_ignore.update(pkgnames) if pkgnames:
to_ignore.update(pkgnames)
all_requirements = {req for reqs in pacman.map_required_by(pkgnames).values() for req in reqs if req not in to_ignore} all_requirements = {req for reqs in pacman.map_required_by(pkgnames).values() for req in reqs if req not in to_ignore}
if all_requirements: if all_requirements:

View File

@@ -4,8 +4,7 @@ from threading import Thread
from typing import List, Set, Tuple, Dict, Iterable from typing import List, Set, Tuple, Dict, Iterable
from bauh.commons import system from bauh.commons import system
from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess, \ from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess
ProcessHandler
from bauh.commons.util import size_to_byte from bauh.commons.util import size_to_byte
from bauh.gems.arch.exceptions import PackageNotFoundException from bauh.gems.arch.exceptions import PackageNotFoundException
@@ -539,16 +538,6 @@ def upgrade_system(root_password: str) -> SimpleProcess:
return SimpleProcess(cmd=['pacman', '-Syyu', '--noconfirm'], root_password=root_password) return SimpleProcess(cmd=['pacman', '-Syyu', '--noconfirm'], root_password=root_password)
def get_dependencies_to_remove(pkgs: Iterable[str], root_password: str) -> Dict[str, str]:
proc = SimpleProcess(cmd=['pacman', '-R', *pkgs, '--confirm'], root_password=root_password)
success, output = ProcessHandler().handle_simple(proc)
if not output:
return {}
return {t[1]: t[0] for t in RE_REMOVE_TRANSITIVE_DEPS.findall(output)}
def fill_provided_map(key: str, val: str, output: dict): def fill_provided_map(key: str, val: str, output: dict):
current_val = output.get(key) current_val = output.get(key)
@@ -744,8 +733,12 @@ def upgrade_several(pkgnames: Iterable[str], root_password: str, overwrite_confl
error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}) error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'})
def remove_several(pkgnames: Iterable[str], root_password: str) -> SystemProcess: def remove_several(pkgnames: Iterable[str], root_password: str, skip_checks: bool = False) -> SystemProcess:
cmd = ['pacman', '-R', *pkgnames, '--noconfirm'] cmd = ['pacman', '-R', *pkgnames, '--noconfirm']
if skip_checks:
cmd.append('-dd')
if root_password: if root_password:
return SystemProcess(new_root_subprocess(cmd, root_password), wrong_error_phrase='warning:') return SystemProcess(new_root_subprocess(cmd, root_password), wrong_error_phrase='warning:')
else: else:
@@ -865,6 +858,51 @@ def map_all_deps(names: Iterable[str], only_installed: bool = False) -> Dict[str
return res return res
def map_required_dependencies(*names: str) -> Dict[str, Set[str]]:
output = run_cmd('pacman -Qi {}'.format(' '.join(names) if names else ''))
if output:
res = {}
latest_name, deps, latest_field = 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':
val = line[field_sep_idx + 1:].strip()
latest_name = val
deps = None
elif field == 'Depends On':
val = line[field_sep_idx + 1:].strip()
if deps is None:
deps = set()
if val != 'None':
if ':' in val:
dep_info = val.split(':')
deps.add(dep_info[0].strip())
else:
deps.update({dep.strip() for dep in val.split(' ') if dep})
elif latest_name and deps is not None:
res[latest_name] = deps
latest_name, deps, latest_field = None, None, None
elif latest_name and deps is not None:
if ':' in l:
dep_info = l.split(':')
deps.add(dep_info[0].strip())
else:
deps.update({dep.strip() for dep in l.split(' ') if dep})
return res
def get_cache_dir() -> str: def get_cache_dir() -> str:
dir_pattern = re.compile(r'.*CacheDir\s*=\s*.+') dir_pattern = re.compile(r'.*CacheDir\s*=\s*.+')
@@ -887,8 +925,8 @@ def get_cache_dir() -> str:
return '/var/cache/pacman/pkg' return '/var/cache/pacman/pkg'
def map_required_by(names: Iterable[str]) -> Dict[str, Set[str]]: def map_required_by(names: Iterable[str] = None) -> Dict[str, Set[str]]:
output = run_cmd('pacman -Qi {}'.format(' '.join(names))) output = run_cmd('pacman -Qi {}'.format(' '.join(names) if names else ''))
if output: if output:
res = {} res = {}

View File

@@ -10,6 +10,7 @@ from bauh.gems.arch.aur import AURClient
from bauh.gems.arch.dependencies import DependenciesAnalyser from bauh.gems.arch.dependencies import DependenciesAnalyser
from bauh.gems.arch.exceptions import PackageNotFoundException from bauh.gems.arch.exceptions import PackageNotFoundException
from bauh.gems.arch.model import ArchPackage from bauh.gems.arch.model import ArchPackage
from bauh.gems.arch.pacman import RE_DEP_OPERATORS
from bauh.view.util.translation import I18n from bauh.view.util.translation import I18n
@@ -181,45 +182,13 @@ class UpdatesSummarizer:
root_conflict = self._filter_and_map_conflicts(context) root_conflict = self._filter_and_map_conflicts(context)
sub_conflict = pacman.get_dependencies_to_remove(root_conflict.keys(), context.root_password) if root_conflict else None
to_remove_map = {}
if sub_conflict:
for dep, source in sub_conflict.items():
if dep not in to_remove_map and (not blacklist or dep not in blacklist):
req = ArchPackage(name=dep, installed=True, i18n=self.i18n)
to_remove_map[dep] = req
reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), source)
context.to_remove[dep] = UpgradeRequirement(req, reason)
if root_conflict: if root_conflict:
for dep, source in root_conflict.items(): for dep, source in root_conflict.items():
if dep not in to_remove_map and (not blacklist or dep not in blacklist): if dep not in context.to_remove and (not blacklist or dep not in blacklist):
req = ArchPackage(name=dep, installed=True, i18n=self.i18n) req = ArchPackage(name=dep, installed=True, i18n=self.i18n)
to_remove_map[dep] = req
reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), source) reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), source)
context.to_remove[dep] = UpgradeRequirement(req, reason) context.to_remove[dep] = UpgradeRequirement(req, reason)
if to_remove_map:
for name in to_remove_map.keys(): # upgrading lists
if name in context.pkgs_data:
del context.pkgs_data[name]
if name in context.aur_to_update:
del context.aur_to_update[name]
if name in context.repo_to_update:
del context.repo_to_update[name]
removed_size = pacman.get_installed_size([*to_remove_map.keys()])
if removed_size:
for name, size in removed_size.items():
if size is not None:
req = context.to_remove.get(name)
if req:
req.extra_size = size
def _map_and_add_package(self, pkg_data: Tuple[str, str], idx: int, output: dict): def _map_and_add_package(self, pkg_data: Tuple[str, str], idx: int, output: dict):
version = None version = None
@@ -334,7 +303,7 @@ class UpdatesSummarizer:
context.aur_index.update(names) context.aur_index.update(names)
self.logger.info("AUR index loaded on the context") self.logger.info("AUR index loaded on the context")
def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None, to_install: bool = False, to_upgrade: Set[str] = None) -> UpgradeRequirement: def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None, to_install: bool = False, to_sync: Set[str] = None) -> UpgradeRequirement:
requirement = UpgradeRequirement(pkg) requirement = UpgradeRequirement(pkg)
if pkg.repository != 'aur': if pkg.repository != 'aur':
@@ -347,10 +316,32 @@ class UpdatesSummarizer:
if current_size is not None and data['s']: if current_size is not None and data['s']:
requirement.extra_size = data['s'] - current_size requirement.extra_size = data['s'] - current_size
if to_install and context.pkgs_data and context.to_update: required_by = set()
if to_install and to_sync and context.pkgs_data:
names = context.pkgs_data[pkg.name].get('p', {pkg.name}) names = context.pkgs_data[pkg.name].get('p', {pkg.name})
required_by = ','.join([p for p in to_upgrade if p != pkg.name and context.pkgs_data[p]['d'] and any([n for n in names if n in context.pkgs_data[p]['d']])]) to_sync_deps_cache = {}
requirement.reason = '{}: {}'.format(self.i18n['arch.info.required by'].capitalize(), required_by if required_by else '?') for p in to_sync:
if p != pkg.name and p in context.pkgs_data:
deps = to_sync_deps_cache.get(p)
if deps is None:
deps = context.pkgs_data[p]['d']
if deps is None:
deps = set()
else:
deps = {RE_DEP_OPERATORS.split(d)[0] for d in deps}
to_sync_deps_cache[p] = deps
if deps:
for n in names:
if n in deps:
required_by.add(p)
break
requirement.reason = '{}: {}'.format(self.i18n['arch.info.required by'].capitalize(), ','.join(required_by) if required_by else '?')
return requirement return requirement
@@ -403,6 +394,8 @@ class UpdatesSummarizer:
self.logger.error("Package '{}' not found".format(e.name)) self.logger.error("Package '{}' not found".format(e.name))
return return
self.__update_context_based_on_to_remove(context)
if context.to_update: if context.to_update:
installed_sizes = pacman.get_installed_size(list(context.to_update.keys())) installed_sizes = pacman.get_installed_size(list(context.to_update.keys()))
@@ -427,7 +420,140 @@ class UpdatesSummarizer:
res.cannot_upgrade = [d for d in context.cannot_upgrade.values()] res.cannot_upgrade = [d for d in context.cannot_upgrade.values()]
if context.to_install: if context.to_install:
to_upgrade = {r.pkg.name for r in res.to_upgrade} if res.to_upgrade else None to_sync = {r.pkg.name for r in res.to_upgrade} if res.to_upgrade else {}
res.to_install = [self._map_requirement(p, context, to_install=True, to_upgrade=to_upgrade) for p in context.to_install.values()] to_sync.update(context.to_install.keys())
res.to_install = [self._map_requirement(p, context, to_install=True, to_sync=to_sync) for p in context.to_install.values()]
return res return res
def __update_context_based_on_to_remove(self, context: UpdateRequirementsContext):
if context.to_remove:
to_remove_provided = {}
# filtering all package to synchronization from the transaction context
to_sync = {*(context.to_update.keys() if context.to_update else set()), *(context.to_install.keys() if context.to_install else set())}
if to_sync: # checking if any packages to sync on the context rely on the 'to remove' ones
to_remove_provided.update(pacman.map_provided(remote=False, pkgs=context.to_remove.keys()))
to_remove_from_sync = {} # will store all packages that should be removed
for pname in to_sync:
if pname in context.pkgs_data:
deps = context.pkgs_data[pname].get('d')
if deps:
required = set()
for pkg in context.to_remove:
for provided in to_remove_provided[pkg]:
if provided in deps:
required.add(pkg)
break
if required:
to_remove_from_sync[pname] = required
else:
self.logger.warning("Conflict resolution: package '{}' marked to synchronization has no data loaded")
if to_remove_from_sync: # removing all these packages and their dependents from the context
self._add_to_remove(to_sync, to_remove_from_sync, context)
# checking if the installed packages that are not in the transaction context rely on the current packages to be removed:
current_to_remove = {*context.to_remove.keys()}
required_by_installed = self.deps_analyser.map_all_required_by(current_to_remove, {*to_sync})
if required_by_installed:
# updating provided context:
provided_not_mapped = set()
for pkg in current_to_remove.difference({*to_remove_provided.keys()}):
if pkg not in context.pkgs_data:
provided_not_mapped.add(pkg)
else:
provided = context.pkgs_data[pkg].get('p')
if provided:
to_remove_provided[pkg] = provided
else:
provided_not_mapped.add(pkg)
if provided_not_mapped:
to_remove_provided.update(pacman.map_provided(remote=False, pkgs=provided_not_mapped))
deps_no_data = {dep for dep in required_by_installed if dep in context.pkgs_data}
deps_nodata_deps = pacman.map_required_dependencies(*deps_no_data) if deps_no_data else {}
reverse_to_remove_provided = {p: name for name, provided in to_remove_provided for p in provided}
for pkg in required_by_installed:
if pkg not in context.to_remove:
if pkg in context.pkgs_data:
dep_deps = context.pkgs_data[pkg].get('d')
else:
dep_deps = deps_nodata_deps.get(pkg)
if dep_deps:
source = ', '.join((reverse_to_remove_provided[d] for d in dep_deps if d in reverse_to_remove_provided))
reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), source if source else '?')
context.to_remove[pkg] = UpgradeRequirement(pkg=ArchPackage(name=pkg,
installed=True,
i18n=self.i18n),
reason=reason)
for name in context.to_remove: # upgrading lists
if name in context.pkgs_data:
del context.pkgs_data[name]
if name in context.aur_to_update:
del context.aur_to_update[name]
if name in context.repo_to_update:
del context.repo_to_update[name]
removed_size = pacman.get_installed_size([*context.to_remove.keys()])
if removed_size:
for name, size in removed_size.items():
if size is not None:
req = context.to_remove.get(name)
if req:
req.extra_size = size
def _add_to_remove(self, pkgs_to_sync: Set[str], names: Dict[str, Set[str]], context: UpdateRequirementsContext, to_ignore: Set[str] = None):
blacklist = to_ignore if to_ignore else set()
blacklist.update(names)
dependents = {}
for pname in pkgs_to_sync:
if pname not in blacklist:
data = context.pkgs_data.get(pname)
if data:
deps = data.get('d')
if deps:
for n in names:
if n in deps:
all_deps = dependents.get(n, set())
all_deps.update(pname)
dependents[n] = all_deps
else:
self.logger.warning("Package '{}' to sync could not be removed from the transaction context because its data was not loaded")
for n in names:
if n in context.pkgs_data:
if n not in context.to_remove:
depends_on = names.get(n)
if depends_on:
reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), ', '.join(depends_on))
else:
reason = '?'
context.to_remove[n] = UpgradeRequirement(pkg=ArchPackage(name=n,
installed=True,
i18n=self.i18n),
reason=reason)
all_deps = dependents.get(n)
if all_deps:
self._add_to_remove(pkgs_to_sync, {dep: {n} for dep in all_deps}, context, blacklist)
else:
self.logger.warning("Package '{}' could not be removed from the transaction context because its data was not loaded")

View File

@@ -192,7 +192,7 @@ class UpgradeSelected(AsyncAction):
self.manager = manager self.manager = manager
self.i18n = i18n self.i18n = i18n
def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None) -> InputOption: def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None, required_size: bool = True) -> InputOption:
if req.pkg.installed: if req.pkg.installed:
icon_path = req.pkg.get_disk_icon_path() icon_path = req.pkg.get_disk_icon_path()
@@ -204,7 +204,7 @@ class UpgradeSelected(AsyncAction):
size_str = '{}: {}'.format(self.i18n['size'].capitalize(), size_str = '{}: {}'.format(self.i18n['size'].capitalize(),
'?' if req.extra_size is None else get_human_size_str(req.extra_size)) '?' if req.extra_size is None else get_human_size_str(req.extra_size))
if req.extra_size != req.required_size: if required_size and req.extra_size != req.required_size:
size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(), size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(),
'?' if req.required_size is None else get_human_size_str(req.required_size)) '?' if req.required_size is None else get_human_size_str(req.required_size))
@@ -250,7 +250,7 @@ class UpgradeSelected(AsyncAction):
return FormComponent(label=lb, components=comps), (required_size, extra_size) return FormComponent(label=lb, components=comps), (required_size, extra_size)
def _gen_to_remove_form(self, reqs: List[UpgradeRequirement]) -> FormComponent: def _gen_to_remove_form(self, reqs: List[UpgradeRequirement]) -> FormComponent:
opts = [self._req_as_option(r, False, r.reason) for r in reqs] opts = [self._req_as_option(r, False, r.reason, required_size=False) for r in reqs]
comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))]
required_size, extra_size = self._sum_pkgs_size(reqs) required_size, extra_size = self._sum_pkgs_size(reqs)