diff --git a/plinth/package.py b/plinth/package.py index db9a07428..2d41c4806 100644 --- a/plinth/package.py +++ b/plinth/package.py @@ -3,12 +3,13 @@ Framework for installing and updating distribution packages """ +import enum import json import logging import subprocess import sys import threading -from typing import Union +from typing import Optional, Union import apt.cache from django.utils.translation import gettext as _ @@ -28,8 +29,15 @@ class Packages(app.FollowerComponent): of packages required by an app. """ + class ConflictsAction(enum.Enum): + """Action to take when a conflicting package is installed.""" + IGNORE = 'ignore' # Proceed as if there are no conflicts + REMOVE = 'remove' # Remove the packages before installing the app + def __init__(self, component_id: str, packages: list[str], - skip_recommends=False): + skip_recommends: bool = False, + conflicts: Optional[list[str]] = None, + conflicts_action: Optional[ConflictsAction] = None): """Initialize a new packages component. 'component_id' should be a unique ID across all components of an app @@ -39,12 +47,22 @@ class Packages(app.FollowerComponent): 'skip_recommends' is a boolean specifying whether recommended packages should be installed along with the listed packages. + + 'conflicts' is the list of Debian packages that can't simultaneously be + installed with packages listed here. None if there are no known + conflicting packages. + + 'conflicts_action' is a string representing the action to take when it + is found that conflicting Debian packages are installed on the system. + None if there are no known conflicting packages. """ super().__init__(component_id) self.component_id = component_id self._packages = packages self.skip_recommends = skip_recommends + self.conflicts = conflicts + self.conflicts_action = conflicts_action @property def packages(self) -> list[str]: @@ -59,6 +77,13 @@ class Packages(app.FollowerComponent): helper = module.setup_helper helper.install(self.packages, skip_recommends=self.skip_recommends) + def find_conflicts(self) -> Optional[list[str]]: + """Return list of conflicting packages installed on the system.""" + if not self.conflicts: + return None + + return packages_installed(self.conflicts) + class PackageException(Exception): """A package operation has failed.""" diff --git a/plinth/tests/test_package.py b/plinth/tests/test_package.py index 8f6db81eb..5435c4ab6 100644 --- a/plinth/tests/test_package.py +++ b/plinth/tests/test_package.py @@ -20,13 +20,19 @@ def test_packages_init(): assert component.component_id == 'test-component' assert component.packages == ['foo', 'bar'] assert not component.skip_recommends + assert component.conflicts is None + assert component.conflicts_action is None with pytest.raises(ValueError): Packages(None, []) - component = Packages('test-component', [], skip_recommends=True) + component = Packages('test-component', [], skip_recommends=True, + conflicts=['conflict1', 'conflict2'], + conflicts_action=Packages.ConflictsAction.IGNORE) assert component.packages == [] assert component.skip_recommends + assert component.conflicts == ['conflict1', 'conflict2'] + assert component.conflicts_action == Packages.ConflictsAction.IGNORE def test_packages_setup(): @@ -54,6 +60,26 @@ def test_packages_setup(): [call(['foo2', 'bar2'], skip_recommends=True)]) +@patch('plinth.package.packages_installed') +def test_packages_find_conflicts(packages_installed_): + """Test finding conflicts.""" + packages_installed_.return_value = [] + component = Packages('test-component', ['package3', 'package4']) + assert component.find_conflicts() is None + + packages_installed_.return_value = [] + component = Packages('test-component', ['package3', 'package4'], + conflicts=['package5', 'package6'], + conflicts_action=Packages.ConflictsAction.IGNORE) + assert component.find_conflicts() == [] + + packages_installed_.return_value = ['package1', 'package2'] + component = Packages('test-component', ['package3', 'package4'], + conflicts=['package1', 'package2'], + conflicts_action=Packages.ConflictsAction.IGNORE) + assert component.find_conflicts() == ['package1', 'package2'] + + def test_packages_installed(): """Test packages_installed().""" # list as input