File PackageKit-1.3.3-Initial-DNF5-Backend.patch of Package PackageKit
From 02c96b6d49025921d7b6757e11bc2102c92ff8f5 Mon Sep 17 00:00:00 2001
From: Neal Gompa <neal@gompa.dev>
Date: Tue, 30 Dec 2025 19:44:19 -0500
Subject: [PATCH 1/4] backends: Initial implementation of the DNF5 backend
This provides a complete implementation for PackageKit to leverage
libdnf5 as the backing package management interface for RPM-based
operating systems.
---
AUTHORS | 3 +
backends/dnf5/README.md | 19 +
backends/dnf5/dnf5-backend-thread.cpp | 722 ++++++++++++++++++
backends/dnf5/dnf5-backend-thread.hpp | 28 +
backends/dnf5/dnf5-backend-utils.cpp | 593 ++++++++++++++
backends/dnf5/dnf5-backend-utils.hpp | 91 +++
backends/dnf5/dnf5-backend-vendor-fedora.cpp | 35 +
backends/dnf5/dnf5-backend-vendor-mageia.cpp | 47 ++
.../dnf5/dnf5-backend-vendor-openmandriva.cpp | 47 ++
.../dnf5/dnf5-backend-vendor-opensuse.cpp | 44 ++
backends/dnf5/dnf5-backend-vendor-rosa.cpp | 44 ++
backends/dnf5/dnf5-backend-vendor.hpp | 25 +
backends/dnf5/meson.build | 27 +
backends/dnf5/pk-backend-dnf5.cpp | 367 +++++++++
meson_options.txt | 15 +-
15 files changed, 2106 insertions(+), 1 deletion(-)
create mode 100644 backends/dnf5/README.md
create mode 100644 backends/dnf5/dnf5-backend-thread.cpp
create mode 100644 backends/dnf5/dnf5-backend-thread.hpp
create mode 100644 backends/dnf5/dnf5-backend-utils.cpp
create mode 100644 backends/dnf5/dnf5-backend-utils.hpp
create mode 100644 backends/dnf5/dnf5-backend-vendor-fedora.cpp
create mode 100644 backends/dnf5/dnf5-backend-vendor-mageia.cpp
create mode 100644 backends/dnf5/dnf5-backend-vendor-openmandriva.cpp
create mode 100644 backends/dnf5/dnf5-backend-vendor-opensuse.cpp
create mode 100644 backends/dnf5/dnf5-backend-vendor-rosa.cpp
create mode 100644 backends/dnf5/dnf5-backend-vendor.hpp
create mode 100644 backends/dnf5/meson.build
create mode 100644 backends/dnf5/pk-backend-dnf5.cpp
diff --git a/AUTHORS b/AUTHORS
index 3f624dce2..aa4c6dcbb 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -13,6 +13,9 @@ Backend: dnf
Richard Hughes <richard@hughsie.com>
Neal Gompa <ngompa13@gmail.com>
+Backend: dnf5
+ Neal Gompa <neal@gompa.dev>
+
Backend: eopkg
Joey Riches <josephriches@gmail.com> <joey@getsol.us>
diff --git a/backends/dnf5/README.md b/backends/dnf5/README.md
new file mode 100644
index 000000000..276123d6d
--- /dev/null
+++ b/backends/dnf5/README.md
@@ -0,0 +1,19 @@
+DNF5 PackageKit Backend
+----------------------
+
+It uses the following libraries:
+
+ * libdnf5 : for the actual package management functions
+ * rpm : for actually installing the packages on the system
+
+For AppStream data, the libdnf5 AppStream plugin is used.
+
+These are some key file locations:
+
+* /var/cache/PackageKit/$releasever/metadata/ : Used to store the repository metadata
+* /var/cache/PackageKit/$releasever/metadata/*/packages : Used for cached packages
+* $libdir/packagekit-backend/ : location of PackageKit backend objects
+
+Things we haven't yet decided:
+
+* How to access comps data
diff --git a/backends/dnf5/dnf5-backend-thread.cpp b/backends/dnf5/dnf5-backend-thread.cpp
new file mode 100644
index 000000000..30e71fe00
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-thread.cpp
@@ -0,0 +1,722 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-thread.hpp"
+#include "dnf5-backend-utils.hpp"
+#include <libdnf5/base/goal.hpp>
+#include <libdnf5/comps/environment/query.hpp>
+#include <libdnf5/comps/group/query.hpp>
+#include <libdnf5/advisory/advisory_query.hpp>
+#include <libdnf5/rpm/reldep_list.hpp>
+#include <libdnf5/base/transaction.hpp>
+#include <libdnf5/repo/package_downloader.hpp>
+#include <libdnf5/conf/config_parser.hpp>
+#include <packagekit-glib2/pk-common-private.h>
+#include <packagekit-glib2/pk-update-detail.h>
+#include <rpm/rpmlib.h>
+#include <glib/gstdio.h>
+#include <filesystem>
+#include <map>
+
+void
+dnf5_query_thread (PkBackendJob *job, GVariant *params, gpointer user_data)
+{
+ PkBackend *backend = (PkBackend *) pk_backend_job_get_backend (job);
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ PkRoleEnum role = pk_backend_job_get_role (job);
+
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ try {
+ if (role == PK_ROLE_ENUM_SEARCH_NAME || role == PK_ROLE_ENUM_SEARCH_DETAILS || role == PK_ROLE_ENUM_SEARCH_FILE || role == PK_ROLE_ENUM_RESOLVE || role == PK_ROLE_ENUM_WHAT_PROVIDES) {
+ PkBitfield filters;
+ g_auto(GStrv) values = NULL;
+ g_variant_get (params, "(t^as)", &filters, &values);
+
+ g_debug("Query role=%d, filters=%lu", role, (unsigned long)filters);
+
+ std::vector<libdnf5::rpm::Package> results;
+ libdnf5::rpm::PackageQuery query(*priv->base);
+
+ std::vector<std::string> search_terms;
+ for (int i = 0; values[i]; i++) search_terms.push_back(values[i]);
+
+ if (role == PK_ROLE_ENUM_SEARCH_NAME) {
+ query.filter_name(search_terms, libdnf5::sack::QueryCmp::ICONTAINS);
+ } else if (role == PK_ROLE_ENUM_SEARCH_FILE) {
+ query.filter_file(search_terms);
+ } else if (role == PK_ROLE_ENUM_RESOLVE) {
+ // For RESOLVE, filter by name FIRST, then apply other filters
+ // This matches the old DNF backend behavior
+ for (const auto &term : search_terms)
+ g_debug("Resolving package name: %s", term.c_str());
+ query.filter_name(search_terms, libdnf5::sack::QueryCmp::EQ);
+ g_debug("After filter_name: query has %zu packages", query.size());
+ } else if (role == PK_ROLE_ENUM_WHAT_PROVIDES) {
+ std::vector<std::string> provides;
+ for (const auto &term : search_terms) {
+ provides.push_back(term);
+ provides.push_back("gstreamer0.10(" + term + ")");
+ provides.push_back("gstreamer1(" + term + ")");
+ provides.push_back("font(" + term + ")");
+ provides.push_back("mimehandler(" + term + ")");
+ provides.push_back("postscriptdriver(" + term + ")");
+ provides.push_back("plasma4(" + term + ")");
+ provides.push_back("plasma5(" + term + ")");
+ provides.push_back("language(" + term + ")");
+ }
+ query.filter_provides(provides);
+ } else if (role == PK_ROLE_ENUM_SEARCH_DETAILS) {
+ libdnf5::rpm::PackageQuery query_sum(*priv->base);
+ query.filter_description(search_terms, libdnf5::sack::QueryCmp::ICONTAINS);
+ query_sum.filter_summary(search_terms, libdnf5::sack::QueryCmp::ICONTAINS);
+ // Apply filters to both queries before merging
+ dnf5_apply_filters(*priv->base, query, filters);
+ dnf5_apply_filters(*priv->base, query_sum, filters);
+ for (auto p : query_sum) {
+ if (dnf5_package_filter(p, filters))
+ results.push_back(p);
+ }
+ }
+
+ // Apply filters AFTER filtering by name/file/provides for most roles
+ // Exception: SEARCH_DETAILS already applied filters above
+ if (role != PK_ROLE_ENUM_SEARCH_DETAILS) {
+ g_debug("Before dnf5_apply_filters: query has %zu packages", query.size());
+ dnf5_apply_filters(*priv->base, query, filters);
+ g_debug("After dnf5_apply_filters: query has %zu packages", query.size());
+ }
+
+ // For RESOLVE, we've already applied all necessary filters via dnf5_apply_filters
+ // Don't apply dnf5_package_filter again as it causes incorrect filtering
+ if (role == PK_ROLE_ENUM_RESOLVE) {
+ for (auto p : query) {
+ results.push_back(p);
+ }
+ } else {
+ for (auto p : query) {
+ if (dnf5_package_filter(p, filters))
+ results.push_back(p);
+ }
+ }
+ g_debug("Final results: %zu packages", results.size());
+ dnf5_sort_and_emit(job, results);
+
+
+
+ } else if (role == PK_ROLE_ENUM_DEPENDS_ON || role == PK_ROLE_ENUM_REQUIRED_BY) {
+ PkBitfield filters;
+ g_auto(GStrv) package_ids = NULL;
+ gboolean recursive;
+ g_variant_get (params, "(t^asb)", &filters, &package_ids, &recursive);
+
+ auto input_pkgs = dnf5_resolve_package_ids(*priv->base, package_ids);
+ std::vector<libdnf5::rpm::Package> results;
+ for (const auto &pkg : input_pkgs) {
+ auto deps = dnf5_process_dependency(*priv->base, pkg, role, recursive);
+ for (auto dep : deps) {
+ if (dnf5_package_filter(dep, filters))
+ results.push_back(dep);
+ }
+ }
+ dnf5_sort_and_emit(job, results);
+
+ } else if (role == PK_ROLE_ENUM_GET_PACKAGES || role == PK_ROLE_ENUM_GET_UPDATES) {
+ PkBitfield filters;
+ g_variant_get (params, "(t)", &filters);
+
+ libdnf5::rpm::PackageQuery query(*priv->base);
+ dnf5_apply_filters(*priv->base, query, filters);
+
+ if (role == PK_ROLE_ENUM_GET_UPDATES) {
+ libdnf5::Goal goal(*priv->base);
+ if (dnf5_force_distupgrade_on_upgrade (*priv->base))
+ goal.add_rpm_distro_sync();
+ else
+ goal.add_rpm_upgrade();
+ auto trans = goal.resolve();
+
+ std::vector<libdnf5::rpm::Package> update_pkgs;
+ for (const auto &item : trans.get_transaction_packages()) {
+ auto action = item.get_action();
+ if (action == libdnf5::transaction::TransactionItemAction::UPGRADE || action == libdnf5::transaction::TransactionItemAction::INSTALL) {
+ update_pkgs.push_back(item.get_package());
+ }
+ }
+
+ libdnf5::advisory::AdvisoryQuery adv_query(*priv->base);
+ libdnf5::rpm::PackageSet pkg_set(priv->base->get_weak_ptr());
+ for (const auto &pkg : update_pkgs) pkg_set.add(pkg);
+ adv_query.filter_packages(pkg_set);
+
+ std::map<std::string, libdnf5::advisory::Advisory> pkg_to_advisory;
+ for (const auto &adv_pkg : adv_query.get_advisory_packages_sorted(pkg_set)) {
+ std::string key = adv_pkg.get_name() + ";" + adv_pkg.get_evr() + ";" + adv_pkg.get_arch();
+ pkg_to_advisory.emplace(key, adv_pkg.get_advisory());
+ }
+
+ for (const auto &pkg : update_pkgs) {
+ if (dnf5_package_filter(pkg, filters)) {
+ PkInfoEnum info = PK_INFO_ENUM_UNKNOWN;
+ PkInfoEnum severity = PK_INFO_ENUM_UNKNOWN;
+
+ std::string key = pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch();
+ auto it = pkg_to_advisory.find(key);
+ if (it != pkg_to_advisory.end()) {
+ info = dnf5_advisory_kind_to_info_enum(it->second.get_type());
+ severity = dnf5_update_severity_to_enum(it->second.get_severity());
+ }
+ dnf5_emit_pkg(job, pkg, info, severity);
+ }
+ }
+ } else {
+ std::vector<libdnf5::rpm::Package> results;
+ for (auto p : query) {
+ if (dnf5_package_filter(p, filters))
+ results.push_back(p);
+ }
+ dnf5_sort_and_emit(job, results);
+ }
+ } else if (role == PK_ROLE_ENUM_GET_DETAILS || role == PK_ROLE_ENUM_GET_FILES || role == PK_ROLE_ENUM_DOWNLOAD_PACKAGES || role == PK_ROLE_ENUM_GET_UPDATE_DETAIL) {
+ g_auto(GStrv) package_ids = NULL;
+ if (role == PK_ROLE_ENUM_DOWNLOAD_PACKAGES) {
+ gchar *directory = NULL;
+ g_variant_get (params, "(^as&s)", &package_ids, &directory);
+ auto pkgs = dnf5_resolve_package_ids(*priv->base, package_ids);
+ libdnf5::repo::PackageDownloader downloader(*priv->base);
+ uint64_t total_download_size = 0;
+ for (const auto &pkg : pkgs) total_download_size += pkg.get_download_size();
+
+ priv->base->set_download_callbacks(std::make_unique<Dnf5DownloadCallbacks>(job, total_download_size));
+ for (auto &pkg : pkgs) {
+ dnf5_emit_pkg(job, pkg, PK_INFO_ENUM_DOWNLOADING);
+ downloader.add(pkg, directory);
+ }
+ downloader.download();
+
+ std::vector<char*> files_c;
+ for (auto &pkg : pkgs) {
+ std::string path = pkg.get_package_path();
+ if (!path.empty()) files_c.push_back(g_strdup(path.c_str()));
+ }
+ files_c.push_back(nullptr);
+ pk_backend_job_files (job, NULL, files_c.data());
+ for (auto p : files_c) g_free(p);
+ pk_backend_job_finished (job);
+ return;
+ } else {
+ g_variant_get (params, "(^as)", &package_ids);
+ }
+
+ auto pkgs = dnf5_resolve_package_ids(*priv->base, package_ids);
+ if (role == PK_ROLE_ENUM_GET_UPDATE_DETAIL) {
+ libdnf5::advisory::AdvisoryQuery adv_query(*priv->base);
+ libdnf5::rpm::PackageSet pkg_set(priv->base->get_weak_ptr());
+ for (const auto &pkg : pkgs) pkg_set.add(pkg);
+ adv_query.filter_packages(pkg_set);
+
+ std::map<std::string, libdnf5::advisory::AdvisoryPackage> pkg_to_adv_pkg;
+ for (const auto &adv_pkg : adv_query.get_advisory_packages_sorted(pkg_set)) {
+ std::string key = adv_pkg.get_name() + ";" + adv_pkg.get_evr() + ";" + adv_pkg.get_arch();
+ pkg_to_adv_pkg.emplace(key, adv_pkg);
+ }
+
+ g_autoptr(GPtrArray) update_details = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ for (auto &pkg : pkgs) {
+ std::string repo_id = pkg.get_repo_id();
+ if (pkg.get_install_time() > 0) repo_id = "installed";
+ std::string pid = pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch() + ";" + repo_id;
+
+ std::string key = pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch();
+ auto it = pkg_to_adv_pkg.find(key);
+ if (it != pkg_to_adv_pkg.end()) {
+ auto advisory = it->second.get_advisory();
+ g_autoptr(PkUpdateDetail) item = pk_update_detail_new ();
+
+ std::vector<std::string> bugzilla_urls, cve_urls, vendor_urls;
+ for (const auto &ref : advisory.get_references()) {
+ if (ref.get_url().empty()) continue;
+ if (ref.get_type() == "bugzilla") bugzilla_urls.push_back(ref.get_url());
+ else if (ref.get_type() == "cve") cve_urls.push_back(ref.get_url());
+ else if (ref.get_type() == "vendor") vendor_urls.push_back(ref.get_url());
+ }
+
+ auto buildtime = advisory.get_buildtime();
+ g_autoptr(GDateTime) dt = g_date_time_new_from_unix_local(buildtime);
+ g_autofree gchar *date_str = g_date_time_format(dt, "%Y-%m-%d");
+
+ PkRestartEnum restart = PK_RESTART_ENUM_NONE;
+ if (it->second.get_reboot_suggested()) restart = PK_RESTART_ENUM_SYSTEM;
+ else if (it->second.get_restart_suggested()) restart = PK_RESTART_ENUM_APPLICATION;
+ else if (it->second.get_relogin_suggested()) restart = PK_RESTART_ENUM_SESSION;
+
+ g_auto(GStrv) bugzilla_strv = (gchar **) g_new0 (gchar *, bugzilla_urls.size() + 1);
+ for (size_t i = 0; i < bugzilla_urls.size(); i++) bugzilla_strv[i] = g_strdup(bugzilla_urls[i].c_str());
+ g_auto(GStrv) cve_strv = (gchar **) g_new0 (gchar *, cve_urls.size() + 1);
+ for (size_t i = 0; i < cve_urls.size(); i++) cve_strv[i] = g_strdup(cve_urls[i].c_str());
+ g_auto(GStrv) vendor_strv = (gchar **) g_new0 (gchar *, vendor_urls.size() + 1);
+ for (size_t i = 0; i < vendor_urls.size(); i++) vendor_strv[i] = g_strdup(vendor_urls[i].c_str());
+
+ g_object_set (item,
+ "package-id", pid.c_str(),
+ "bugzilla-urls", bugzilla_strv,
+ "cve-urls", cve_strv,
+ "vendor-urls", vendor_strv,
+ "update-text", advisory.get_description().c_str(),
+ "restart", restart,
+ "state", PK_UPDATE_STATE_ENUM_STABLE,
+ "issued", date_str,
+ "updated", date_str,
+ NULL);
+ g_ptr_array_add (update_details, g_steal_pointer (&item));
+ }
+ }
+ pk_backend_job_update_details (job, update_details);
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ for (auto &pkg : pkgs) {
+ std::string repo_id = pkg.get_repo_id();
+ if (pkg.get_install_time() > 0) repo_id = "installed";
+ std::string pid = pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch() + ";" + repo_id;
+
+ if (role == PK_ROLE_ENUM_GET_DETAILS) {
+ std::string license = pkg.get_license();
+ if (license.empty()) license = "unknown";
+ pk_backend_job_details(job, pid.c_str(), pkg.get_summary().c_str(), license.c_str(), PK_GROUP_ENUM_UNKNOWN, pkg.get_description().c_str(), pkg.get_url().c_str(), pkg.get_install_size(), pkg.get_download_size());
+ } else if (role == PK_ROLE_ENUM_GET_FILES) {
+ auto files_vec = pkg.get_files();
+ std::vector<char*> files_c;
+ for (const auto &f : files_vec) files_c.push_back(const_cast<char*>(f.c_str()));
+ files_c.push_back(nullptr);
+ pk_backend_job_files(job, pid.c_str(), files_c.data());
+ }
+ }
+ } else if (role == PK_ROLE_ENUM_GET_DETAILS_LOCAL || role == PK_ROLE_ENUM_GET_FILES_LOCAL) {
+ g_auto(GStrv) files = NULL;
+ g_variant_get (params, "(^as)", &files);
+ libdnf5::Base local_base;
+ local_base.load_config();
+ local_base.get_config().get_pkg_gpgcheck_option().set(false);
+ local_base.setup();
+ std::vector<std::string> paths;
+ for (int i = 0; files[i]; i++) paths.push_back(files[i]);
+ auto added = local_base.get_repo_sack()->add_cmdline_packages(paths);
+ for (const auto &pair : added) {
+ const auto &pkg = pair.second;
+ std::string pid = pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch() + ";" + (pkg.get_repo_id().empty() ? "local" : pkg.get_repo_id());
+ if (role == PK_ROLE_ENUM_GET_DETAILS_LOCAL) {
+ pk_backend_job_details(job, pid.c_str(), pkg.get_summary().c_str(), pkg.get_license().c_str(), PK_GROUP_ENUM_UNKNOWN, pkg.get_description().c_str(), pkg.get_url().c_str(), pkg.get_install_size(), 0);
+ } else {
+ auto files_vec = pkg.get_files();
+ std::vector<char*> files_c;
+ for (const auto &f : files_vec) files_c.push_back(const_cast<char*>(f.c_str()));
+ files_c.push_back(nullptr);
+ pk_backend_job_files(job, pid.c_str(), files_c.data());
+ }
+ }
+ } else if (role == PK_ROLE_ENUM_GET_REPO_LIST) {
+ PkBitfield filters;
+ g_variant_get (params, "(t)", &filters);
+ libdnf5::repo::RepoQuery query(*priv->base);
+ for (auto repo : query) {
+ std::string id = repo->get_id();
+ if (id == "@System" || id == "@commandline") continue;
+ if (!dnf5_backend_pk_repo_filter(*repo, filters)) continue;
+ pk_backend_job_repo_detail(job, id.c_str(), repo->get_name().c_str(), repo->is_enabled());
+ }
+ }
+ } catch (const std::exception &e) {
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "%s", e.what());
+ }
+ pk_backend_job_finished (job);
+}
+
+void
+dnf5_transaction_thread (PkBackendJob *job, GVariant *params, gpointer user_data)
+{
+ PkBackend *backend = (PkBackend *) pk_backend_job_get_backend (job);
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ PkRoleEnum role = pk_backend_job_get_role (job);
+
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ try {
+ if (role == PK_ROLE_ENUM_UPGRADE_SYSTEM) {
+ gchar *distro_id = NULL;
+ PkUpgradeKindEnum upgrade_kind;
+ PkBitfield transaction_flags;
+ g_variant_get (params, "(t&su)", &transaction_flags, &distro_id, &upgrade_kind);
+ if (distro_id) {
+ dnf5_setup_base(priv, TRUE, TRUE, distro_id);
+
+ g_debug("Checking repositories for system upgrade to %s:", distro_id);
+ // ... logging code ...
+ libdnf5::repo::RepoQuery query(*priv->base);
+ for (auto repo : query) {
+ // Check if baseurl contains the correct version
+ auto baseurl = repo->get_config().get_baseurl_option().get_value();
+ std::string url_str = baseurl.empty() ? "null" : baseurl[0];
+ g_debug("Repo %s: enabled=%d, url=%s",
+ repo->get_id().c_str(),
+ repo->is_enabled(),
+ url_str.c_str());
+ }
+ }
+ }
+
+ libdnf5::Goal goal(*priv->base);
+ PkBitfield transaction_flags = 0;
+
+ if (role == PK_ROLE_ENUM_INSTALL_PACKAGES || role == PK_ROLE_ENUM_UPDATE_PACKAGES || role == PK_ROLE_ENUM_REMOVE_PACKAGES) {
+ g_auto(GStrv) package_ids = NULL;
+ if (role == PK_ROLE_ENUM_REMOVE_PACKAGES) {
+ gboolean allow_deps, autoremove;
+ g_variant_get (params, "(t^asbb)", &transaction_flags, &package_ids, &allow_deps, &autoremove);
+ if (autoremove) priv->base->get_config().get_clean_requirements_on_remove_option().set(true);
+ } else {
+ g_variant_get (params, "(t^as)", &transaction_flags, &package_ids);
+ }
+
+ auto pkgs = dnf5_resolve_package_ids(*priv->base, package_ids);
+ if (pkgs.empty() && role != PK_ROLE_ENUM_UPDATE_PACKAGES) {
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_PACKAGE_NOT_FOUND, "No packages found");
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ for (auto &pkg : pkgs) {
+ if (role == PK_ROLE_ENUM_INSTALL_PACKAGES) goal.add_rpm_install(pkg);
+ else if (role == PK_ROLE_ENUM_REMOVE_PACKAGES) goal.add_rpm_remove(pkg);
+ else if (role == PK_ROLE_ENUM_UPDATE_PACKAGES) goal.add_rpm_upgrade(pkg);
+ }
+ if (role == PK_ROLE_ENUM_UPDATE_PACKAGES && pkgs.empty()) {
+ if (dnf5_force_distupgrade_on_upgrade (*priv->base))
+ goal.add_rpm_distro_sync();
+ else
+ goal.add_rpm_upgrade();
+ }
+
+ } else if (role == PK_ROLE_ENUM_INSTALL_FILES) {
+ g_auto(GStrv) full_paths = NULL;
+ g_variant_get (params, "(t^as)", &transaction_flags, &full_paths);
+ std::vector<std::string> paths;
+ for (int i = 0; full_paths[i]; i++) paths.push_back(full_paths[i]);
+ auto added = priv->base->get_repo_sack()->add_cmdline_packages(paths);
+ for (const auto &p : added) goal.add_rpm_install(p.second);
+ } else if (role == PK_ROLE_ENUM_UPGRADE_SYSTEM) {
+ const gchar *distro_id = NULL;
+ PkUpgradeKindEnum upgrade_kind;
+ g_variant_get (params, "(t&su)", &transaction_flags, &distro_id, &upgrade_kind);
+
+ // System upgrades require allowing erasure of packages (e.g. obsoletes)
+ // and downgrades if necessary to match repo versions.
+ goal.set_allow_erasing(true);
+ goal.add_rpm_distro_sync();
+ // System upgrades require processing groups to be upgraded
+ libdnf5::comps::GroupQuery q_groups(*priv->base);
+ q_groups.filter_installed(true);
+ for (const auto & grp : q_groups) {
+ goal.add_group_upgrade(grp.get_groupid());
+ }
+ libdnf5::comps::EnvironmentQuery q_environments(*priv->base);
+ q_environments.filter_installed(true);
+ for (const auto & env : q_environments) {
+ goal.add_group_upgrade(env.get_environmentid());
+ }
+ } else if (role == PK_ROLE_ENUM_REPAIR_SYSTEM) {
+ g_variant_get (params, "(t)", &transaction_flags);
+ if (pk_bitfield_contain (transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) {
+ pk_backend_job_finished (job);
+ return;
+ }
+ std::filesystem::path rpm_db_path("/var/lib/rpm");
+ if (std::filesystem::exists(rpm_db_path) && std::filesystem::is_directory(rpm_db_path)) {
+ for (const auto& entry : std::filesystem::directory_iterator(rpm_db_path)) {
+ if (entry.is_regular_file() && entry.path().filename().string().starts_with("__db.")) {
+ std::filesystem::remove(entry.path());
+ }
+ }
+ }
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_QUERY);
+ auto trans = goal.resolve();
+ auto problems = trans.get_transaction_problems();
+ if (!problems.empty()) {
+ std::string msg;
+ for (const auto &p : problems) msg += p + "; ";
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_DEP_RESOLUTION_FAILED, "%s", msg.c_str());
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ g_debug("Resolved transaction has %zu packages", trans.get_transaction_packages().size());
+ for (const auto &item : trans.get_transaction_packages()) {
+ g_debug("Transaction item: %s - %d", item.get_package().get_name().c_str(), (int)item.get_action());
+ }
+
+ if (pk_bitfield_contain (transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) {
+ std::set<std::string> continuing_names;
+ for (const auto &item : trans.get_transaction_packages()) {
+ auto action = item.get_action();
+ if (action == libdnf5::transaction::TransactionItemAction::UPGRADE ||
+ action == libdnf5::transaction::TransactionItemAction::DOWNGRADE ||
+ action == libdnf5::transaction::TransactionItemAction::REINSTALL) {
+ continuing_names.insert(item.get_package().get_name());
+ }
+ }
+
+ for (const auto &item : trans.get_transaction_packages()) {
+ auto action = item.get_action();
+ PkInfoEnum info = PK_INFO_ENUM_UNKNOWN;
+ if (action == libdnf5::transaction::TransactionItemAction::INSTALL) info = PK_INFO_ENUM_INSTALLING;
+ else if (action == libdnf5::transaction::TransactionItemAction::UPGRADE) info = PK_INFO_ENUM_UPDATING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REMOVE) info = PK_INFO_ENUM_REMOVING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REINSTALL) info = PK_INFO_ENUM_REINSTALLING;
+ else if (action == libdnf5::transaction::TransactionItemAction::DOWNGRADE) info = PK_INFO_ENUM_DOWNGRADING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REPLACED) {
+ if (continuing_names.find(item.get_package().get_name()) == continuing_names.end()) {
+ info = PK_INFO_ENUM_OBSOLETING;
+ }
+ }
+
+ if (info != PK_INFO_ENUM_UNKNOWN)
+ dnf5_emit_pkg(job, item.get_package(), info);
+ }
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_DOWNLOAD);
+
+ uint64_t total_download_size = 0;
+ for (const auto &item : trans.get_transaction_packages()) {
+ if (libdnf5::transaction::transaction_item_action_is_inbound(item.get_action())) {
+ auto pkg = item.get_package();
+ if (!pkg.is_available_locally()) {
+ total_download_size += pkg.get_download_size();
+ }
+ }
+ }
+
+ priv->base->set_download_callbacks(std::make_unique<Dnf5DownloadCallbacks>(job, total_download_size));
+ trans.download();
+
+ if (pk_bitfield_contain (transaction_flags, PK_TRANSACTION_FLAG_ENUM_ONLY_DOWNLOAD)) {
+ // Iterate over transaction items and report them as if they were being processed
+ for (const auto &item : trans.get_transaction_packages()) {
+ auto action = item.get_action();
+ PkInfoEnum info = PK_INFO_ENUM_UNKNOWN;
+
+ if (action == libdnf5::transaction::TransactionItemAction::INSTALL) info = PK_INFO_ENUM_INSTALLING;
+ else if (action == libdnf5::transaction::TransactionItemAction::UPGRADE) info = PK_INFO_ENUM_UPDATING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REMOVE) info = PK_INFO_ENUM_REMOVING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REINSTALL) info = PK_INFO_ENUM_REINSTALLING;
+ else if (action == libdnf5::transaction::TransactionItemAction::DOWNGRADE) info = PK_INFO_ENUM_DOWNGRADING;
+
+ if (info != PK_INFO_ENUM_UNKNOWN)
+ dnf5_emit_pkg(job, item.get_package(), info);
+ }
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_RUNNING);
+ trans.set_callbacks(std::make_unique<Dnf5TransactionCallbacks>(job));
+ auto res = trans.run();
+ g_debug("Transaction run result: %s", libdnf5::base::Transaction::transaction_result_to_string(res).c_str());
+ if (res != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) {
+ std::string msg;
+ for (const auto &p : trans.get_transaction_problems()) msg += p + "; ";
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "Transaction failed: %s", msg.c_str());
+ }
+
+ // Post-transaction base re-initialization to ensure state consistency
+ dnf5_setup_base (priv);
+
+ } catch (const std::exception &e) {
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "%s", e.what());
+ }
+ pk_backend_job_finished (job);
+}
+
+void
+dnf5_repo_thread (PkBackendJob *job, GVariant *params, gpointer user_data)
+{
+ PkBackend *backend = (PkBackend *) pk_backend_job_get_backend (job);
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ PkRoleEnum role = pk_backend_job_get_role (job);
+
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ try {
+ if (role == PK_ROLE_ENUM_REPO_ENABLE || role == PK_ROLE_ENUM_REPO_SET_DATA) {
+ gchar *repo_id = NULL;
+ const gchar *parameter, *value;
+ if (role == PK_ROLE_ENUM_REPO_ENABLE) {
+ gboolean enabled;
+ g_variant_get (params, "(&sb)", &repo_id, &enabled);
+ parameter = "enabled";
+ value = enabled ? "1" : "0";
+ } else {
+ g_variant_get (params, "(&s&s&s)", &repo_id, ¶meter, &value);
+ }
+
+ libdnf5::repo::RepoQuery query(*priv->base);
+ query.filter_id(repo_id);
+ for (auto repo : query) {
+ if (g_strcmp0(parameter, "enabled") == 0) {
+ bool enable = (g_strcmp0(value, "1") == 0 || g_strcmp0(value, "true") == 0);
+ if (repo->is_enabled() == enable) {
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_REPO_ALREADY_SET, "Repo already in state");
+ pk_backend_job_finished (job);
+ return;
+ }
+ if (enable) repo->enable(); else repo->disable();
+ libdnf5::ConfigParser parser;
+ parser.read(repo->get_repo_file_path());
+ parser.set_value(repo_id, "enabled", value);
+ parser.write(repo->get_repo_file_path(), false);
+ }
+ }
+ dnf5_setup_base (priv);
+ } else if (role == PK_ROLE_ENUM_REPO_REMOVE) {
+ gchar *repo_id = NULL;
+ gboolean autoremove;
+ PkBitfield transaction_flags;
+ g_variant_get (params, "(t&sb)", &transaction_flags, &repo_id, &autoremove);
+
+ libdnf5::repo::RepoQuery query(*priv->base);
+ query.filter_id(repo_id);
+ std::string repo_file;
+ for (auto repo : query) {
+ repo_file = repo->get_repo_file_path();
+ break;
+ }
+
+ if (repo_file.empty()) {
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_REPO_NOT_FOUND, "Repo %s not found", repo_id);
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ g_debug("Repo %s uses file %s", repo_id, repo_file.c_str());
+
+ // Find all repos in the same file to track all packages that should be removed
+ std::vector<std::string> all_repo_ids;
+ libdnf5::repo::RepoQuery all_repos_query(*priv->base);
+ for (auto repo : all_repos_query) {
+ if (repo->get_repo_file_path() == repo_file) {
+ all_repo_ids.push_back(repo->get_id());
+ }
+ }
+
+ libdnf5::Goal goal(*priv->base);
+
+ // Remove the owner package(s) of the repo file
+ libdnf5::rpm::PackageQuery owner_query(*priv->base);
+ owner_query.filter_installed();
+ owner_query.filter_file({repo_file});
+
+ if (owner_query.empty()) {
+ g_debug("filter_file failed, trying provides for %s", repo_file.c_str());
+ owner_query.filter_provides(repo_file);
+ }
+
+ for (auto pkg : owner_query) {
+ g_debug("Adding owner package %s to removal goal", pkg.get_name().c_str());
+ goal.add_remove(pkg.get_name());
+ }
+
+ // If autoremove is true, also remove packages installed from these repos
+ if (autoremove) {
+ libdnf5::rpm::PackageQuery inst_query(*priv->base);
+ inst_query.filter_installed();
+ for (auto pkg : inst_query) {
+ std::string from_repo = pkg.get_from_repo_id();
+ for (const auto &id : all_repo_ids) {
+ if (from_repo == id) {
+ goal.add_remove(pkg.get_name());
+ break;
+ }
+ }
+ }
+ // Also enable unused dependency removal
+ priv->base->get_config().get_clean_requirements_on_remove_option().set(true);
+ }
+
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_QUERY);
+ auto trans = goal.resolve();
+ g_debug("Transaction has %zu packages", trans.get_transaction_packages().size());
+ if (!trans.get_transaction_packages().empty()) {
+ for (const auto &item : trans.get_transaction_packages()) {
+ auto action = item.get_action();
+ PkInfoEnum info = PK_INFO_ENUM_UNKNOWN;
+ if (action == libdnf5::transaction::TransactionItemAction::INSTALL || action == libdnf5::transaction::TransactionItemAction::UPGRADE) info = PK_INFO_ENUM_INSTALLING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REMOVE || action == libdnf5::transaction::TransactionItemAction::REPLACED) info = PK_INFO_ENUM_REMOVING;
+ else if (action == libdnf5::transaction::TransactionItemAction::REINSTALL) info = PK_INFO_ENUM_REINSTALLING;
+ else if (action == libdnf5::transaction::TransactionItemAction::DOWNGRADE) info = PK_INFO_ENUM_DOWNGRADING;
+
+ dnf5_emit_pkg(job, item.get_package(), info);
+ }
+ }
+
+ if (!trans.get_transaction_problems().empty()) {
+ std::string msg;
+ for (const auto &p : trans.get_transaction_problems()) msg += p + "; ";
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_DEP_RESOLUTION_FAILED, "%s", msg.c_str());
+ pk_backend_job_finished (job);
+ return;
+ }
+
+ if (role == PK_ROLE_ENUM_REPO_REMOVE || !pk_bitfield_contain (transaction_flags, PK_TRANSACTION_FLAG_ENUM_SIMULATE)) {
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_DOWNLOAD);
+ g_debug("Starting transaction download...");
+ trans.download();
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_RUNNING);
+ g_debug("Starting transaction execution...");
+ trans.set_description("PackageKit: repo-remove " + std::string(repo_id));
+ auto res = trans.run();
+ g_debug("Transaction run result: %s", libdnf5::base::Transaction::transaction_result_to_string(res).c_str());
+ if (res != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) {
+ std::vector<std::string> problems = trans.get_transaction_problems();
+ std::string msg;
+ for (const auto &p : problems) msg += p + "; ";
+ g_warning("Transaction failed: %s", msg.c_str());
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "Transaction failed: %s", msg.c_str());
+ } else {
+ g_debug("Transaction completed successfully");
+ }
+ dnf5_setup_base (priv);
+ } else {
+ g_debug("Simulation completed, finishing job...");
+ }
+ }
+ } catch (const std::exception &e) {
+ g_warning("Exception in dnf5_repo_thread: %s", e.what());
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "%s", e.what());
+ }
+ g_debug("Calling pk_backend_job_finished in dnf5_repo_thread");
+ pk_backend_job_finished (job);
+}
diff --git a/backends/dnf5/dnf5-backend-thread.hpp b/backends/dnf5/dnf5-backend-thread.hpp
new file mode 100644
index 000000000..99c683150
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-thread.hpp
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <pk-backend.h>
+#include <glib.h>
+
+void dnf5_query_thread(PkBackendJob *job, GVariant *params, gpointer user_data);
+void dnf5_transaction_thread(PkBackendJob *job, GVariant *params, gpointer user_data);
+void dnf5_repo_thread(PkBackendJob *job, GVariant *params, gpointer user_data);
diff --git a/backends/dnf5/dnf5-backend-utils.cpp b/backends/dnf5/dnf5-backend-utils.cpp
new file mode 100644
index 000000000..8ae910fc2
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-utils.cpp
@@ -0,0 +1,593 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-utils.hpp"
+#include <packagekit-glib2/pk-common-private.h>
+#include <packagekit-glib2/pk-update-detail.h>
+#include <libdnf5/conf/config_parser.hpp>
+#include <libdnf5/conf/const.hpp>
+#include <libdnf5/logger/logger.hpp>
+#include <libdnf5/rpm/arch.hpp>
+#include <libdnf5/repo/package_downloader.hpp>
+#include <libdnf5/base/goal.hpp>
+#include <libdnf5/advisory/advisory_query.hpp>
+#include <libdnf5/rpm/reldep_list.hpp>
+#include <libdnf5/base/transaction.hpp>
+#include <rpm/rpmlib.h>
+#include <glib/gstdio.h>
+#include <algorithm>
+#include <set>
+#include <queue>
+#include <filesystem>
+#include <map>
+#include "dnf5-backend-vendor.hpp"
+
+void
+dnf5_setup_base (PkBackendDnf5Private *priv, gboolean refresh, gboolean force, const char *releasever)
+{
+ priv->base = std::make_unique<libdnf5::Base>();
+
+ priv->base->load_config();
+
+ auto &config = priv->base->get_config();
+ if (priv->conf != NULL) {
+ g_autofree gchar *destdir = g_key_file_get_string (priv->conf, "Daemon", "DestDir", NULL);
+ if (destdir != NULL) {
+ config.get_installroot_option().set(libdnf5::Option::Priority::COMMANDLINE, destdir);
+ }
+
+ gboolean keep_cache = g_key_file_get_boolean (priv->conf, "Daemon", "KeepCache", NULL);
+ config.get_keepcache_option().set(libdnf5::Option::Priority::COMMANDLINE, keep_cache != FALSE);
+
+ g_autofree gchar *distro_version = NULL;
+ if (releasever == NULL) {
+ g_autoptr(GError) error = NULL;
+ distro_version = pk_get_distro_version_id (&error);
+ } else {
+ distro_version = g_strdup(releasever);
+ }
+
+ if (distro_version != NULL) {
+ priv->base->get_vars()->set("releasever", distro_version);
+ const char *root = (destdir != NULL) ? destdir : "/";
+ g_autofree gchar *cache_dir = g_build_filename (root, "/var/cache/PackageKit", distro_version, "metadata", NULL);
+ g_debug("Using cachedir: %s", cache_dir);
+ config.get_cachedir_option().set(libdnf5::Option::Priority::COMMANDLINE, cache_dir);
+ }
+
+ auto &optional_metadata_types = config.get_optional_metadata_types_option();
+ auto &optional_metadata_types_setting = optional_metadata_types.get_value();
+ if (!optional_metadata_types_setting.contains(libdnf5::METADATA_TYPE_ALL)) {
+ // Ensure all required repodata types are downloaded
+ if(!optional_metadata_types_setting.contains(libdnf5::METADATA_TYPE_COMPS)) {
+ optional_metadata_types.add_item(libdnf5::Option::Priority::RUNTIME, libdnf5::METADATA_TYPE_COMPS);
+ }
+ if(!optional_metadata_types_setting.contains(libdnf5::METADATA_TYPE_UPDATEINFO)) {
+ optional_metadata_types.add_item(libdnf5::Option::Priority::RUNTIME, libdnf5::METADATA_TYPE_UPDATEINFO);
+ }
+ if(!optional_metadata_types_setting.contains(libdnf5::METADATA_TYPE_APPSTREAM)) {
+ optional_metadata_types.add_item(libdnf5::Option::Priority::RUNTIME, libdnf5::METADATA_TYPE_APPSTREAM);
+ }
+ }
+
+ // Always assume yes to avoid interactive prompts failing the transaction
+ // TODO: Drop this once InstallSignature is implemented
+ config.get_assumeyes_option().set(libdnf5::Option::Priority::COMMANDLINE, true);
+ }
+
+ priv->base->setup();
+
+ // Ensure releasever is set AFTER setup() because setup() might run auto-detection and overwrite it.
+ if (priv->conf != NULL) {
+ g_autofree gchar *distro_version = NULL;
+ if (releasever == NULL) {
+ g_autoptr(GError) error = NULL;
+ distro_version = pk_get_distro_version_id (&error);
+ } else {
+ distro_version = g_strdup(releasever);
+ }
+ if (distro_version != NULL) {
+ priv->base->get_vars()->set("releasever", distro_version);
+ }
+ }
+
+ auto repo_sack = priv->base->get_repo_sack();
+ repo_sack->create_repos_from_system_configuration();
+ repo_sack->get_system_repo();
+
+ if (refresh && force) {
+ libdnf5::repo::RepoQuery query(*priv->base);
+ for (auto repo : query) {
+ if (repo->is_enabled()) {
+ g_debug("Expiring repository metadata: %s", repo->get_id().c_str());
+ repo->expire();
+ }
+ }
+ }
+
+ g_debug("Loading repositories");
+ repo_sack->load_repos();
+
+ libdnf5::repo::RepoQuery query(*priv->base);
+ query.filter_enabled(true);
+ for (auto repo : query) {
+ g_debug("Enabled repository: %s", repo->get_id().c_str());
+ }
+}
+
+void
+dnf5_refresh_cache(PkBackendDnf5Private *priv, gboolean force)
+{
+ dnf5_setup_base(priv, TRUE, force);
+}
+
+PkInfoEnum
+dnf5_advisory_kind_to_info_enum (const std::string &type)
+{
+ if (type == "security")
+ return PK_INFO_ENUM_SECURITY;
+ if (type == "bugfix")
+ return PK_INFO_ENUM_BUGFIX;
+ if (type == "enhancement")
+ return PK_INFO_ENUM_ENHANCEMENT;
+ if (type == "newpackage")
+ return PK_INFO_ENUM_NORMAL;
+ return PK_INFO_ENUM_UNKNOWN;
+}
+
+PkInfoEnum
+dnf5_update_severity_to_enum (const std::string &severity)
+{
+ if (severity == "low")
+ return PK_INFO_ENUM_LOW;
+ if (severity == "moderate")
+ return PK_INFO_ENUM_NORMAL;
+ if (severity == "important")
+ return PK_INFO_ENUM_IMPORTANT;
+ if (severity == "critical")
+ return PK_INFO_ENUM_CRITICAL;
+ return PK_INFO_ENUM_UNKNOWN;
+}
+
+bool
+dnf5_force_distupgrade_on_upgrade (libdnf5::Base &base)
+{
+ std::vector<std::string> distroverpkg_names = { "system-release", "distribution-release" };
+ std::vector<std::string> distupgrade_provides = { "system-upgrade(dsync)", "product-upgrade() = dup" };
+
+ libdnf5::rpm::PackageQuery query(base);
+ query.filter_installed();
+ query.filter_name(distroverpkg_names);
+ query.filter_provides(distupgrade_provides);
+
+ return !query.empty();
+}
+
+bool
+dnf5_repo_is_devel (const libdnf5::repo::Repo &repo)
+{
+ std::string id = repo.get_id();
+ return (id.ends_with("-debuginfo") || id.ends_with("-debugsource") || id.ends_with("-devel"));
+}
+
+bool
+dnf5_repo_is_source (const libdnf5::repo::Repo &repo)
+{
+ std::string id = repo.get_id();
+ return id.ends_with("-source");
+}
+
+bool
+dnf5_repo_is_supported (const libdnf5::repo::Repo &repo)
+{
+ return dnf5_validate_supported_repo(repo.get_id());
+}
+
+bool
+dnf5_backend_pk_repo_filter (const libdnf5::repo::Repo &repo, PkBitfield filters)
+{
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_DEVELOPMENT) && !dnf5_repo_is_devel (repo))
+ return false;
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_DEVELOPMENT) && dnf5_repo_is_devel (repo))
+ return false;
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_SOURCE) && !dnf5_repo_is_source (repo))
+ return false;
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_SOURCE) && dnf5_repo_is_source (repo))
+ return false;
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_INSTALLED) && !repo.is_enabled())
+ return false;
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_INSTALLED) && repo.is_enabled())
+ return false;
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_SUPPORTED) && !dnf5_repo_is_supported (repo))
+ return false;
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_SUPPORTED) && dnf5_repo_is_supported (repo))
+ return false;
+
+ return true;
+}
+
+bool
+dnf5_package_is_gui (const libdnf5::rpm::Package &pkg)
+{
+ for (const auto &provide : pkg.get_provides()) {
+ std::string name = provide.get_name();
+ if (name.starts_with("application("))
+ return true;
+ }
+ return false;
+}
+
+bool
+dnf5_package_filter (const libdnf5::rpm::Package &pkg, PkBitfield filters)
+{
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_GUI) && !dnf5_package_is_gui (pkg))
+ return false;
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_GUI) && dnf5_package_is_gui (pkg))
+ return false;
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_DOWNLOADED) && !pkg.is_available_locally())
+ return false;
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_DOWNLOADED) && pkg.is_available_locally())
+ return false;
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_DEVELOPMENT) ||
+ pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_DEVELOPMENT) ||
+ pk_bitfield_contain (filters, PK_FILTER_ENUM_SOURCE) ||
+ pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_SOURCE) ||
+ pk_bitfield_contain (filters, PK_FILTER_ENUM_SUPPORTED) ||
+ pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_SUPPORTED)) {
+ auto repo_weak = pkg.get_repo();
+ if (repo_weak.is_valid()) {
+ if (!dnf5_backend_pk_repo_filter(*repo_weak, filters))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+std::vector<libdnf5::rpm::Package>
+dnf5_process_dependency (libdnf5::Base &base, const libdnf5::rpm::Package &pkg, PkRoleEnum role, gboolean recursive)
+{
+ std::vector<libdnf5::rpm::Package> results;
+ std::set<std::string> visited;
+ std::queue<libdnf5::rpm::Package> queue;
+ queue.push(pkg);
+ visited.insert(pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch());
+
+ while (!queue.empty()) {
+ auto curr = queue.front();
+ queue.pop();
+ libdnf5::rpm::ReldepList reldeps(base);
+ if (role == PK_ROLE_ENUM_DEPENDS_ON) reldeps = curr.get_requires();
+ else reldeps = curr.get_provides();
+
+ for (const auto &reldep : reldeps) {
+ std::string req = reldep.to_string();
+ libdnf5::rpm::PackageQuery query(base);
+ if (role == PK_ROLE_ENUM_DEPENDS_ON) query.filter_provides(req);
+ else query.filter_requires(req);
+
+ // Filter for latest version and supported architectures to avoid duplicates
+ // for available packages
+ query.filter_latest_evr();
+ query.filter_arch(libdnf5::rpm::get_supported_arches());
+
+ for (const auto &res : query) {
+ std::string res_nevra = res.get_name() + ";" + res.get_evr() + ";" + res.get_arch();
+ if (visited.find(res_nevra) == visited.end()) {
+ visited.insert(res_nevra);
+ results.push_back(res);
+ if (recursive) queue.push(res);
+ }
+ }
+ }
+ }
+ return results;
+}
+
+void
+dnf5_emit_pkg (PkBackendJob *job, const libdnf5::rpm::Package &pkg, PkInfoEnum info, PkInfoEnum severity)
+{
+ if (info == PK_INFO_ENUM_UNKNOWN) {
+ info = PK_INFO_ENUM_AVAILABLE;
+ if (pkg.get_install_time() > 0) {
+ info = PK_INFO_ENUM_INSTALLED;
+ }
+ }
+
+ std::string evr = pkg.get_evr();
+ std::string repo_id = pkg.get_repo_id();
+ if (pkg.get_install_time() > 0) {
+ repo_id = "installed";
+ }
+
+ std::string package_id = pkg.get_name() + ";" + evr + ";" + pkg.get_arch() + ";" + repo_id;
+ if (severity != PK_INFO_ENUM_UNKNOWN) {
+ pk_backend_job_package_full (job, info, package_id.c_str(), pkg.get_summary().c_str(), severity);
+ } else {
+ pk_backend_job_package (job, info, package_id.c_str(), pkg.get_summary().c_str());
+ }
+}
+
+void
+dnf5_sort_and_emit (PkBackendJob *job, std::vector<libdnf5::rpm::Package> &pkgs)
+{
+ std::sort(pkgs.begin(), pkgs.end(), [](const libdnf5::rpm::Package &a, const libdnf5::rpm::Package &b) {
+ bool a_installed = (a.get_install_time() > 0);
+ bool b_installed = (b.get_install_time() > 0);
+ if (a_installed != b_installed) return a_installed;
+ if (a.get_name() != b.get_name()) return a.get_name() < b.get_name();
+ if (a.get_arch() != b.get_arch()) return a.get_arch() < b.get_arch();
+ return a.get_evr() < b.get_evr();
+ });
+
+ std::set<std::string> seen_nevras;
+ for (auto &pkg : pkgs) {
+ std::string nevra = pkg.get_name() + ";" + pkg.get_evr() + ";" + pkg.get_arch();
+ if (seen_nevras.find(nevra) == seen_nevras.end()) {
+ dnf5_emit_pkg(job, pkg);
+ seen_nevras.insert(nevra);
+ }
+ }
+}
+
+void
+dnf5_apply_filters (libdnf5::Base &base, libdnf5::rpm::PackageQuery &query, PkBitfield filters)
+{
+ gboolean installed = pk_bitfield_contain (filters, PK_FILTER_ENUM_INSTALLED);
+ gboolean available = pk_bitfield_contain (filters, PK_FILTER_ENUM_NOT_INSTALLED);
+
+ if (installed && !available) {
+ query.filter_installed();
+ } else if (!installed && available) {
+ query.filter_available();
+ }
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_ARCH)) {
+ auto vars = base.get_vars();
+ if (vars.is_valid()) {
+ std::string arch = vars->get_value("arch");
+ if (!arch.empty()) {
+ query.filter_arch({arch, "noarch"});
+ } else {
+ query.filter_arch(libdnf5::rpm::get_supported_arches());
+ }
+ }
+ }
+
+ if (pk_bitfield_contain (filters, PK_FILTER_ENUM_NEWEST)) {
+ query.filter_latest_evr();
+ }
+}
+
+std::vector<libdnf5::rpm::Package>
+dnf5_resolve_package_ids(libdnf5::Base &base, gchar **package_ids)
+{
+ std::vector<libdnf5::rpm::Package> pkgs;
+ if (!package_ids) return pkgs;
+
+ for (int i = 0; package_ids[i] != NULL; i++) {
+ // Check if this is a simple package name (no semicolons) or a full package ID
+ if (strchr(package_ids[i], ';') == NULL) {
+ // Simple package name - search by name and get latest available
+ try {
+ g_debug("Resolving simple package name: %s", package_ids[i]);
+ libdnf5::rpm::PackageQuery query(base);
+ query.filter_name(std::string(package_ids[i]), libdnf5::sack::QueryCmp::EQ);
+ query.filter_available();
+ query.filter_latest_evr();
+ query.filter_arch(libdnf5::rpm::get_supported_arches());
+
+
+ if (!query.empty()) {
+ for (auto pkg : query) {
+ g_debug("Found package: name=%s, evr=%s, arch=%s, repo=%s",
+ pkg.get_name().c_str(), pkg.get_evr().c_str(),
+ pkg.get_arch().c_str(), pkg.get_repo_id().c_str());
+ pkgs.push_back(pkg);
+ break; // Take the first match
+ }
+ } else {
+ g_debug("No available package found for name: %s", package_ids[i]);
+ }
+ } catch (const std::exception &e) {
+ g_debug("Exception resolving package name %s: %s", package_ids[i], e.what());
+ }
+ continue;
+ }
+
+ // Full package ID - use existing logic
+ g_auto(GStrv) split = pk_package_id_split(package_ids[i]);
+ if (!split) continue;
+
+ try {
+ libdnf5::rpm::PackageQuery query(base);
+ g_debug("Resolving package ID: name=%s, version=%s, arch=%s, repo=%s",
+ split[PK_PACKAGE_ID_NAME], split[PK_PACKAGE_ID_VERSION],
+ split[PK_PACKAGE_ID_ARCH], split[PK_PACKAGE_ID_DATA]);
+ query.filter_name(split[PK_PACKAGE_ID_NAME]);
+ query.filter_evr(split[PK_PACKAGE_ID_VERSION]);
+ query.filter_arch(split[PK_PACKAGE_ID_ARCH]);
+
+ if (g_strcmp0(split[PK_PACKAGE_ID_DATA], "installed") == 0) {
+ query.filter_installed();
+ } else {
+ query.filter_repo_id(split[PK_PACKAGE_ID_DATA]);
+ }
+
+ if (query.empty()) {
+ g_debug("No exact match for ID: %s. Listing similar packages...", package_ids[i]);
+ libdnf5::rpm::PackageQuery fallback(base);
+ fallback.filter_name(split[PK_PACKAGE_ID_NAME]);
+ for (const auto &p : fallback) {
+ g_debug("Found similar package: name=%s, evr=%s, arch=%s, repo=%s",
+ p.get_name().c_str(), p.get_evr().c_str(), p.get_arch().c_str(), p.get_repo_id().c_str());
+ }
+ }
+
+ for (auto pkg : query) {
+ pkgs.push_back(pkg);
+ break;
+ }
+ } catch (const std::exception &e) {
+ g_debug("Exception resolving package ID %s: %s", package_ids[i], e.what());
+ }
+ }
+ return pkgs;
+}
+
+
+void
+dnf5_remove_old_cache_directories (PkBackend *backend, const gchar *release_ver)
+{
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ g_assert (priv->conf != NULL);
+
+ /* cache cleanup disabled? */
+ if (g_key_file_get_boolean (priv->conf, "Daemon", "KeepCache", NULL)) {
+ g_debug ("KeepCache config option set; skipping old cache directory cleanup");
+ return;
+ }
+
+ /* only do cache cleanup for regular installs */
+ g_autofree gchar *destdir = g_key_file_get_string (priv->conf, "Daemon", "DestDir", NULL);
+ if (destdir != NULL) {
+ g_debug ("DestDir config option set; skipping old cache directory cleanup");
+ return;
+ }
+
+ std::filesystem::path cache_path("/var/cache/PackageKit");
+ if (!std::filesystem::exists(cache_path) || !std::filesystem::is_directory(cache_path))
+ return;
+
+ /* look at each subdirectory */
+ for (const auto &entry : std::filesystem::directory_iterator(cache_path)) {
+ if (!entry.is_directory())
+ continue;
+
+ std::string filename = entry.path().filename().string();
+
+ /* is the version older than the current release ver? */
+ if (rpmvercmp (filename.c_str(), release_ver) < 0) {
+ g_debug ("removing old cache directory %s", entry.path().c_str());
+ std::error_code ec;
+ std::filesystem::remove_all(entry.path(), ec);
+ if (ec)
+ g_warning ("failed to remove directory %s: %s", entry.path().c_str(), ec.message().c_str());
+ }
+ }
+}
+
+Dnf5DownloadCallbacks::Dnf5DownloadCallbacks(PkBackendJob *job, uint64_t total_size)
+ : job(job), total_size(total_size), finished_size(0), next_id(1) {}
+
+void *
+Dnf5DownloadCallbacks::add_new_download(void *user_data, const char *description, double total_to_download)
+{
+ std::lock_guard<std::mutex> lock(mutex);
+ void *id = reinterpret_cast<void*>(next_id++);
+ item_progress[id] = 0;
+ return id;
+}
+
+int
+Dnf5DownloadCallbacks::progress(void *user_cb_data, double total_to_download, double downloaded)
+{
+ std::lock_guard<std::mutex> lock(mutex);
+ item_progress[user_cb_data] = downloaded;
+
+ if (total_size > 0) {
+ double current_total = finished_size;
+ for (auto const& [id, prog] : item_progress) {
+ current_total += prog;
+ }
+ pk_backend_job_set_percentage(job, (uint)(current_total * 100 / total_size));
+ }
+ return 0;
+}
+
+int
+Dnf5DownloadCallbacks::end(void *user_cb_data, TransferStatus status, const char *msg)
+{
+ std::lock_guard<std::mutex> lock(mutex);
+ finished_size += item_progress[user_cb_data];
+ item_progress.erase(user_cb_data);
+ return 0;
+}
+
+Dnf5TransactionCallbacks::Dnf5TransactionCallbacks(PkBackendJob *job)
+ : job(job), total_items(0), current_item_index(0) {}
+
+void
+Dnf5TransactionCallbacks::before_begin(uint64_t total)
+{
+ total_items = total;
+}
+
+void
+Dnf5TransactionCallbacks::elem_progress(const libdnf5::base::TransactionPackage &item, uint64_t amount, uint64_t total)
+{
+ current_item_index = amount;
+}
+
+void
+Dnf5TransactionCallbacks::install_progress(const libdnf5::base::TransactionPackage &item, uint64_t amount, uint64_t total)
+{
+ if (total_items > 0 && total > 0) {
+ double item_frac = (double)amount / total;
+ pk_backend_job_set_percentage(job, (uint)((current_item_index + item_frac) * 100 / total_items));
+ }
+}
+
+void
+Dnf5TransactionCallbacks::install_start(const libdnf5::base::TransactionPackage &item, uint64_t total)
+{
+ auto action = item.get_action();
+ PkInfoEnum info = PK_INFO_ENUM_INSTALLING;
+ if (action == libdnf5::transaction::TransactionItemAction::UPGRADE ||
+ action == libdnf5::transaction::TransactionItemAction::DOWNGRADE) {
+ info = PK_INFO_ENUM_UPDATING;
+ }
+ dnf5_emit_pkg(job, item.get_package(), info);
+}
+
+void
+Dnf5TransactionCallbacks::uninstall_progress(const libdnf5::base::TransactionPackage &item, uint64_t amount, uint64_t total)
+{
+ if (total_items > 0 && total > 0) {
+ double item_frac = (double)amount / total;
+ pk_backend_job_set_percentage(job, (uint)((current_item_index + item_frac) * 100 / total_items));
+ }
+}
+
+void
+Dnf5TransactionCallbacks::uninstall_start(const libdnf5::base::TransactionPackage &item, uint64_t total)
+{
+ auto action = item.get_action();
+ PkInfoEnum info = PK_INFO_ENUM_REMOVING;
+ if (action == libdnf5::transaction::TransactionItemAction::REPLACED) {
+ info = PK_INFO_ENUM_CLEANUP;
+ }
+ dnf5_emit_pkg(job, item.get_package(), info);
+}
diff --git a/backends/dnf5/dnf5-backend-utils.hpp b/backends/dnf5/dnf5-backend-utils.hpp
new file mode 100644
index 000000000..4b4d0c23a
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-utils.hpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <pk-backend.h>
+#include <libdnf5/base/base.hpp>
+#include <libdnf5/rpm/package_query.hpp>
+#include <libdnf5/repo/repo_query.hpp>
+#include <libdnf5/repo/download_callbacks.hpp>
+#include <libdnf5/rpm/transaction_callbacks.hpp>
+#include <glib.h>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+// Private data structures
+typedef struct {
+ std::unique_ptr<libdnf5::Base> base;
+ GKeyFile *conf;
+ GMutex mutex;
+} PkBackendDnf5Private;
+
+void dnf5_setup_base(PkBackendDnf5Private *priv, gboolean refresh = FALSE, gboolean force = FALSE, const char *releasever = nullptr);
+void dnf5_refresh_cache(PkBackendDnf5Private *priv, gboolean force);
+PkInfoEnum dnf5_advisory_kind_to_info_enum(const std::string &type);
+PkInfoEnum dnf5_update_severity_to_enum(const std::string &severity);
+bool dnf5_force_distupgrade_on_upgrade(libdnf5::Base &base);
+bool dnf5_repo_is_devel(const libdnf5::repo::Repo &repo);
+bool dnf5_repo_is_source(const libdnf5::repo::Repo &repo);
+bool dnf5_repo_is_supported(const libdnf5::repo::Repo &repo);
+bool dnf5_backend_pk_repo_filter(const libdnf5::repo::Repo &repo, PkBitfield filters);
+bool dnf5_package_is_gui(const libdnf5::rpm::Package &pkg);
+bool dnf5_package_filter(const libdnf5::rpm::Package &pkg, PkBitfield filters);
+std::vector<libdnf5::rpm::Package> dnf5_process_dependency(libdnf5::Base &base, const libdnf5::rpm::Package &pkg, PkRoleEnum role, gboolean recursive);
+void dnf5_emit_pkg(PkBackendJob *job, const libdnf5::rpm::Package &pkg, PkInfoEnum info = PK_INFO_ENUM_UNKNOWN, PkInfoEnum severity = PK_INFO_ENUM_UNKNOWN);
+void dnf5_sort_and_emit(PkBackendJob *job, std::vector<libdnf5::rpm::Package> &pkgs);
+void dnf5_apply_filters(libdnf5::Base &base, libdnf5::rpm::PackageQuery &query, PkBitfield filters);
+std::vector<libdnf5::rpm::Package> dnf5_resolve_package_ids(libdnf5::Base &base, gchar **package_ids);
+
+
+
+void dnf5_remove_old_cache_directories(PkBackend *backend, const gchar *release_ver);
+
+class Dnf5DownloadCallbacks : public libdnf5::repo::DownloadCallbacks {
+public:
+ explicit Dnf5DownloadCallbacks(PkBackendJob *job, uint64_t total_size = 0);
+ void * add_new_download(void *user_data, const char *description, double total_to_download) override;
+ int progress(void *user_cb_data, double total_to_download, double downloaded) override;
+ int end(void *user_cb_data, TransferStatus status, const char *msg) override;
+private:
+ PkBackendJob *job;
+ uint64_t total_size;
+ double finished_size;
+ std::map<void*, double> item_progress;
+ std::mutex mutex;
+ uint64_t next_id;
+};
+
+class Dnf5TransactionCallbacks : public libdnf5::rpm::TransactionCallbacks {
+public:
+ explicit Dnf5TransactionCallbacks(PkBackendJob *job);
+ void before_begin(uint64_t total) override;
+ void elem_progress(const libdnf5::base::TransactionPackage &item, uint64_t amount, uint64_t total) override;
+ void install_progress(const libdnf5::base::TransactionPackage &item, uint64_t amount, uint64_t total) override;
+ void install_start(const libdnf5::base::TransactionPackage &item, uint64_t total) override;
+ void uninstall_progress(const libdnf5::base::TransactionPackage &item, uint64_t amount, uint64_t total) override;
+ void uninstall_start(const libdnf5::base::TransactionPackage &item, uint64_t total) override;
+private:
+ PkBackendJob *job;
+ uint64_t total_items;
+ uint64_t current_item_index;
+};
diff --git a/backends/dnf5/dnf5-backend-vendor-fedora.cpp b/backends/dnf5/dnf5-backend-vendor-fedora.cpp
new file mode 100644
index 000000000..5b1ac9542
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-vendor-fedora.cpp
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-vendor.hpp"
+#include <vector>
+#include <string>
+#include <algorithm>
+
+bool dnf5_validate_supported_repo(const std::string &id)
+{
+ const std::vector<std::string> default_repos = {
+ "fedora",
+ "rawhide",
+ "updates"
+ };
+
+ return std::find(default_repos.begin(), default_repos.end(), id) != default_repos.end();
+}
diff --git a/backends/dnf5/dnf5-backend-vendor-mageia.cpp b/backends/dnf5/dnf5-backend-vendor-mageia.cpp
new file mode 100644
index 000000000..e4c89659f
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-vendor-mageia.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-vendor.hpp"
+#include <vector>
+#include <string>
+
+bool dnf5_validate_supported_repo(const std::string &id)
+{
+ const std::vector<std::string> valid_sourcesect = { "", "-core", "-nonfree", "-tainted" };
+ const std::vector<std::string> valid_sourcetype = { "", "-debuginfo", "-source" };
+ const std::vector<std::string> valid_arch = { "x86_64", "i586", "armv7hl", "aarch64" };
+ const std::vector<std::string> valid_stage = { "", "-updates", "-testing" };
+ const std::vector<std::string> valid = { "mageia", "updates", "testing", "cauldron" };
+
+ for (const auto &v : valid) {
+ for (const auto &s : valid_stage) {
+ for (const auto &a : valid_arch) {
+ for (const auto &sec : valid_sourcesect) {
+ for (const auto &t : valid_sourcetype) {
+ if (id == v + s + "-" + a + sec + t) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
diff --git a/backends/dnf5/dnf5-backend-vendor-openmandriva.cpp b/backends/dnf5/dnf5-backend-vendor-openmandriva.cpp
new file mode 100644
index 000000000..6a3665aa9
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-vendor-openmandriva.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-vendor.hpp"
+#include <vector>
+#include <string>
+
+bool dnf5_validate_supported_repo(const std::string &id)
+{
+ const std::vector<std::string> valid_sourcesect = { "", "-extra", "-restricted", "-non-free" };
+ const std::vector<std::string> valid_sourcetype = { "", "-debuginfo", "-source" };
+ const std::vector<std::string> valid_arch = { "znver1", "x86_64", "i686", "aarch64", "armv7hnl", "riscv64" };
+ const std::vector<std::string> valid_stage = { "", "-updates", "-testing" };
+ const std::vector<std::string> valid = { "openmandriva", "updates", "testing", "cooker", "rolling", "rock", "release" };
+
+ for (const auto &v : valid) {
+ for (const auto &s : valid_stage) {
+ for (const auto &a : valid_arch) {
+ for (const auto &sec : valid_sourcesect) {
+ for (const auto &t : valid_sourcetype) {
+ if (id == v + s + "-" + a + sec + t) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
diff --git a/backends/dnf5/dnf5-backend-vendor-opensuse.cpp b/backends/dnf5/dnf5-backend-vendor-opensuse.cpp
new file mode 100644
index 000000000..e50cfcd40
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-vendor-opensuse.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-vendor.hpp"
+#include <vector>
+#include <string>
+
+bool dnf5_validate_supported_repo(const std::string &id)
+{
+ const std::vector<std::string> valid_sourcesect = { "-oss", "-non-oss" };
+ const std::vector<std::string> valid_sourcetype = { "", "-debuginfo", "-source" };
+ const std::vector<std::string> valid_sourcechan = { "", "-update" };
+ const std::vector<std::string> valid = { "opensuse-tumbleweed", "opensuse-leap" };
+
+ for (const auto &v : valid) {
+ for (const auto &sec : valid_sourcesect) {
+ for (const auto &c : valid_sourcechan) {
+ for (const auto &t : valid_sourcetype) {
+ if (id == v + sec + c + t) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
diff --git a/backends/dnf5/dnf5-backend-vendor-rosa.cpp b/backends/dnf5/dnf5-backend-vendor-rosa.cpp
new file mode 100644
index 000000000..98afe3f86
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-vendor-rosa.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-vendor.hpp"
+#include <vector>
+#include <string>
+
+bool dnf5_validate_supported_repo(const std::string &id)
+{
+ const std::vector<std::string> valid_sourcesect = { "", "-main", "-contrib", "-non-free" };
+ const std::vector<std::string> valid_sourcetype = { "", "-debuginfo", "-source" };
+ const std::vector<std::string> valid_arch = { "x86_64", "i686", "aarch64", "loongarch64", "riscv64", "e2kv4", "e2kv5", "e2kv6" };
+ const std::vector<std::string> valid = { "rosa", "updates", "testing" };
+
+ for (const auto &v : valid) {
+ for (const auto &a : valid_arch) {
+ for (const auto &sec : valid_sourcesect) {
+ for (const auto &t : valid_sourcetype) {
+ if (id == v + "-" + a + sec + t) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
diff --git a/backends/dnf5/dnf5-backend-vendor.hpp b/backends/dnf5/dnf5-backend-vendor.hpp
new file mode 100644
index 000000000..3c9d254b4
--- /dev/null
+++ b/backends/dnf5/dnf5-backend-vendor.hpp
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+bool dnf5_validate_supported_repo(const std::string &id);
diff --git a/backends/dnf5/meson.build b/backends/dnf5/meson.build
new file mode 100644
index 000000000..efb2b6be3
--- /dev/null
+++ b/backends/dnf5/meson.build
@@ -0,0 +1,27 @@
+
+add_languages('cpp', native: false)
+
+dnf5_dep = dependency('libdnf5', version: '>=5.2.17.0')
+libdnf5_version = dnf5_dep.version().split('.')
+
+shared_module(
+ 'pk_backend_dnf5',
+ 'pk-backend-dnf5.cpp',
+ 'dnf5-backend-utils.cpp',
+ 'dnf5-backend-thread.cpp',
+ 'dnf5-backend-vendor-@0@.cpp'.format(get_option('dnf_vendor')),
+ dependencies: [
+ dnf5_dep,
+ packagekit_glib2_dep,
+ ],
+ cpp_args: [
+ '-std=c++20',
+ '-DLIBDNF5_VERSION_MAJOR=' + libdnf5_version[0],
+ '-DLIBDNF5_VERSION_MINOR=' + libdnf5_version[1],
+ '-DLIBDNF5_VERSION_PATCH=' + libdnf5_version[2],
+ '-DG_LOG_DOMAIN="PackageKit-DNF5"',
+ ],
+ include_directories: packagekit_src_include,
+ install: true,
+ install_dir: pk_plugin_dir,
+)
diff --git a/backends/dnf5/pk-backend-dnf5.cpp b/backends/dnf5/pk-backend-dnf5.cpp
new file mode 100644
index 000000000..421ba61d1
--- /dev/null
+++ b/backends/dnf5/pk-backend-dnf5.cpp
@@ -0,0 +1,367 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2025 Neal Gompa <neal@gompa.dev>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "dnf5-backend-utils.hpp"
+#include "dnf5-backend-thread.hpp"
+#include <packagekit-glib2/pk-common-private.h>
+#include <packagekit-glib2/pk-debug.h>
+
+// Backend API Implementation
+
+extern "C" {
+
+const char *
+pk_backend_get_description (PkBackend *backend)
+{
+ return "DNF5 package manager backend";
+}
+
+const char *
+pk_backend_get_author (PkBackend *backend)
+{
+ return "Neal Gompa <neal@gompa.dev>";
+}
+
+gboolean
+pk_backend_supports_parallelization (PkBackend *backend)
+{
+ return TRUE;
+}
+
+gchar **
+pk_backend_get_mime_types (PkBackend *backend)
+{
+ const gchar *mime_types[] = { "application/x-rpm", NULL };
+ return g_strdupv ((gchar **) mime_types);
+}
+
+PkBitfield
+pk_backend_get_roles (PkBackend *backend)
+{
+ return pk_bitfield_from_enums (
+ PK_ROLE_ENUM_DEPENDS_ON,
+ PK_ROLE_ENUM_DOWNLOAD_PACKAGES,
+ PK_ROLE_ENUM_GET_DETAILS,
+ PK_ROLE_ENUM_GET_DETAILS_LOCAL,
+ PK_ROLE_ENUM_GET_FILES,
+ PK_ROLE_ENUM_GET_FILES_LOCAL,
+ PK_ROLE_ENUM_GET_PACKAGES,
+ PK_ROLE_ENUM_GET_REPO_LIST,
+ PK_ROLE_ENUM_INSTALL_FILES,
+ PK_ROLE_ENUM_INSTALL_PACKAGES,
+ PK_ROLE_ENUM_REMOVE_PACKAGES,
+ PK_ROLE_ENUM_UPDATE_PACKAGES,
+ PK_ROLE_ENUM_REPAIR_SYSTEM,
+ PK_ROLE_ENUM_UPGRADE_SYSTEM,
+ PK_ROLE_ENUM_REPO_ENABLE,
+ PK_ROLE_ENUM_REPO_REMOVE,
+ PK_ROLE_ENUM_REPO_SET_DATA,
+ PK_ROLE_ENUM_REQUIRED_BY,
+ PK_ROLE_ENUM_RESOLVE,
+ PK_ROLE_ENUM_REFRESH_CACHE,
+ PK_ROLE_ENUM_GET_UPDATES,
+ PK_ROLE_ENUM_GET_UPDATE_DETAIL,
+ PK_ROLE_ENUM_WHAT_PROVIDES,
+ PK_ROLE_ENUM_SEARCH_NAME,
+ PK_ROLE_ENUM_SEARCH_DETAILS,
+ PK_ROLE_ENUM_SEARCH_FILE,
+ PK_ROLE_ENUM_CANCEL,
+ -1);
+}
+
+void
+pk_backend_initialize (GKeyFile *conf, PkBackend *backend)
+{
+ g_autofree gchar *release_ver = NULL;
+ g_autoptr(GError) error = NULL;
+
+ // use logging
+ pk_debug_add_log_domain (G_LOG_DOMAIN);
+ pk_debug_add_log_domain ("DNF5");
+
+ PkBackendDnf5Private *priv = g_new0 (PkBackendDnf5Private, 1);
+
+ g_debug ("Using libdnf5 %i.%i.%i",
+ LIBDNF5_VERSION_MAJOR,
+ LIBDNF5_VERSION_MINOR,
+ LIBDNF5_VERSION_PATCH);
+
+ g_mutex_init (&priv->mutex);
+ priv->conf = g_key_file_ref (conf);
+
+ pk_backend_set_user_data (backend, priv);
+
+ release_ver = pk_get_distro_version_id (&error);
+ if (release_ver == NULL) {
+ g_warning ("Failed to parse os-release: %s", error->message);
+ } else {
+ /* clean up any cache directories left over from a distro upgrade */
+ dnf5_remove_old_cache_directories (backend, release_ver);
+ }
+
+ try {
+ dnf5_setup_base (priv);
+ } catch (const std::exception &e) {
+ g_warning ("Init failed: %s", e.what());
+ }
+}
+
+void
+pk_backend_destroy (PkBackend *backend)
+{
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ priv->base.reset();
+ if (priv->conf != NULL)
+ g_key_file_unref (priv->conf);
+ g_mutex_clear (&priv->mutex);
+ g_free (priv);
+}
+
+void
+pk_backend_start_job (PkBackend *backend, PkBackendJob *job)
+{
+}
+
+void
+pk_backend_stop_job (PkBackend *backend, PkBackendJob *job)
+{
+}
+
+void
+pk_backend_search_names (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", filters, values);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_search_details (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", filters, values);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_search_files (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **values)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", filters, values);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_packages (PkBackend *backend, PkBackendJob *job, PkBitfield filters)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t)", filters);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_resolve (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **package_ids)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", filters, package_ids);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_details (PkBackend *backend, PkBackendJob *job, gchar **package_ids)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(^as)", package_ids);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_files (PkBackend *backend, PkBackendJob *job, gchar **package_ids)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(^as)", package_ids);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_repo_list (PkBackend *backend, PkBackendJob *job, PkBitfield filters)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t)", filters);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_updates (PkBackend *backend, PkBackendJob *job, PkBitfield filters)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t)", filters);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_what_provides (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **search)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", filters, search);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_depends_on (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **package_ids, gboolean recursive)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^asb)", filters, package_ids, recursive);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_required_by (PkBackend *backend, PkBackendJob *job, PkBitfield filters, gchar **package_ids, gboolean recursive)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^asb)", filters, package_ids, recursive);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_update_detail (PkBackend *backend, PkBackendJob *job, gchar **package_ids)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(^as)", package_ids);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_download_packages (PkBackend *backend, PkBackendJob *job, gchar **package_ids, const gchar *directory)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(^as&s)", package_ids, directory);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_details_local (PkBackend *backend, PkBackendJob *job, gchar **files)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(^as)", files);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_get_files_local (PkBackend *backend, PkBackendJob *job, gchar **files)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(^as)", files);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_query_thread, NULL, NULL);
+}
+
+void
+pk_backend_install_packages (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags, gchar **package_ids)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", transaction_flags, package_ids);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_transaction_thread, NULL, NULL);
+}
+
+void
+pk_backend_remove_packages (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags, gchar **package_ids, gboolean allow_deps, gboolean autoremove)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^asbb)", transaction_flags, package_ids, allow_deps, autoremove);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_transaction_thread, NULL, NULL);
+}
+
+void
+pk_backend_update_packages (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags, gchar **package_ids)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", transaction_flags, package_ids);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_transaction_thread, NULL, NULL);
+}
+
+void
+pk_backend_install_files (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags, gchar **full_paths)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t^as)", transaction_flags, full_paths);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_transaction_thread, NULL, NULL);
+}
+
+void
+pk_backend_upgrade_system (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags, const gchar *distro_id, PkUpgradeKindEnum upgrade_kind)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t&su)", transaction_flags, distro_id, upgrade_kind);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_transaction_thread, NULL, NULL);
+}
+
+void
+pk_backend_repair_system (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t)", transaction_flags);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_transaction_thread, NULL, NULL);
+}
+
+void
+pk_backend_repo_enable (PkBackend *backend, PkBackendJob *job, const gchar *repo_id, gboolean enabled)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(sb)", repo_id, enabled);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_repo_thread, NULL, NULL);
+}
+
+void
+pk_backend_repo_set_data (PkBackend *backend, PkBackendJob *job, const gchar *repo_id, const gchar *parameter, const gchar *value)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(sss)", repo_id, parameter, value);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_repo_thread, NULL, NULL);
+}
+
+void
+pk_backend_repo_remove (PkBackend *backend, PkBackendJob *job, PkBitfield transaction_flags, const gchar *repo_id, gboolean autoremove)
+{
+ g_autoptr(GVariant) params = g_variant_new ("(t&sb)", transaction_flags, repo_id, autoremove);
+ pk_backend_job_set_parameters (job, g_steal_pointer (¶ms));
+ pk_backend_job_thread_create (job, dnf5_repo_thread, NULL, NULL);
+}
+
+void
+pk_backend_refresh_cache (PkBackend *backend, PkBackendJob *job, gboolean force)
+{
+ pk_backend_job_set_status (job, PK_STATUS_ENUM_REFRESH_CACHE);
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+ try {
+ dnf5_refresh_cache (priv, force);
+ } catch (const std::exception &e) {
+ pk_backend_job_error_code (job, PK_ERROR_ENUM_INTERNAL_ERROR, "%s", e.what());
+ }
+ pk_backend_job_finished (job);
+}
+
+void
+pk_backend_cancel (PkBackend *backend, PkBackendJob *job)
+{
+}
+
+}
diff --git a/meson_options.txt b/meson_options.txt
index 7136d1b40..0dbfbd232 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -10,7 +10,20 @@ option('maintainer',
option('packaging_backend',
type : 'array',
- choices : ['alpm', 'apt', 'dnf', 'dummy', 'entropy', 'eopkg', 'pisi', 'poldek', 'portage', 'slack', 'zypp', 'nix', 'freebsd'],
+ choices : ['alpm',
+ 'apt',
+ 'dnf',
+ 'dnf5',
+ 'dummy',
+ 'entropy',
+ 'eopkg',
+ 'pisi',
+ 'poldek',
+ 'portage',
+ 'slack',
+ 'zypp',
+ 'nix',
+ 'freebsd'],
value : ['dummy'],
description : 'The name of the backend to use'
)
--
2.52.0
From 512df952c4c53a6671dc68ac1a0909ff1f7c4480 Mon Sep 17 00:00:00 2001
From: Gordon Messmer <gordon.messmer@gmail.com>
Date: Fri, 2 Jan 2026 21:50:18 -0800
Subject: [PATCH 2/4] dnf5: Invalidate context when receiving updates-changed
signal
This uses the PkBackend object's updates-changed signal to invalidate
the dnf5 base object and reload all data from the system.
---
backends/dnf5/pk-backend-dnf5.cpp | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/backends/dnf5/pk-backend-dnf5.cpp b/backends/dnf5/pk-backend-dnf5.cpp
index 421ba61d1..cc5c3f118 100644
--- a/backends/dnf5/pk-backend-dnf5.cpp
+++ b/backends/dnf5/pk-backend-dnf5.cpp
@@ -87,6 +87,19 @@ pk_backend_get_roles (PkBackend *backend)
-1);
}
+static void
+pk_backend_context_invalidate_cb (PkBackend *backend, PkBackend *backend_data)
+{
+ g_return_if_fail (PK_IS_BACKEND (backend));
+
+ g_debug ("invalidating dnf5 base");
+
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ dnf5_setup_base (priv);
+}
+
void
pk_backend_initialize (GKeyFile *conf, PkBackend *backend)
{
@@ -119,6 +132,8 @@ pk_backend_initialize (GKeyFile *conf, PkBackend *backend)
try {
dnf5_setup_base (priv);
+ g_signal_connect (backend, "updates-changed",
+ G_CALLBACK (pk_backend_context_invalidate_cb), backend);
} catch (const std::exception &e) {
g_warning ("Init failed: %s", e.what());
}
--
2.52.0
From abd71b176412682ff16ba0e5bce03faea5d4570b Mon Sep 17 00:00:00 2001
From: Gordon Messmer <gordon.messmer@gmail.com>
Date: Fri, 9 Jan 2026 12:57:53 -0800
Subject: [PATCH 3/4] dnf5: Inhibit handling duplicate/redundant change
notifications
This ensures we do not handle potentially duplicate change notifications from
multiple producers within the same transaction to avoid wasting cycles.
---
backends/dnf5/dnf5-backend-thread.cpp | 7 ++++++-
backends/dnf5/dnf5-backend-utils.hpp | 1 +
backends/dnf5/pk-backend-dnf5.cpp | 20 ++++++++++++++++++++
3 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/backends/dnf5/dnf5-backend-thread.cpp b/backends/dnf5/dnf5-backend-thread.cpp
index 30e71fe00..8ad82caa3 100644
--- a/backends/dnf5/dnf5-backend-thread.cpp
+++ b/backends/dnf5/dnf5-backend-thread.cpp
@@ -550,8 +550,11 @@ dnf5_transaction_thread (PkBackendJob *job, GVariant *params, gpointer user_data
std::string msg;
for (const auto &p : trans.get_transaction_problems()) msg += p + "; ";
pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "Transaction failed: %s", msg.c_str());
+ } else {
+ // Update timestamp to inhibit notifications from our own transaction
+ priv->last_notification_timestamp = g_get_monotonic_time ();
}
-
+
// Post-transaction base re-initialization to ensure state consistency
dnf5_setup_base (priv);
@@ -707,6 +710,8 @@ dnf5_repo_thread (PkBackendJob *job, GVariant *params, gpointer user_data)
pk_backend_job_error_code (job, PK_ERROR_ENUM_TRANSACTION_ERROR, "Transaction failed: %s", msg.c_str());
} else {
g_debug("Transaction completed successfully");
+ // Update timestamp to inhibit notifications from our own transaction
+ priv->last_notification_timestamp = g_get_monotonic_time ();
}
dnf5_setup_base (priv);
} else {
diff --git a/backends/dnf5/dnf5-backend-utils.hpp b/backends/dnf5/dnf5-backend-utils.hpp
index 4b4d0c23a..39ae9b30d 100644
--- a/backends/dnf5/dnf5-backend-utils.hpp
+++ b/backends/dnf5/dnf5-backend-utils.hpp
@@ -37,6 +37,7 @@ typedef struct {
std::unique_ptr<libdnf5::Base> base;
GKeyFile *conf;
GMutex mutex;
+ gint64 last_notification_timestamp;
} PkBackendDnf5Private;
void dnf5_setup_base(PkBackendDnf5Private *priv, gboolean refresh = FALSE, gboolean force = FALSE, const char *releasever = nullptr);
diff --git a/backends/dnf5/pk-backend-dnf5.cpp b/backends/dnf5/pk-backend-dnf5.cpp
index cc5c3f118..06dc0b772 100644
--- a/backends/dnf5/pk-backend-dnf5.cpp
+++ b/backends/dnf5/pk-backend-dnf5.cpp
@@ -87,6 +87,22 @@ pk_backend_get_roles (PkBackend *backend)
-1);
}
+static int
+pk_backend_dnf5_inhibit_notify (PkBackend *backend)
+{
+ PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
+ gint64 current_time = g_get_monotonic_time ();
+ gint64 time_since_last_notification = current_time - priv->last_notification_timestamp;
+
+ /* Inhibit notifications for 5 seconds to avoid processing our own RPM transactions */
+ if (time_since_last_notification < 5 * G_USEC_PER_SEC) {
+ g_debug ("Ignoring signal: too soon after last notification (%" G_GINT64_FORMAT " µs)",
+ time_since_last_notification);
+ return 1;
+ }
+ return 0;
+}
+
static void
pk_backend_context_invalidate_cb (PkBackend *backend, PkBackend *backend_data)
{
@@ -94,10 +110,13 @@ pk_backend_context_invalidate_cb (PkBackend *backend, PkBackend *backend_data)
g_debug ("invalidating dnf5 base");
+ if (pk_backend_dnf5_inhibit_notify (backend)) return;
+
PkBackendDnf5Private *priv = (PkBackendDnf5Private *) pk_backend_get_user_data (backend);
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
dnf5_setup_base (priv);
+ priv->last_notification_timestamp = g_get_monotonic_time ();
}
void
@@ -119,6 +138,7 @@ pk_backend_initialize (GKeyFile *conf, PkBackend *backend)
g_mutex_init (&priv->mutex);
priv->conf = g_key_file_ref (conf);
+ priv->last_notification_timestamp = 0;
pk_backend_set_user_data (backend, priv);
--
2.52.0
From 96da00e0a78886c3e779416cfc29fdcd71002d43 Mon Sep 17 00:00:00 2001
From: Gordon Messmer <gordon.messmer@gmail.com>
Date: Fri, 9 Jan 2026 12:48:30 -0800
Subject: [PATCH 4/4] dnf5: Add rpm plugin to notify PackageKit when
transactions complete
This specifically is used to ensure that even if PackageKit
is asleep, it will wake up via D-Bus to refresh its caches.
---
.../dnf5/macros.transaction_notify_packagekit | 1 +
backends/dnf5/meson.build | 29 ++++
backends/dnf5/notify_packagekit.cpp | 156 ++++++++++++++++++
3 files changed, 186 insertions(+)
create mode 100644 backends/dnf5/macros.transaction_notify_packagekit
create mode 100644 backends/dnf5/notify_packagekit.cpp
diff --git a/backends/dnf5/macros.transaction_notify_packagekit b/backends/dnf5/macros.transaction_notify_packagekit
new file mode 100644
index 000000000..3dedf51bf
--- /dev/null
+++ b/backends/dnf5/macros.transaction_notify_packagekit
@@ -0,0 +1 @@
+%__transaction_notify_packagekit %{__plugindir}/notify_packagekit.so
diff --git a/backends/dnf5/meson.build b/backends/dnf5/meson.build
index efb2b6be3..236220744 100644
--- a/backends/dnf5/meson.build
+++ b/backends/dnf5/meson.build
@@ -25,3 +25,32 @@ shared_module(
install: true,
install_dir: pk_plugin_dir,
)
+
+# Build rpm plugin for notifying PackageKit
+rpm_dep = dependency('rpm', version: '>=4.20')
+sdbus_cpp_dep = dependency('sdbus-c++')
+sdbus_cpp_version = sdbus_cpp_dep.version().split('.')
+
+rpm_plugindir = rpm_dep.get_variable(pkgconfig: 'rpmplugindir')
+rpm_macrosdir = rpm_dep.get_variable(pkgconfig: 'rpmhome') + '/macros.d'
+
+shared_module(
+ 'notify_packagekit',
+ 'notify_packagekit.cpp',
+ cpp_args: [
+ '-std=c++20',
+ '-DSDBUSCPP_VERSION_MAJOR=' + sdbus_cpp_version[0],
+ ],
+ dependencies: [
+ rpm_dep,
+ sdbus_cpp_dep,
+ ],
+ name_prefix: '',
+ install: true,
+ install_dir: rpm_plugindir,
+)
+
+install_data(
+ 'macros.transaction_notify_packagekit',
+ install_dir: rpm_macrosdir,
+)
diff --git a/backends/dnf5/notify_packagekit.cpp b/backends/dnf5/notify_packagekit.cpp
new file mode 100644
index 000000000..403e07298
--- /dev/null
+++ b/backends/dnf5/notify_packagekit.cpp
@@ -0,0 +1,156 @@
+// RPM plugin to notify PackageKit that the system changed
+// Copyright (C) 2026 Gordon Messmer <gordon.messmer@gmail.com>
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// Based on https://github.com/rpm-software-management/rpm/blob/master/plugins/dbus_announce.c
+// Copyright (C) 2021 by Red Hat, Inc.
+// SPDX-License-Identifier: GPL-2.0-or-later
+//
+// and backends/dnf/notify_packagekit.cpp
+// Copyright (C) 2024 Alessandro Astone <ales.astone@gmail.com>
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#include <rpm/rpmlog.h>
+#include <rpm/rpmstring.h>
+#include <rpm/rpmts.h>
+#include <rpm/rpmplugin.h>
+
+#include <sdbus-c++/sdbus-c++.h>
+
+#include <cstdlib>
+#include <memory>
+
+using namespace std::literals;
+
+namespace {
+
+constexpr const char * PLUGIN_NAME = "notify_packagekit";
+
+struct NotifyPackagekitData {
+ std::unique_ptr<sdbus::IConnection> connection{nullptr};
+ std::unique_ptr<sdbus::IProxy> proxy{nullptr};
+
+ void close_bus() noexcept {
+ proxy.reset();
+ connection.reset();
+ }
+
+ rpmRC open_dbus(rpmPlugin plugin, rpmts ts) noexcept {
+ // Already open
+ if (connection) {
+ return RPMRC_OK;
+ }
+
+ // ...don't notify on test transactions
+ if (rpmtsFlags(ts) & (RPMTRANS_FLAG_TEST | RPMTRANS_FLAG_BUILD_PROBS)) {
+ return RPMRC_OK;
+ }
+
+ // ...don't notify on chroot transactions
+ if (!rstreq(rpmtsRootDir(ts), "/")) {
+ return RPMRC_OK;
+ }
+
+ try {
+#if SDBUSCPP_VERSION_MAJOR >= 2
+ auto serviceName = sdbus::ServiceName{"org.freedesktop.PackageKit"};
+ auto objectPath = sdbus::ObjectPath{"/org/freedesktop/PackageKit"};
+#else
+ auto serviceName = "org.freedesktop.PackageKit"s;
+ auto objectPath = "/org/freedesktop/PackageKit"s;
+#endif
+
+ connection = sdbus::createSystemBusConnection();
+ proxy = sdbus::createProxy(
+ std::move(connection),
+ std::move(serviceName),
+ std::move(objectPath),
+ sdbus::dont_run_event_loop_thread);
+ } catch (const sdbus::Error & e) {
+ rpmlog(RPMLOG_DEBUG,
+ "%s plugin: Error connecting to dbus (%s)\n",
+ PLUGIN_NAME, e.what());
+ connection.reset();
+ proxy.reset();
+ }
+
+ return RPMRC_OK;
+ }
+
+ rpmRC send_state_changed() noexcept {
+ if (!proxy) {
+ return RPMRC_OK;
+ }
+
+ try {
+#if SDBUSCPP_VERSION_MAJOR >= 2
+ auto interfaceName = sdbus::InterfaceName{"org.freedesktop.PackageKit"};
+ auto methodName = sdbus::MethodName{"StateHasChanged"};
+#else
+ auto interfaceName = "org.freedesktop.PackageKit"s;
+ auto methodName = "StateHasChanged"s;
+#endif
+
+ auto method = proxy->createMethodCall(std::move(interfaceName), std::move(methodName));
+ method << "posttrans";
+
+#if SDBUSCPP_VERSION_MAJOR >= 2
+ proxy->callMethodAsync(method, sdbus::with_future);
+#else
+ proxy->callMethod(method, sdbus::with_future);
+#endif
+ } catch (const sdbus::Error & e) {
+ rpmlog(RPMLOG_WARNING,
+ "%s plugin: Error sending message (%s)\n",
+ PLUGIN_NAME, e.what());
+ }
+
+ return RPMRC_OK;
+ }
+};
+
+} // namespace
+
+// C linkage for RPM plugin interface
+extern "C" {
+
+static rpmRC notify_packagekit_init(rpmPlugin plugin, rpmts ts) {
+ auto * state = new (std::nothrow) NotifyPackagekitData();
+ if (state == nullptr) {
+ return RPMRC_FAIL;
+ }
+ rpmPluginSetData(plugin, state);
+ return RPMRC_OK;
+}
+
+static void notify_packagekit_cleanup(rpmPlugin plugin) {
+ auto * state = static_cast<NotifyPackagekitData *>(rpmPluginGetData(plugin));
+ delete state;
+}
+
+static rpmRC notify_packagekit_tsm_pre(rpmPlugin plugin, rpmts ts) {
+ auto * state = static_cast<NotifyPackagekitData *>(rpmPluginGetData(plugin));
+ return state->open_dbus(plugin, ts);
+}
+
+static rpmRC notify_packagekit_tsm_post(rpmPlugin plugin, rpmts ts, int res) {
+ auto * state = static_cast<NotifyPackagekitData *>(rpmPluginGetData(plugin));
+ return state->send_state_changed();
+}
+
+struct rpmPluginHooks_s notify_packagekit_hooks = {
+ .init = notify_packagekit_init,
+ .cleanup = notify_packagekit_cleanup,
+ .tsm_pre = notify_packagekit_tsm_pre,
+ .tsm_post = notify_packagekit_tsm_post,
+ .psm_pre = NULL,
+ .psm_post = NULL,
+ .scriptlet_pre = NULL,
+ .scriptlet_fork_post = NULL,
+ .scriptlet_post = NULL,
+ .fsm_file_pre = NULL,
+ .fsm_file_post = NULL,
+ .fsm_file_prepare = NULL,
+};
+
+} // extern "C"
--
2.52.0