diff --git a/actions/packages b/actions/packages index 996450141..3e8934462 100755 --- a/actions/packages +++ b/actions/packages @@ -131,7 +131,7 @@ def _assert_managed_packages(module, packages): app = cls() components = app.get_components_of_type(Packages) for component in components: - managed_packages += component.packages + managed_packages += component.managed_packages() for package in packages: assert package in managed_packages diff --git a/plinth/package.py b/plinth/package.py index b4767524c..287423aee 100644 --- a/plinth/package.py +++ b/plinth/package.py @@ -101,7 +101,8 @@ class Packages(app.FollowerComponent): 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], + def __init__(self, component_id: str, + packages: list[Union[str, PackageExpression]], skip_recommends: bool = False, conflicts: Optional[list[str]] = None, conflicts_action: Optional[ConflictsAction] = None): @@ -126,29 +127,50 @@ class Packages(app.FollowerComponent): super().__init__(component_id) self.component_id = component_id - self._packages = packages + self._packages = [] + for package in packages: + if isinstance(package, str): + self._packages.append(Package(package)) + else: + self._packages.append(package) + self.skip_recommends = skip_recommends self.conflicts = conflicts self.conflicts_action = conflicts_action @property - def packages(self) -> list[str]: - """Return the list of packages managed by this component.""" + def packages(self) -> list[Union[str, PackageExpression]]: + """Return the list of packages and package expressions managed by this + component.""" return self._packages + def managed_packages(self) -> list[str]: + """Return the list of possible packages before resolving.""" + managed_packages: list[str] = [] + for package in self.packages: + managed_package = package.possible() + managed_packages.extend(managed_package) + + return managed_packages + + def resolve(self) -> list[str]: + """Return the resolved list of packages to install.""" + return [package.actual() for package in self.packages] + def setup(self, old_version): """Install the packages.""" # TODO: Drop the need for setup helper. module_name = self.app.__module__ module = sys.modules[module_name] helper = module.setup_helper - helper.install(self.packages, skip_recommends=self.skip_recommends) + helper.install(self.resolve(), skip_recommends=self.skip_recommends) def diagnose(self): """Run diagnostics and return results.""" results = super().diagnose() cache = apt.Cache() - for package_name in self.packages: + # XXX: Needs to be able to handle missing packages. + for package_name in self.resolve(): result = 'warning' latest_version = '?' if package_name in cache: @@ -187,9 +209,12 @@ class Packages(app.FollowerComponent): return None # List of all packages from all Package components - cache = apt.Cache() - return any(package for package in self.packages - if package not in cache) + try: + self.resolve() + except MissingPackageError: + return True + + return False class PackageException(Exception): diff --git a/plinth/tests/test_package.py b/plinth/tests/test_package.py index 4b35a8517..e787e8e8e 100644 --- a/plinth/tests/test_package.py +++ b/plinth/tests/test_package.py @@ -53,8 +53,8 @@ class TestPackageExpressions(unittest.TestCase): def test_packages_init(): """Test initialization of packages component.""" component = Packages('test-component', ['foo', 'bar']) + assert component.managed_packages() == ['foo', 'bar'] 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 @@ -65,12 +65,27 @@ def test_packages_init(): component = Packages('test-component', [], skip_recommends=True, conflicts=['conflict1', 'conflict2'], conflicts_action=Packages.ConflictsAction.IGNORE) - assert component.packages == [] + assert component.managed_packages() == [] assert component.skip_recommends assert component.conflicts == ['conflict1', 'conflict2'] assert component.conflicts_action == Packages.ConflictsAction.IGNORE +def test_packages_resolve(): + """Test resolving of package expressions.""" + component = Packages('test-component', ['python3']) + assert component.resolve() == ['python3'] + + component = Packages('test-component', + [Package('unknown-package') | Package('python3')]) + assert component.resolve() == ['python3'] + + component = Packages('test-component', [], skip_recommends=True, + conflicts=['conflict1', 'conflict2'], + conflicts_action=Packages.ConflictsAction.IGNORE) + assert component.resolve() == [] + + def test_packages_setup(): """Test setting up packages component.""" @@ -78,22 +93,31 @@ def test_packages_setup(): """Test app""" app_id = 'test-app' - component = Packages('test-component', ['foo1', 'bar1']) + component = Packages('test-component', ['python3', 'bash']) app = TestApp() app.add(component) setup_helper.reset_mock() app.setup(old_version=3) setup_helper.install.assert_has_calls( - [call(['foo1', 'bar1'], skip_recommends=False)]) + [call(['python3', 'bash'], skip_recommends=False)]) - component = Packages('test-component', ['foo2', 'bar2'], + component = Packages('test-component', ['bash', 'perl'], skip_recommends=True) app = TestApp() app.add(component) setup_helper.reset_mock() app.setup(old_version=3) setup_helper.install.assert_has_calls( - [call(['foo2', 'bar2'], skip_recommends=True)]) + [call(['bash', 'perl'], skip_recommends=True)]) + + component = Packages('test-component', + [Package('python3') | Package('unknown-package')]) + app = TestApp() + app.add(component) + setup_helper.reset_mock() + app.setup(old_version=3) + setup_helper.install.assert_has_calls( + [call(['python3'], skip_recommends=False)]) @patch('apt.Cache')