diff --git a/CHANGELOG.md b/CHANGELOG.md index b41f1573..84db3961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - Arch - - not properly checking all provided packages by the system when checking for conflicts (upgrade requirements) + - not properly checking all provided packages by the system when checking for conflicts (upgrade requirements). it could lead to an unbootable system or package losses. - UI - some windows not properly being centralized (regression introduced in **0.10.4**) - some windows being displayed in the wrong place (regression introduced in **0.10.4**) diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 2d5abe17..4ff57e32 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -22,7 +22,7 @@ class UpdateRequirementsContext: aur_to_update: Dict[str, ArchPackage], repo_to_install: Dict[str, ArchPackage], aur_to_install: Dict[str, ArchPackage], to_install: Dict[str, ArchPackage], pkgs_data: Dict[str, dict], cannot_upgrade: Dict[str, UpgradeRequirement], - to_remove: Dict[str, UpgradeRequirement], installed_names: Dict[str, str], + to_remove: Dict[str, UpgradeRequirement], installed: Dict[str, str], provided_map: Dict[str, Set[str]], aur_index: Set[str], arch_config: dict, remote_provided_map: Dict[str, Set[str]], remote_repo_map: Dict[str, str], root_password: Optional[str], aur_supported: bool): @@ -34,7 +34,7 @@ class UpdateRequirementsContext: self.pkgs_data = pkgs_data self.cannot_upgrade = cannot_upgrade self.root_password = root_password - self.installed = installed_names + self.installed = installed self.provided_map = provided_map self.to_remove = to_remove self.to_install = to_install @@ -167,6 +167,23 @@ class UpdatesSummarizer: else: del context.aur_to_update[p] + def _map_virtual_providers(self, providers: Dict[str, Set[str]], installed: Dict[str, str]) -> Dict[str, Set[str]]: + ti = time.time() + virtual_version = dict() + for provider in providers: + name_version = provider.split("=") + if len(name_version) == 2 and name_version[0] not in installed: + versions = virtual_version.get(name_version[0]) + + if not versions: + versions = set() + virtual_version[name_version[0]] = versions + + versions.add(name_version[1]) + tf = time.time() + self.logger.info(f"Took {tf - ti:.6f} seconds to map virtual providers of {len(providers)} packages") + return virtual_version + def _map_conflicts(self, data: Dict[str, Dict[str, Any]], providers: Dict[str, Set[str]], versions: Dict[str, str]) -> Tuple[Dict[str, str], Dict[str, str]]: """ @@ -180,6 +197,8 @@ class UpdatesSummarizer: - first: containing all conflicts - second: containing mutual conflicts """ + virtual_providers = self._map_virtual_providers(providers, versions) + root_conflict = {} mutual_conflicts = {} @@ -192,20 +211,33 @@ class UpdatesSummarizer: if conflict_name != pkg_name: conflict_providers = providers.get(conflict_name) + if conflict_providers: # it means the conflict name matches a provided package + checked_conflicts = set() - if conflict_providers: - checked_conflicts = [] - for provider in conflict_providers: - if provider != pkg_name: - if len(name_op_exp) == 1: - checked_conflicts.append(provider) - else: - provider_version = versions.get(provider) + if len(name_op_exp) == 1: # if no expression is provided, add all providers + checked_conflicts.update((p for p in conflict_providers if p != pkg_name)) + else: + virtual_versions = virtual_providers.get(conflict_name) - if match_required_version(provider_version, - name_op_exp[1], - name_op_exp[2]): - checked_conflicts.append(provider) + if virtual_versions: + # it means it's a virtual package + # (e.g: 'xorg-server' provides a virtual package called 'x-server') + for pversion in virtual_versions: + if match_required_version(pversion, name_op_exp[1], name_op_exp[2]): + # read the packages providing this specific virtual package version + real_providers = providers.get(f"{conflict_name}={pversion}") + + if real_providers: + checked_conflicts.update(p for p in real_providers if p != pkg_name) + + else: + for provider in conflict_providers: + if provider != pkg_name: + provider_version = versions.get(provider) + if provider_version and match_required_version(provider_version, + name_op_exp[1], + name_op_exp[2]): + checked_conflicts.add(provider) for provider in checked_conflicts: root_conflict[provider] = pkg_name @@ -476,7 +508,7 @@ class UpdatesSummarizer: remote_repo_map = pacman.map_repositories() context = UpdateRequirementsContext(to_update={}, repo_to_update={}, aur_to_update={}, repo_to_install={}, aur_to_install={}, to_install={}, pkgs_data={}, cannot_upgrade={}, - to_remove={}, installed_names=dict(), provided_map={}, aur_index=set(), + to_remove={}, installed=dict(), provided_map={}, aur_index=set(), arch_config=arch_config, root_password=root_password, remote_provided_map=remote_provided_map, remote_repo_map=remote_repo_map, aur_supported=self.aur_supported) diff --git a/tests/gems/arch/test_updates.py b/tests/gems/arch/test_updates.py index fd717fbd..0f209d60 100644 --- a/tests/gems/arch/test_updates.py +++ b/tests/gems/arch/test_updates.py @@ -273,6 +273,199 @@ class UpdatesSummarizerGetUpgradeRequirementsTest(TestCase): self.assertFalse(res.to_install) self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__not_return_to_remove_when_conflict_with_provided_virtual_package_with_version_fails(self, pacman: Mock): + """ + Scenario: + - Package V (2.5.0)[update] -> conflicts: X<21.1.1, X-ABI-VIDEODRV_VERSION<25, X-ABI-VIDEODRV_VERSION>=26 + - Package X (21.1.5)[installed] -> provides: X-ABI-VIDEODRV_VERSION=25.2, X + + * X provides X-ABI-VIDEODRV_VERSION with a specific version (25.2) + """ + pkg_a = ArchPackage(name="V", version="2.5.0-1", latest_version="2.6.0-1", repository="community") + pacman.map_provided.side_effect = [{"V": {"V"}, # remote provided + "V=2.5.0": {"V"}, + "X": {"X"}, + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=25.2": {"X"}, + "X-ABI-VIDEODRV_VERSION": {"X"} + }, + {"V": {"V"}, # provided + "V=2.5.0": {"V"}, + "X": {"X"}, + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=25.2": {"X"}, + "X-ABI-VIDEODRV_VERSION": {"X"} + }, + ] + pacman.map_repositories.return_value = {"V": pkg_a.repository, "X": "community"} + pacman.map_updates_data.return_value = {"V": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"X<21.1.1", + "X-ABI-VIDEODRV_VERSION<25", + "X-ABI-VIDEODRV_VERSION>=26"}, + 'p': {"V": {"V"}, + "V=2.5.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"V": pkg_a.version, "X": "21.1.5-1"} + pacman.get_installed_size.return_value = {"V": 1, "X": 1} + pacman.map_required_by.return_value = {"V": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + self.assertFalse(res.to_remove) + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_installed_virtual_with_defined_version_to_remove_when_conflict_version_matches(self, pacman: Mock): + """ + Scenario: + - Package V (2.5.0)[update] -> conflicts: X<21.1.1, X-ABI-VIDEODRV_VERSION<25, X-ABI-VIDEODRV_VERSION>=26 + - Package X (21.1.5)[installed] -> provides: X-ABI-VIDEODRV_VERSION=26, X + + * X provides X-ABI-VIDEODRV_VERSION with a specific version (26) + """ + pkg_a = ArchPackage(name="V", version="2.5.0-1", latest_version="2.6.0-1", repository="community") + + pacman.map_provided.side_effect = [{"V": {"V"}, # remote provided + "V=2.5.0": {"V"}, + "X": {"X"}, + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=26": {"X"}, + "X-ABI-VIDEODRV_VERSION": {"X"} + }, + {"V": {"V"}, # provided + "V=2.5.0": {"V"}, + "X": {"X"}, + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=26": {"X"}, + "X-ABI-VIDEODRV_VERSION": {"X"} + }, + {"X": {"X"}, # provided to remove + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=26": {"X"}, + "X-ABI-VIDEODRV_VERSION": {"X"} + } + ] + pacman.map_repositories.return_value = {"V": pkg_a.repository, "X": "community"} + pacman.map_updates_data.return_value = {"V": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"X<21.1.1", + "X-ABI-VIDEODRV_VERSION<25", + "X-ABI-VIDEODRV_VERSION>=26"}, + 'p': {"V": {"V"}, + "V=2.5.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"V": pkg_a.version, "X": "21.1.5-1"} + pacman.get_installed_size.return_value = {"V": 1, "X": 1} + pacman.map_required_by.return_value = {"V": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_b = ArchPackage(name="X", installed=True, i18n=self.i18n) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'V'", extra_size=1)], res.to_remove) + + @patch(f"{__app_name__}.gems.arch.updates.pacman") + def test__return_installed_virtual_with_defined_version_to_remove_when_conflict_matches__case_2(self, pacman: Mock): + """ + This test case covers the same scenario as the above, but adds an additional provider for the same + virtual package with a non-conflicting version + + Scenario: + - Package V (2.5.0)[update] -> conflicts: X<21.1.1, X-ABI-VIDEODRV_VERSION<25, X-ABI-VIDEODRV_VERSION>=26 + - Package X (21.1.5)[installed] -> provides: X-ABI-VIDEODRV_VERSION=26, X + - Package Z (2.0.0)[installed] -> provides: X-ABI-VIDEODRV_VERSION=25.2, Z + + * X provides X-ABI-VIDEODRV_VERSION with a specific version (26) [conflict] + * Z provides X-ABI-VIDEODRV_VERSION with a specific version (25.2) [no conflict] + """ + pkg_a = ArchPackage(name="V", version="2.5.0-1", latest_version="2.6.0-1", repository="community") + + pacman.map_provided.side_effect = [{"V": {"V"}, # remote provided + "V=2.5.0": {"V"}, + "X": {"X"}, + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=26": {"X"}, + "Z": {"Z"}, + "Z=2.0.0": {"X"}, + "X-ABI-VIDEODRV_VERSION=25.2": {"Z"}, + "X-ABI-VIDEODRV_VERSION": {"X", "Z"}, + }, + {"V": {"V"}, # provided + "V=2.5.0": {"V"}, + "X": {"X"}, + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=26": {"X"}, + "Z": {"Z"}, + "Z=2.0.0": {"X"}, + "X-ABI-VIDEODRV_VERSION=25.2": {"Z"}, + "X-ABI-VIDEODRV_VERSION": {"X", "Z"}, + }, + {"X": {"X"}, # provided to remove + "X=21.1.5": {"X"}, + "X-ABI-VIDEODRV_VERSION=26": {"X"}, + "X-ABI-VIDEODRV_VERSION": {"X", "Z"} + } + ] + pacman.map_repositories.return_value = {"V": pkg_a.repository, "X": "community"} + pacman.map_updates_data.return_value = {"V": {'ds': 1, + 's': 1, + 'v': pkg_a.latest_version, + 'c': {"X<21.1.1", + "X-ABI-VIDEODRV_VERSION<25", + "X-ABI-VIDEODRV_VERSION>=26"}, + 'p': {"V": {"V"}, + "V=2.5.0": {"A"}}, + 'd': set(), + 'r': "community", + 'des': pkg_a.name}} + pacman.map_installed.return_value = {"V": pkg_a.version, "X": "21.1.5-1", "Z": "2.0.0-1"} + pacman.get_installed_size.return_value = {"V": 1, "X": 1, "Z": 1} + pacman.map_required_by.return_value = {"V": set()} + + self.deps_analyser.map_missing_deps.return_value = list() + self.deps_analyser.map_all_required_by.return_value = set() + + res = self.summarizer.summarize(pkgs=[pkg_a], root_password=None, arch_config=self.config_) + + for method in ('map_provided', 'map_repositories', 'map_updates_data', 'map_installed', + 'get_installed_size', 'map_required_by'): + getattr(pacman, method).assert_called() + + for method in ('map_missing_deps', 'map_all_required_by'): + getattr(self.deps_analyser, method).assert_called() + + self.assertFalse(res.to_install) + self.assertEqual([UpgradeRequirement(pkg=pkg_a, required_size=1, extra_size=0)], res.to_upgrade) + + pkg_b = ArchPackage(name="X", installed=True, i18n=self.i18n) + self.assertEqual([UpgradeRequirement(pkg=pkg_b, reason=" 'V'", extra_size=1)], res.to_remove) + @patch(f"{__app_name__}.gems.arch.updates.pacman") def test__should_return_installed_package_to_remove_when_conflict_with_provided_matches_version(self, pacman: Mock): """