diff --git a/ahriman-core/src/ahriman/core/repository/update_handler.py b/ahriman-core/src/ahriman/core/repository/update_handler.py index 27dac131..e889bd23 100644 --- a/ahriman-core/src/ahriman/core/repository/update_handler.py +++ b/ahriman-core/src/ahriman/core/repository/update_handler.py @@ -36,6 +36,17 @@ class UpdateHandler(PackageInfo, Cleaner): trait to get package update list """ + def _updates_ignored(self) -> set[str]: + """ + collect package bases excluded from automatic updates + + Returns: + set[str]: held and configured ignored package bases + """ + return set(self._ignore_list) | { + package.base for package, status in self.reporter.package_get(None) if status.is_held + } + def updates_aur(self, filter_packages: Iterable[str], *, vcs: bool) -> list[Package]: """ check AUR for updates @@ -58,9 +69,7 @@ class UpdateHandler(PackageInfo, Cleaner): continue raise UnknownPackageError(package.base) - ignore_list = self._ignore_list + [ - package.base for package, status in self.reporter.package_get(None) if status.is_held - ] + ignore_list = self._updates_ignored() result: list[Package] = [] for local in self.packages(filter_packages): @@ -106,8 +115,13 @@ class UpdateHandler(PackageInfo, Cleaner): return files + ignore_list = self._updates_ignored() + result: list[Package] = [] for local in self.packages(filter_packages): + if local.base in ignore_list: + self.logger.info("package %s is held, skip update check", local.base) + continue dependencies = self.reporter.package_dependencies_get(local.base) if not dependencies.paths: continue # skip check if no package dependencies found @@ -141,9 +155,13 @@ class UpdateHandler(PackageInfo, Cleaner): """ packages = {local.base: local for local in self.packages()} local_versions = {package_base: package.version for package_base, package in packages.items()} + ignore_list = self._updates_ignored() result: list[Package] = [] for cache_dir in self.paths.cache.iterdir(): + if cache_dir.name in ignore_list: + self.logger.info("package %s is held, skip update check", cache_dir.name) + continue with self.in_package_context(cache_dir.name, local_versions.get(cache_dir.name)): try: source = RemoteSource( @@ -162,6 +180,9 @@ class UpdateHandler(PackageInfo, Cleaner): continue # we don't add packages automatically if local.remote.is_remote: continue # avoid checking AUR packages + if local.base in ignore_list: + self.logger.info("package %s is held, skip update check", local.base) + continue if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs): self.reporter.set_pending(local.base) diff --git a/ahriman-core/tests/ahriman/core/repository/test_update_handler.py b/ahriman-core/tests/ahriman/core/repository/test_update_handler.py index ae39c5b1..61374253 100644 --- a/ahriman-core/tests/ahriman/core/repository/test_update_handler.py +++ b/ahriman-core/tests/ahriman/core/repository/test_update_handler.py @@ -14,6 +14,20 @@ from ahriman.models.package_source import PackageSource from ahriman.models.remote_source import RemoteSource +def test_updates_ignored(update_handler: UpdateHandler, package_ahriman: Package, + package_python_schedule: Package, mocker: MockerFixture) -> None: + """ + must combine configured ignored packages and held packages only + """ + update_handler._ignore_list = ["ignored-package"] + mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[ + (package_ahriman, BuildStatus(is_held=True)), + (package_python_schedule, BuildStatus(is_held=False)), + ]) + + assert update_handler._updates_ignored() == {"ignored-package", package_ahriman.base} + + def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ @@ -194,6 +208,18 @@ def test_updates_dependencies(update_handler: UpdateHandler, package_ahriman: Pa pytest.helpers.anyvar(str, True)) +def test_updates_dependencies_ignore(update_handler: UpdateHandler, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must skip held packages when checking broken dependencies + """ + mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", + return_value=[(package_ahriman, BuildStatus(is_held=True))]) + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) + + assert update_handler.updates_dependencies([]) == [] + + def test_updates_dependencies_skip_unknown(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ @@ -249,6 +275,19 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, calculate_version=True) +def test_updates_local_ignore(update_handler: UpdateHandler, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must skip held packages before fetching local sources + """ + mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", + return_value=[(package_ahriman, BuildStatus(is_held=True))]) + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) + mocker.patch("pathlib.Path.iterdir", return_value=[Path(package_ahriman.base)]) + + assert update_handler.updates_local(vcs=True) == [] + + def test_updates_local_ignore_vcs(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ @@ -306,6 +345,22 @@ def test_updates_local_with_failures(update_handler: UpdateHandler, package_ahri assert not update_handler.updates_local(vcs=True) +def test_updates_local_held_metadata(update_handler: UpdateHandler, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must respect the held base even when the source directory has a different name + """ + package_ahriman.remote = RemoteSource(source=PackageSource.Local) + mocker.patch.object(update_handler.reporter, "package_get", + return_value=[(package_ahriman, BuildStatus(is_held=True))]) + mocker.patch.object(update_handler, "packages", return_value=[package_ahriman]) + mocker.patch("pathlib.Path.iterdir", return_value=[Path("different-base")]) + mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") + mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman) + + assert update_handler.updates_local(vcs=True) == [] + + def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixture) -> None: """ requesting manual updates must clear packages directory diff --git a/docs/architecture.rst b/docs/architecture.rst index 4a473869..2f0b64c8 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -257,6 +257,8 @@ There are few ways for packages to be marked as out-of-date and hence requiring #. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one. #. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-date as well. +Held packages and bases listed in ``build.ignore_packages`` are excluded from automatic AUR, official-repository, local-source, and broken-dependency update checks. These checks leave their package status unchanged. Manual updates and explicit rebuilds remain available. + Update packages ^^^^^^^^^^^^^^^