mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
package: Don't uninstall packages that are in use by other apps
Closes: #2262. Tests: - Unit tests work - Uninstall of email app succeeds without uninstalling openssl package. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
bc09cd42bb
commit
16cb8ee021
@ -12,7 +12,7 @@ from django.utils.translation import gettext as _
|
|||||||
from django.utils.translation import gettext_lazy
|
from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
import plinth.privileged.packages as privileged
|
import plinth.privileged.packages as privileged
|
||||||
from plinth import app
|
from plinth import app as app_module
|
||||||
from plinth.errors import MissingPackageError
|
from plinth.errors import MissingPackageError
|
||||||
from plinth.utils import format_lazy
|
from plinth.utils import format_lazy
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ class PackageOr(PackageExpression):
|
|||||||
return self.package2.actual()
|
return self.package2.actual()
|
||||||
|
|
||||||
|
|
||||||
class Packages(app.FollowerComponent):
|
class Packages(app_module.FollowerComponent):
|
||||||
"""Component to manage the packages of an app.
|
"""Component to manage the packages of an app.
|
||||||
|
|
||||||
This component is responsible for installation, upgrades and uninstallation
|
This component is responsible for installation, upgrades and uninstallation
|
||||||
@ -182,7 +182,23 @@ class Packages(app.FollowerComponent):
|
|||||||
|
|
||||||
def uninstall(self):
|
def uninstall(self):
|
||||||
"""Uninstall and purge the packages."""
|
"""Uninstall and purge the packages."""
|
||||||
uninstall(self.get_actual_packages())
|
packages = self.get_actual_packages()
|
||||||
|
packages_set = set(packages)
|
||||||
|
for app in app_module.App.list():
|
||||||
|
# uninstall() will be called on Packages of this app separately
|
||||||
|
# for uninstalling this app.
|
||||||
|
if app == self.app:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if app.get_setup_state() == app_module.App.SetupState.NEEDS_SETUP:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Remove packages used by other installed apps
|
||||||
|
for component in app.get_components_of_type(Packages):
|
||||||
|
packages_set -= set(component.get_actual_packages())
|
||||||
|
|
||||||
|
# Preserve order of packages for ease of testing
|
||||||
|
uninstall([package for package in packages if package in packages_set])
|
||||||
|
|
||||||
def diagnose(self):
|
def diagnose(self):
|
||||||
"""Run diagnostics and return results."""
|
"""Run diagnostics and return results."""
|
||||||
|
|||||||
@ -13,6 +13,12 @@ from plinth.errors import MissingPackageError
|
|||||||
from plinth.package import Package, Packages, packages_installed
|
from plinth.package import Package, Packages, packages_installed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fixture_clean_apps():
|
||||||
|
"""Fixture to ensure clean set of global apps."""
|
||||||
|
App._all_apps = {}
|
||||||
|
|
||||||
|
|
||||||
class TestPackageExpressions(unittest.TestCase):
|
class TestPackageExpressions(unittest.TestCase):
|
||||||
|
|
||||||
def test_package(self):
|
def test_package(self):
|
||||||
@ -159,6 +165,64 @@ def test_packages_uninstall(uninstall):
|
|||||||
uninstall.assert_has_calls([call(['python3', 'bash'])])
|
uninstall.assert_has_calls([call(['python3', 'bash'])])
|
||||||
|
|
||||||
|
|
||||||
|
@patch('plinth.package.uninstall')
|
||||||
|
@patch('apt.Cache')
|
||||||
|
def test_packages_uninstall_exclusion(cache, uninstall):
|
||||||
|
"""Test excluding packages from other installed apps when uninstalling."""
|
||||||
|
cache.return_value = {
|
||||||
|
'package11': Mock(candidate=Mock(version='2.0', is_installed=True)),
|
||||||
|
'package12': Mock(candidate=Mock(version='3.0', is_installed=False)),
|
||||||
|
'package2': Mock(candidate=Mock(version='4.0', is_installed=True)),
|
||||||
|
'package3': Mock(candidate=Mock(version='5.0', is_installed=True)),
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestApp1(App):
|
||||||
|
"""Test app."""
|
||||||
|
app_id = 'test-app1'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
component = Packages('test-component11',
|
||||||
|
['package11', 'package2', 'package3'])
|
||||||
|
self.add(component)
|
||||||
|
|
||||||
|
component = Packages('test-component12',
|
||||||
|
['package12', 'package2', 'package3'])
|
||||||
|
self.add(component)
|
||||||
|
|
||||||
|
class TestApp2(App):
|
||||||
|
"""Test app."""
|
||||||
|
app_id = 'test-app2'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
component = Packages('test-component2', ['package2'])
|
||||||
|
self.add(component)
|
||||||
|
|
||||||
|
def get_setup_state(self):
|
||||||
|
return App.SetupState.UP_TO_DATE
|
||||||
|
|
||||||
|
class TestApp3(App):
|
||||||
|
"""Test app."""
|
||||||
|
app_id = 'test-app3'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
component = Packages('test-component3', ['package3'])
|
||||||
|
self.add(component)
|
||||||
|
|
||||||
|
def get_setup_state(self):
|
||||||
|
return App.SetupState.NEEDS_SETUP
|
||||||
|
|
||||||
|
app1 = TestApp1()
|
||||||
|
TestApp2()
|
||||||
|
TestApp3()
|
||||||
|
app1.uninstall()
|
||||||
|
uninstall.assert_has_calls(
|
||||||
|
[call(['package11', 'package3']),
|
||||||
|
call(['package12', 'package3'])])
|
||||||
|
|
||||||
|
|
||||||
@patch('apt.Cache')
|
@patch('apt.Cache')
|
||||||
def test_diagnose(cache):
|
def test_diagnose(cache):
|
||||||
"""Test checking for latest version of the package."""
|
"""Test checking for latest version of the package."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user