diff --git a/0020-Mark-all-protected-packages-as-user-installed-for-al.patch b/0020-Mark-all-protected-packages-as-user-installed-for-al.patch new file mode 100644 index 0000000..ea7c2e7 --- /dev/null +++ b/0020-Mark-all-protected-packages-as-user-installed-for-al.patch @@ -0,0 +1,34 @@ +From 9d5c8b965294d52a0feda55f274d033a279fe090 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Fri, 9 Jan 2026 07:26:34 +0100 +Subject: [PATCH 1/5] Mark all protected packages as user installed for all + transactions + +Closes: https://github.com/rpm-software-management/dnf/issues/2192 +--- + libdnf/goal/Goal.cpp | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/libdnf/goal/Goal.cpp b/libdnf/goal/Goal.cpp +index 9882273c..f2b20ced 100644 +--- a/libdnf/goal/Goal.cpp ++++ b/libdnf/goal/Goal.cpp +@@ -987,6 +987,15 @@ Goal::jobLength() + bool + Goal::run(DnfGoalActions flags) + { ++ // Automatically mark all protected packages as user installed. ++ // When a protected package is installed as a dependency it can block ++ // removal of the last package that depends on it (because the protected ++ // package cannot be removed, not even as an unused dependency). ++ // To prevent this and still correctly resolve dependencies of the protected ++ // packages mark them all as user installed. ++ if (pImpl->protectedPkgs) { ++ userInstalled(*pImpl->protectedPkgs); ++ } + auto job = pImpl->constructJob(flags); + pImpl->actions = static_cast(pImpl->actions | flags); + int ret = pImpl->solve(job->getQueue(), flags); +-- +2.53.0 + diff --git a/0021-Add-filterUnneededExtraUserinstalled-and-Python-vers.patch b/0021-Add-filterUnneededExtraUserinstalled-and-Python-vers.patch new file mode 100644 index 0000000..837b5b1 --- /dev/null +++ b/0021-Add-filterUnneededExtraUserinstalled-and-Python-vers.patch @@ -0,0 +1,149 @@ +From 93d8bfbe36913804d8963f04e4d9491d6bfa25a1 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Mon, 26 Jan 2026 14:07:37 +0100 +Subject: [PATCH 2/5] Add `filterUnneededExtraUserinstalled` and Python version + to the API + +This can be used to get unneeded packages with for example protected +packages extra marked as userinstalled. +--- + libdnf/sack/query.cpp | 18 +++++++++++---- + libdnf/sack/query.hpp | 1 + + python/hawkey/query-py.cpp | 47 ++++++++++++++++++++++++++++++++++++++ + 3 files changed, 62 insertions(+), 4 deletions(-) + +diff --git a/libdnf/sack/query.cpp b/libdnf/sack/query.cpp +index e4682927..c1c50ec1 100644 +--- a/libdnf/sack/query.cpp ++++ b/libdnf/sack/query.cpp +@@ -830,7 +830,7 @@ private: + void filterUpdownByPriority(const Filter & f, Map *m); + void filterUpdownAble(const Filter &f, Map *m); + void filterDataiterator(const Filter & f, Map *m); +- int filterUnneededOrSafeToRemove(const Swdb &swdb, bool debug_solver, bool safeToRemove); ++ int filterUnneededOrSafeToRemove(const Swdb &swdb, bool debug_solver, bool safeToRemove, PackageSet *extra_userinstalled); + void obsoletesByPriority(Pool * pool, Solvable * candidate, Map * m, const Map * target, int obsprovides); + + bool isGlob(const std::vector &matches) const; +@@ -2245,7 +2245,7 @@ Query::Impl::filterDataiterator(const Filter & f, Map *m) + } + + int +-Query::Impl::filterUnneededOrSafeToRemove(const Swdb &swdb, bool debug_solver, bool safeToRemove) ++Query::Impl::filterUnneededOrSafeToRemove(const Swdb &swdb, bool debug_solver, bool safeToRemove, PackageSet *extra_userinstalled) + { + apply(); + Goal goal(sack); +@@ -2260,6 +2260,10 @@ Query::Impl::filterUnneededOrSafeToRemove(const Swdb &swdb, bool debug_solver, b + } + goal.userInstalled(*userInstalled); + ++ if (extra_userinstalled != NULL) { ++ goal.userInstalled(*extra_userinstalled); ++ } ++ + int ret1 = goal.run(DNF_NONE); + if (ret1) + return -1; +@@ -2575,13 +2579,19 @@ Query::filterDuplicated() + int + Query::filterUnneeded(const Swdb &swdb, bool debug_solver) + { +- return pImpl->filterUnneededOrSafeToRemove(swdb, debug_solver, false); ++ return pImpl->filterUnneededOrSafeToRemove(swdb, debug_solver, false, NULL); + } + + int + Query::filterSafeToRemove(const Swdb &swdb, bool debug_solver) + { +- return pImpl->filterUnneededOrSafeToRemove(swdb, debug_solver, true); ++ return pImpl->filterUnneededOrSafeToRemove(swdb, debug_solver, true, NULL); ++} ++ ++int ++Query::filterUnneededExtraUserinstalled(const Swdb &swdb, PackageSet &extra_userinstalled, bool debug_solver) ++{ ++ return pImpl->filterUnneededOrSafeToRemove(swdb, debug_solver, false, &extra_userinstalled); + } + + void +diff --git a/libdnf/sack/query.hpp b/libdnf/sack/query.hpp +index 306b24e3..a5ed7745 100644 +--- a/libdnf/sack/query.hpp ++++ b/libdnf/sack/query.hpp +@@ -177,6 +177,7 @@ public: + void filterRecent(const long unsigned int recent_limit); + void filterDuplicated(); + int filterUnneeded(const Swdb &swdb, bool debug_solver); ++ int filterUnneededExtraUserinstalled(const Swdb &swdb, PackageSet &extra_userinstalled, bool debug_solver); + int filterSafeToRemove(const Swdb &swdb, bool debug_solver); + void getAdvisoryPkgs(int cmpType, std::vector & advisoryPkgs); + void filterUserInstalled(const Swdb &swdb); +diff --git a/python/hawkey/query-py.cpp b/python/hawkey/query-py.cpp +index a25a0d6b..b30cc22f 100644 +--- a/python/hawkey/query-py.cpp ++++ b/python/hawkey/query-py.cpp +@@ -819,6 +819,52 @@ filter_unneeded(PyObject *self, PyObject *args, PyObject *kwds) try + return filter_unneeded_or_safe_to_remove(self, args, kwds, false); + } CATCH_TO_PYTHON + ++static PyObject * ++filter_unneeded_extra_userinstalled(PyObject *self, PyObject *args, PyObject *kwds) try ++{ ++ const char *kwlist[] = {"swdb", "extra_userinstalled", "debug_solver", NULL}; ++ PyObject *pySwdb; ++ PyObject *extra_userinstalled; ++ PyObject *debug_solver = NULL; ++ ++ if (!PyArg_ParseTupleAndKeywords( ++ args, kwds, "OO|O!", (char **)kwlist, &pySwdb, &extra_userinstalled, &PyBool_Type, &debug_solver)) { ++ return NULL; ++ } ++ ++ UniquePtrPyObject thisPySwdb(PyObject_GetAttrString(pySwdb, "this")); ++ auto swigSwdb = reinterpret_cast< SwdbSwigPyObject * >(thisPySwdb.get()); ++ if (swigSwdb == nullptr) { ++ PyErr_SetString(PyExc_SystemError, "Unable to parse SwigPyObject"); ++ return NULL; ++ } ++ libdnf::Swdb *swdb = swigSwdb->ptr; ++ if (swdb == NULL) { ++ PyErr_SetString(PyExc_SystemError, "Unable to parse swig object"); ++ return NULL; ++ } ++ ++ HyQuery query = ((_QueryObject *) self)->query; ++ auto extra_userinstalled_pset = pyseq_to_packageset(extra_userinstalled, query->getSack()); ++ if (!extra_userinstalled_pset) { ++ PyErr_SetString(PyExc_SystemError, "Unable to parse SwigPyObject: extra_userinstalled PackageSet"); ++ return NULL; ++ } ++ ++ std::unique_ptr self_query_copy(new libdnf::Query(*query)); ++ gboolean c_debug_solver = debug_solver != NULL && PyObject_IsTrue(debug_solver); ++ ++ int ret = self_query_copy->filterUnneededExtraUserinstalled(*swdb, *extra_userinstalled_pset, c_debug_solver); ++ if (ret == -1) { ++ PyErr_SetString(PyExc_SystemError, "Unable to provide query with unneded filter"); ++ return NULL; ++ } ++ ++ PyObject *final_query = queryToPyObject(self_query_copy.release(), ((_QueryObject *) self)->sack, ++ Py_TYPE(self)); ++ return final_query; ++} CATCH_TO_PYTHON ++ + static PyObject * + q_add(_QueryObject *self, PyObject *list) try + { +@@ -1081,6 +1127,7 @@ static struct PyMethodDef query_methods[] = { + {"_nevra", (PyCFunction)add_nevra_or_other_filter, METH_VARARGS, NULL}, + {"_recent", (PyCFunction)add_filter_recent, METH_VARARGS, NULL}, + {"_unneeded", (PyCFunction)filter_unneeded, METH_KEYWORDS|METH_VARARGS, NULL}, ++ {"_unneeded_extra_userinstalled", (PyCFunction)filter_unneeded_extra_userinstalled, METH_KEYWORDS|METH_VARARGS, NULL}, + {"_safe_to_remove", (PyCFunction)filter_safe_to_remove, METH_KEYWORDS|METH_VARARGS, NULL}, + {NULL} /* sentinel */ + }; +-- +2.53.0 + diff --git a/0022-Describe-all-problems-even-when-there-are-protected-.patch b/0022-Describe-all-problems-even-when-there-are-protected-.patch new file mode 100644 index 0000000..89f3789 --- /dev/null +++ b/0022-Describe-all-problems-even-when-there-are-protected-.patch @@ -0,0 +1,31 @@ +From 3da41b9d881fc5f0bb8c09b4d1ae4dde9626dd1e Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 27 Jan 2026 13:00:50 -0500 +Subject: [PATCH 4/5] Describe all problems even when there are protected + removals + +Previously, if a transaction involved removal (or dependency break) of a +protected package, only the protected package problem would be described +by `describeProblemRules`. Information about excluded or versionlocked +packages would be missing. + +For: RHEL-146507 +--- + libdnf/goal/Goal.cpp | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/libdnf/goal/Goal.cpp b/libdnf/goal/Goal.cpp +index f2b20ced..47d9888e 100644 +--- a/libdnf/goal/Goal.cpp ++++ b/libdnf/goal/Goal.cpp +@@ -1108,7 +1108,6 @@ Goal::describeProblemRules(unsigned i, bool pkgs) + auto problem = pImpl->describeProtectedRemoval(); + if (!problem.empty()) { + output.push_back(std::move(problem)); +- return output; + } + auto solv = pImpl->solv; + +-- +2.53.0 + diff --git a/0023-Clearer-error-for-protected-package-broken-dependenc.patch b/0023-Clearer-error-for-protected-package-broken-dependenc.patch new file mode 100644 index 0000000..f4ab826 --- /dev/null +++ b/0023-Clearer-error-for-protected-package-broken-dependenc.patch @@ -0,0 +1,60 @@ +From 35f16bc89b567460dd7857f632a38790839baca9 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 27 Jan 2026 13:19:14 -0500 +Subject: [PATCH 5/5] Clearer error for protected package broken dependencies + +The error message now distinguishes removal of a protected package from +breaking dependencies of protected packages. +--- + libdnf/goal/Goal.cpp | 17 +++++++++++------ + 1 file changed, 11 insertions(+), 6 deletions(-) + +diff --git a/libdnf/goal/Goal.cpp b/libdnf/goal/Goal.cpp +index 47d9888e..4544cd9e 100644 +--- a/libdnf/goal/Goal.cpp ++++ b/libdnf/goal/Goal.cpp +@@ -1752,11 +1752,11 @@ Goal::Impl::protectedInRemovals() + std::string + Goal::Impl::describeProtectedRemoval() + { +- std::string message(_("The operation would result in removing" +- " the following protected packages: ")); + Pool * pool = solv->pool; + + if (removalOfProtected && removalOfProtected->size()) { ++ const std::string removal_message(_("The operation would result in removing " ++ "the following protected packages: ")); + Id id = -1; + std::vector names; + while((id = removalOfProtected->next(id)) != -1) { +@@ -1766,9 +1766,13 @@ Goal::Impl::describeProtectedRemoval() + if (names.empty()) { + return {}; + } +- return message + std::accumulate(std::next(names.begin()), names.end(), +- std::string(names[0]), [](std::string a, std::string b) { return a + ", " + b; }); ++ return removal_message + ++ std::accumulate(std::next(names.begin()), names.end(), std::string(names[0]), ++ [](std::string a, std::string b) { return a + ", " + b; }); + } ++ ++ const std::string broken_dependency_message(_("The operation would result in broken " ++ "dependencies for the following protected packages: ")); + auto pset = brokenDependencyAllPkgs(DNF_PACKAGE_STATE_INSTALLED); + Id id = -1; + Id protected_kernel = protectedRunningKernel(); +@@ -1781,8 +1785,9 @@ Goal::Impl::describeProtectedRemoval() + } + if (names.empty()) + return {}; +- return message + std::accumulate(std::next(names.begin()), names.end(), std::string(names[0]), +- [](std::string a, std::string b) { return a + ", " + b; }); ++ return broken_dependency_message + ++ std::accumulate(std::next(names.begin()), names.end(), std::string(names[0]), ++ [](std::string a, std::string b) { return a + ", " + b; }); + } + + } +-- +2.53.0 + diff --git a/0024-Goal-set-protected-as-userinstalled-only-for-the-tem.patch b/0024-Goal-set-protected-as-userinstalled-only-for-the-tem.patch new file mode 100644 index 0000000..c57b6a0 --- /dev/null +++ b/0024-Goal-set-protected-as-userinstalled-only-for-the-tem.patch @@ -0,0 +1,57 @@ +From 83d3c39df84e96860efd35c2e18903dd16d050c7 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Tue, 10 Feb 2026 13:04:42 +0100 +Subject: [PATCH] Goal: set protected as userinstalled only for the temporary + job + +This way the goal's `staging` is not affected. +--- + libdnf/goal/Goal.cpp | 25 ++++++++++++++++--------- + 1 file changed, 16 insertions(+), 9 deletions(-) + +diff --git a/libdnf/goal/Goal.cpp b/libdnf/goal/Goal.cpp +index 4544cd9e..c624fb41 100644 +--- a/libdnf/goal/Goal.cpp ++++ b/libdnf/goal/Goal.cpp +@@ -987,15 +987,6 @@ Goal::jobLength() + bool + Goal::run(DnfGoalActions flags) + { +- // Automatically mark all protected packages as user installed. +- // When a protected package is installed as a dependency it can block +- // removal of the last package that depends on it (because the protected +- // package cannot be removed, not even as an unused dependency). +- // To prevent this and still correctly resolve dependencies of the protected +- // packages mark them all as user installed. +- if (pImpl->protectedPkgs) { +- userInstalled(*pImpl->protectedPkgs); +- } + auto job = pImpl->constructJob(flags); + pImpl->actions = static_cast(pImpl->actions | flags); + int ret = pImpl->solve(job->getQueue(), flags); +@@ -1444,6 +1435,22 @@ Goal::Impl::constructJob(DnfGoalActions flags) + + allowUninstallAllButProtected(job->getQueue(), flags); + ++ // Automatically mark all protected packages as user installed. ++ // When a protected package is installed as a dependency it can block ++ // removal of the last package that depends on it (because the protected ++ // package cannot be removed, not even as an unused dependency). ++ // To prevent this and still correctly resolve dependencies of the protected ++ // packages mark them all as user installed. ++ if (protectedPkgs) { ++ Id id = -1; ++ while (true) { ++ id = protectedPkgs->next(id); ++ if (id == -1) ++ break; ++ queue_push2(job->getQueue(), SOLVER_SOLVABLE|SOLVER_USERINSTALLED, id); ++ } ++ } ++ + if (flags & DNF_VERIFY) + job->pushBack(SOLVER_VERIFY|SOLVER_SOLVABLE_ALL, 0); + +-- +2.53.0 + diff --git a/libdnf.spec b/libdnf.spec index abb4bab..cb97b62 100644 --- a/libdnf.spec +++ b/libdnf.spec @@ -56,7 +56,7 @@ Name: libdnf Version: %{libdnf_major_version}.%{libdnf_minor_version}.%{libdnf_micro_version} -Release: 13%{?dist}.alma.1 +Release: 14%{?dist}.alma.1 Summary: Library providing simplified C and Python API to libsolv License: LGPL-2.1-or-later URL: https://github.com/rpm-software-management/libdnf @@ -80,6 +80,11 @@ Patch16: 0016-history-DB-Add-persistence-column.patch Patch17: 0017-MergedTransaction-listPersistences.patch Patch18: 0018-conf-Add-usr_drift_protected_paths.patch Patch19: 0019-fix-compare-RPMItem-in-transaction-with-rpmvercmp.patch +Patch20: 0020-Mark-all-protected-packages-as-user-installed-for-al.patch +Patch21: 0021-Add-filterUnneededExtraUserinstalled-and-Python-vers.patch +Patch22: 0022-Describe-all-problems-even-when-there-are-protected-.patch +Patch23: 0023-Clearer-error-for-protected-package-broken-dependenc.patch +Patch24: 0024-Goal-set-protected-as-userinstalled-only-for-the-tem.patch # AlmaLinux Patch Patch1001: 0001-Add-link-to-AlmaLinux-bugtracker.patch @@ -328,11 +333,17 @@ popd %endif %changelog -* Sat Feb 07 2026 Eduard Abdullin - 0.73.1-13.alma.1 +* Tue Feb 17 2026 Eduard Abdullin - 0.73.1-14.alma.1 - Add x86_64_v2 to arch_map - Add link to AlmaLinux bugtracker - Fix tests on x86_64_v2 +* Mon Feb 09 2026 Ales Matej - 0.73.1-14 +- Mark all protected packages as user installed for all transactions (RHEL-128445) +- Add `filterUnneededExtraUserinstalled` and Python version to the API +- Describe all problems even when there are protected removals (RHEL-146507) +- Clearer error for protected package broken dependencies + * Tue Feb 03 2026 Matej Focko - 0.73.1-13 - Fix comparison of RPM items in the transaction (RHEL-128443)