From 02c96b6d49025921d7b6757e11bc2102c92ff8f5 Mon Sep 17 00:00:00 2001 From: Neal Gompa 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 Neal Gompa +Backend: dnf5 + Neal Gompa + Backend: eopkg Joey Riches 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 + * + * 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 . + */ + +#include "dnf5-backend-thread.hpp" +#include "dnf5-backend-utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 results; + libdnf5::rpm::PackageQuery query(*priv->base); + + std::vector 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 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 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 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 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 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(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 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 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 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 files_c; + for (const auto &f : files_vec) files_c.push_back(const_cast(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 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 files_c; + for (const auto &f : files_vec) files_c.push_back(const_cast(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 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 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(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(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 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 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 + * + * 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 . + */ + +#pragma once + +#include +#include + +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 + * + * 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 . + */ + +#include "dnf5-backend-utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "dnf5-backend-vendor.hpp" + +void +dnf5_setup_base (PkBackendDnf5Private *priv, gboolean refresh, gboolean force, const char *releasever) +{ + priv->base = std::make_unique(); + + 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 distroverpkg_names = { "system-release", "distribution-release" }; + std::vector 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 +dnf5_process_dependency (libdnf5::Base &base, const libdnf5::rpm::Package &pkg, PkRoleEnum role, gboolean recursive) +{ + std::vector results; + std::set visited; + std::queue 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 &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 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 +dnf5_resolve_package_ids(libdnf5::Base &base, gchar **package_ids) +{ + std::vector 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 lock(mutex); + void *id = reinterpret_cast(next_id++); + item_progress[id] = 0; + return id; +} + +int +Dnf5DownloadCallbacks::progress(void *user_cb_data, double total_to_download, double downloaded) +{ + std::lock_guard 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 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 + * + * 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 . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Private data structures +typedef struct { + std::unique_ptr 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 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 &pkgs); +void dnf5_apply_filters(libdnf5::Base &base, libdnf5::rpm::PackageQuery &query, PkBitfield filters); +std::vector 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 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 + * + * 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 . + */ + +#include "dnf5-backend-vendor.hpp" +#include +#include +#include + +bool dnf5_validate_supported_repo(const std::string &id) +{ + const std::vector 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 + * + * 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 . + */ + +#include "dnf5-backend-vendor.hpp" +#include +#include + +bool dnf5_validate_supported_repo(const std::string &id) +{ + const std::vector valid_sourcesect = { "", "-core", "-nonfree", "-tainted" }; + const std::vector valid_sourcetype = { "", "-debuginfo", "-source" }; + const std::vector valid_arch = { "x86_64", "i586", "armv7hl", "aarch64" }; + const std::vector valid_stage = { "", "-updates", "-testing" }; + const std::vector 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 + * + * 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 . + */ + +#include "dnf5-backend-vendor.hpp" +#include +#include + +bool dnf5_validate_supported_repo(const std::string &id) +{ + const std::vector valid_sourcesect = { "", "-extra", "-restricted", "-non-free" }; + const std::vector valid_sourcetype = { "", "-debuginfo", "-source" }; + const std::vector valid_arch = { "znver1", "x86_64", "i686", "aarch64", "armv7hnl", "riscv64" }; + const std::vector valid_stage = { "", "-updates", "-testing" }; + const std::vector 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 + * + * 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 . + */ + +#include "dnf5-backend-vendor.hpp" +#include +#include + +bool dnf5_validate_supported_repo(const std::string &id) +{ + const std::vector valid_sourcesect = { "-oss", "-non-oss" }; + const std::vector valid_sourcetype = { "", "-debuginfo", "-source" }; + const std::vector valid_sourcechan = { "", "-update" }; + const std::vector 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 + * + * 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 . + */ + +#include "dnf5-backend-vendor.hpp" +#include +#include + +bool dnf5_validate_supported_repo(const std::string &id) +{ + const std::vector valid_sourcesect = { "", "-main", "-contrib", "-non-free" }; + const std::vector valid_sourcetype = { "", "-debuginfo", "-source" }; + const std::vector valid_arch = { "x86_64", "i686", "aarch64", "loongarch64", "riscv64", "e2kv4", "e2kv5", "e2kv6" }; + const std::vector 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 + * + * 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 . + */ + +#pragma once + +#include + +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 + * + * 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 . + */ + +#include "dnf5-backend-utils.hpp" +#include "dnf5-backend-thread.hpp" +#include +#include + +// 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 "; +} + +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 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 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 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 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 +// 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 +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include +#include +#include +#include + +#include + +#include +#include + +using namespace std::literals; + +namespace { + +constexpr const char * PLUGIN_NAME = "notify_packagekit"; + +struct NotifyPackagekitData { + std::unique_ptr connection{nullptr}; + std::unique_ptr 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(rpmPluginGetData(plugin)); + delete state; +} + +static rpmRC notify_packagekit_tsm_pre(rpmPlugin plugin, rpmts ts) { + auto * state = static_cast(rpmPluginGetData(plugin)); + return state->open_dbus(plugin, ts); +} + +static rpmRC notify_packagekit_tsm_post(rpmPlugin plugin, rpmts ts, int res) { + auto * state = static_cast(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