From f70d482a9100db57e9988a203c16048df19081b5 Mon Sep 17 00:00:00 2001 From: Matt Bearup Date: Mon, 20 Jul 2026 09:11:46 -0700 Subject: [PATCH] feat(dnf5): Add a plugin to support virtual snapshots --- ...al-snapshots-using-package-file-time.patch | 783 ++++++++++++++++++ base/comps/dnf5/dnf5.comp.toml | 48 ++ locks/dnf5.lock | 2 +- ...al-snapshots-using-package-file-time.patch | 783 ++++++++++++++++++ specs/d/dnf5/dnf5.spec | 8 +- 5 files changed, 1622 insertions(+), 2 deletions(-) create mode 100644 base/comps/dnf5/0001-Add-a-plugin-to-support-virtual-snapshots-using-package-file-time.patch create mode 100644 specs/d/dnf5/0001-Add-a-plugin-to-support-virtual-snapshots-using-package-file-time.patch diff --git a/base/comps/dnf5/0001-Add-a-plugin-to-support-virtual-snapshots-using-package-file-time.patch b/base/comps/dnf5/0001-Add-a-plugin-to-support-virtual-snapshots-using-package-file-time.patch new file mode 100644 index 00000000000..511f9790818 --- /dev/null +++ b/base/comps/dnf5/0001-Add-a-plugin-to-support-virtual-snapshots-using-package-file-time.patch @@ -0,0 +1,783 @@ +From 8f4e665d68947daa514b9889efa25b7d6ffc82f6 Mon Sep 17 00:00:00 2001 +From: Matt Bearup +Date: Tue, 16 Jun 2026 09:29:45 -0700 +Subject: [PATCH] Add a plugin to support virtual snapshots using package file + time. + +--- + CMakeLists.txt | 1 + + dnf5-plugins/CMakeLists.txt | 4 + + .../repo_snapshot_plugin/CMakeLists.txt | 13 + + .../repo_snapshot_cmd_plugin.cpp | 142 +++++++++++++ + include/libdnf5/rpm/package.hpp | 6 + + libdnf5-plugins/CMakeLists.txt | 1 + + libdnf5-plugins/repo_snapshot/CMakeLists.txt | 13 ++ + .../repo_snapshot/repo_snapshot.conf | 2 + + .../repo_snapshot/repo_snapshot.cpp | 176 ++++++++++++++++++ + .../repo_snapshot/repo_snapshot_filter.hpp | 108 ++++++++++ + libdnf5/rpm/package.cpp | 4 + + .../test_repo_snapshot_filter.cpp | 109 ++++++++++ + .../test_repo_snapshot_filter.hpp | 32 ++++ + test/libdnf5/rpm/test_package.cpp | 9 + + test/libdnf5/rpm/test_package.hpp | 2 + + 15 files changed, 622 insertions(+) + create mode 100644 dnf5-plugins/repo_snapshot_plugin/CMakeLists.txt + create mode 100644 dnf5-plugins/repo_snapshot_plugin/repo_snapshot_cmd_plugin.cpp + create mode 100644 libdnf5-plugins/repo_snapshot/CMakeLists.txt + create mode 100644 libdnf5-plugins/repo_snapshot/repo_snapshot.conf + create mode 100644 libdnf5-plugins/repo_snapshot/repo_snapshot.cpp + create mode 100644 libdnf5-plugins/repo_snapshot/repo_snapshot_filter.hpp + create mode 100644 test/libdnf5/plugins/repo_snapshot/test_repo_snapshot_filter.cpp + create mode 100644 test/libdnf5/plugins/repo_snapshot/test_repo_snapshot_filter.hpp + +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 063dfff..daf22c5 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -32,6 +32,7 @@ option(WITH_PLUGIN_EXPIRED_PGP_KEYS "Build a libdnf5 expired pgp keys plugin" ON + option(WITH_PLUGIN_RHSM "Build a libdnf5 rhsm (Red Hat Subscription Manager) plugin" OFF) + option(WITH_PYTHON_PLUGINS_LOADER "Build a special dnf5 plugin that loads Python plugins. Requires WITH_PYTHON3=ON." ON) + option(WITH_PLUGIN_LOCAL "Build a libdnf5 local plugin" ON) ++option(WITH_PLUGIN_REPO_SNAPSHOT "Build a libdnf5 repo snapshot plugin" ON) + + # build options - features + option(WITH_COMPS "Build with comps groups and environments support" ON) +diff --git a/dnf5-plugins/CMakeLists.txt b/dnf5-plugins/CMakeLists.txt +index bca9866..5fd3a61 100644 +--- a/dnf5-plugins/CMakeLists.txt ++++ b/dnf5-plugins/CMakeLists.txt +@@ -19,3 +19,7 @@ add_subdirectory("needs_restarting_plugin") + add_subdirectory("repoclosure_plugin") + add_subdirectory("reposync_plugin") + add_subdirectory("repomanage_plugin") ++ ++if(WITH_PLUGIN_REPO_SNAPSHOT) ++add_subdirectory("repo_snapshot_plugin") ++endif() +diff --git a/dnf5-plugins/repo_snapshot_plugin/CMakeLists.txt b/dnf5-plugins/repo_snapshot_plugin/CMakeLists.txt +new file mode 100644 +index 0000000..50ee979 +--- /dev/null ++++ b/dnf5-plugins/repo_snapshot_plugin/CMakeLists.txt +@@ -0,0 +1,13 @@ ++# set gettext domain for translations (required by the M_() message macro) ++set(GETTEXT_DOMAIN dnf5-plugin-repo-snapshot) ++add_definitions(-DGETTEXT_DOMAIN=\"${GETTEXT_DOMAIN}\") ++ ++add_library(repo_snapshot_cmd_plugin MODULE repo_snapshot_cmd_plugin.cpp) ++ ++# disable the 'lib' prefix in order to create repo_snapshot_cmd_plugin.so ++set_target_properties(repo_snapshot_cmd_plugin PROPERTIES PREFIX "") ++ ++target_link_libraries(repo_snapshot_cmd_plugin PRIVATE libdnf5 libdnf5-cli) ++target_link_libraries(repo_snapshot_cmd_plugin PRIVATE dnf5) ++ ++install(TARGETS repo_snapshot_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/) +diff --git a/dnf5-plugins/repo_snapshot_plugin/repo_snapshot_cmd_plugin.cpp b/dnf5-plugins/repo_snapshot_plugin/repo_snapshot_cmd_plugin.cpp +new file mode 100644 +index 0000000..02be640 +--- /dev/null ++++ b/dnf5-plugins/repo_snapshot_plugin/repo_snapshot_cmd_plugin.cpp +@@ -0,0 +1,142 @@ ++// Copyright Contributors to the DNF5 project ++// SPDX-License-Identifier: LGPL-2.1-or-later ++ ++#include ++#include ++#include ++ ++#include "../../libdnf5-plugins/repo_snapshot/repo_snapshot_filter.hpp" ++ ++#include ++#include ++ ++using namespace dnf5; ++ ++namespace { ++ ++constexpr const char * PLUGIN_NAME{"repo_snapshot"}; ++constexpr PluginVersion PLUGIN_VERSION{.major = 1, .minor = 0, .micro = 0}; ++constexpr PluginAPIVersion REQUIRED_PLUGIN_API_VERSION{.major = 2, .minor = 0}; ++ ++constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; ++constexpr const char * attrs_value[]{ ++ "Microsoft", ++ "azlinux@microsoft.com", ++ "Registers --snapshot-time and --snapshot-exclude-repos global CLI options."}; ++ ++ ++class RepoSnapshotCmdPlugin : public IPlugin { ++public: ++ using IPlugin::IPlugin; ++ ++ PluginAPIVersion get_api_version() const noexcept override { return REQUIRED_PLUGIN_API_VERSION; } ++ const char * get_name() const noexcept override { return PLUGIN_NAME; } ++ PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; } ++ const char * const * get_attributes() const noexcept override { return attrs; } ++ ++ const char * get_attribute(const char * attribute) const noexcept override { ++ for (size_t i = 0; attrs[i]; ++i) { ++ if (std::strcmp(attribute, attrs[i]) == 0) { ++ return attrs_value[i]; ++ } ++ } ++ return nullptr; ++ } ++ ++ void init() override; ++ std::vector> create_commands() override; ++ void finish() noexcept override {} ++}; ++ ++ ++void RepoSnapshotCmdPlugin::init() { ++ auto & ctx = get_context(); ++ auto & parser = ctx.get_argument_parser(); ++ auto & root_cmd = *parser.get_root_command(); ++ ++ // Get the global options group (registered by main.cpp). ++ auto & global_options_group = root_cmd.get_group("global_options"); ++ ++ // Register --snapshot-time global option. ++ auto snapshot_time = parser.add_new_named_arg("snapshot-time"); ++ snapshot_time->set_long_name("snapshot-time"); ++ snapshot_time->set_has_value(true); ++ snapshot_time->set_arg_value_help("POSIX_TIME"); ++ snapshot_time->set_description( ++ "POSIX time to be used for client-side repository snapshot"); ++ snapshot_time->set_parse_hook_func( ++ [](libdnf5::cli::ArgumentParser::NamedArg *, ++ [[maybe_unused]] const char * option, ++ const char * value) { ++ // Reject an invalid timestamp during CLI parsing so an install/upgrade ++ // does not silently run against the current (unfiltered) repository. ++ time_t parsed{0}; ++ if (!repo_snapshot::parse_snapshot_time(value, parsed)) { ++ throw libdnf5::cli::ArgumentParserError( ++ M_("--snapshot-time: invalid POSIX timestamp \"{}\""), std::string(value)); ++ } ++ // Bridge the validated value to an environment variable for the libdnf5 plugin. ++ setenv("DNF5_SNAPSHOT_TIME", value, 1); ++ return true; ++ }); ++ root_cmd.register_named_arg(snapshot_time); ++ global_options_group.register_argument(snapshot_time); ++ ++ // Register --snapshot-exclude-repos global option. ++ auto snapshot_exclude = parser.add_new_named_arg("snapshot-exclude-repos"); ++ snapshot_exclude->set_long_name("snapshot-exclude-repos"); ++ snapshot_exclude->set_has_value(true); ++ snapshot_exclude->set_arg_value_help("REPO_ID,..."); ++ snapshot_exclude->set_description( ++ "Repos to exclude from client-side repository snapshot"); ++ snapshot_exclude->set_parse_hook_func( ++ [](libdnf5::cli::ArgumentParser::NamedArg *, ++ [[maybe_unused]] const char * option, ++ const char * value) { ++ // Bridge the CLI value to an environment variable for the libdnf5 plugin. ++ setenv("DNF5_SNAPSHOT_EXCLUDE_REPOS", value, 1); ++ return true; ++ }); ++ root_cmd.register_named_arg(snapshot_exclude); ++ global_options_group.register_argument(snapshot_exclude); ++} ++ ++ ++std::vector> RepoSnapshotCmdPlugin::create_commands() { ++ // No new commands — this plugin only adds global options. ++ return {}; ++} ++ ++ ++std::exception_ptr last_exception; ++ ++} // namespace ++ ++ ++PluginAPIVersion dnf5_plugin_get_api_version(void) { ++ return REQUIRED_PLUGIN_API_VERSION; ++} ++ ++const char * dnf5_plugin_get_name(void) { ++ return PLUGIN_NAME; ++} ++ ++PluginVersion dnf5_plugin_get_version(void) { ++ return PLUGIN_VERSION; ++} ++ ++IPlugin * dnf5_plugin_new_instance( ++ [[maybe_unused]] ApplicationVersion application_version, Context & context) try { ++ return new RepoSnapshotCmdPlugin(context); ++} catch (...) { ++ last_exception = std::current_exception(); ++ return nullptr; ++} ++ ++void dnf5_plugin_delete_instance(IPlugin * plugin_object) { ++ delete plugin_object; ++} ++ ++std::exception_ptr * dnf5_plugin_get_last_exception(void) { ++ return &last_exception; ++} +diff --git a/include/libdnf5/rpm/package.hpp b/include/libdnf5/rpm/package.hpp +index c240a3d..4e38018 100644 +--- a/include/libdnf5/rpm/package.hpp ++++ b/include/libdnf5/rpm/package.hpp +@@ -242,6 +242,12 @@ public: + // @replaces libdnf:libdnf/hy-package.h:function:dnf_package_get_buildtime(DnfPackage * pkg) + unsigned long long get_build_time() const; + ++ /// @return File publish time from repo metadata (`