LIBDNF5 Plugins

These plugins enable changes to the existing DNF5 workflow. Possible uses of DNF5 passive plugins are modifying the LIBDNF5’s behavior at specific breakpoints, changing or implementing the logic, or triggering the loading of additional plugins via prepared callbacks.

Writing a Passive Plugin

Similarly to the DNF5 Plugins, we have to implement the libdnf5::plugin::IPlugin interface and override the hooks to alter DNF5’s logic.

In the following code block, a simple example plugin introduces logic in two different steps. The first is after preparing the libdnf5::Base object. The second is before the start of the transaction.

  1#include <libdnf5/base/base.hpp>
  2#include <libdnf5/common/exception.hpp>
  3#include <libdnf5/plugin/iplugin.hpp>
  4
  5#include <algorithm>
  6
  7using namespace libdnf5;
  8
  9namespace {
 10
 11constexpr const char * PLUGIN_NAME{"libdnf5_template_plugin"};
 12constexpr plugin::Version PLUGIN_VERSION{.major = 1, .minor = 1, .micro = 0};
 13constexpr PluginAPIVersion REQUIRED_PLUGIN_API_VERSION{.major = 2, .minor = 0};
 14
 15constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr};
 16constexpr const char * attrs_value[]{"Fatima Freedom", "dummy@email.com", "Plugin description."};
 17
 18class TemplatePlugin final : public plugin::IPlugin2_1 {
 19public:
 20    /// Implement custom constructor for the new plugin.
 21    /// This is not necessary when you only need Base object for your implementation.
 22    /// Optional to override.
 23    TemplatePlugin(libdnf5::plugin::IPluginData & data, libdnf5::ConfigParser &) : IPlugin2_1(data) {}
 24
 25    /// Fill in the API version of your plugin.
 26    /// This is used to check if the provided plugin API version is compatible with the library's plugin API version.
 27    /// MANDATORY to override.
 28    PluginAPIVersion get_api_version() const noexcept override { return REQUIRED_PLUGIN_API_VERSION; }
 29
 30    /// Enter the name of your new plugin.
 31    /// This is used in log messages when an action or error related to the plugin occurs.
 32    /// MANDATORY to override.
 33    const char * get_name() const noexcept override { return PLUGIN_NAME; }
 34
 35    /// Fill in the version of your plugin.
 36    /// This is utilized in informative and debugging log messages.
 37    /// MANDATORY to override.
 38    plugin::Version get_version() const noexcept override { return PLUGIN_VERSION; }
 39
 40    /// Add custom attributes, such as information about yourself and a description of the plugin.
 41    /// These can be used to query plugin-specific data through the API.
 42    /// Optional to override.
 43    const char * const * get_attributes() const noexcept override { return attrs; }
 44    const char * get_attribute(const char * attribute) const noexcept override {
 45        for (size_t i = 0; attrs[i]; ++i) {
 46            if (std::strcmp(attribute, attrs[i]) == 0) {
 47                return attrs_value[i];
 48            }
 49        }
 50        return nullptr;
 51    }
 52
 53    /// Initialization method called after the Base object is created and before command-line arguments are parsed.
 54    /// Optional to override.
 55    void init() override {}
 56
 57    /// Cleanup method called when plugin objects are garbage collected.
 58    /// Optional to override.
 59    void finish() noexcept override {}
 60
 61    /// Override the hooks you want to implement.
 62    void post_base_setup() override { post_base_magic(); }
 63    void pre_transaction(const libdnf5::base::Transaction & transaction) override {
 64        pre_transaction_magic(transaction);
 65    };
 66
 67private:
 68    void post_base_magic();
 69    void pre_transaction_magic(const libdnf5::base::Transaction &);
 70};
 71
 72/// Example how to implement additional logic after the Base is set up.
 73void TemplatePlugin::post_base_magic() {
 74    const auto & tsflags = get_base().get_config().get_tsflags_option().get_value();
 75    if (std::find(tsflags.begin(), tsflags.end(), "noscripts") != tsflags.end()) {
 76        libdnf_throw_assertion("Hey, we don't want a transaction without scriptlets!");
 77    }
 78}
 79
 80/// Example how to implement additional logic before starting the transaction.
 81void TemplatePlugin::pre_transaction_magic(const libdnf5::base::Transaction & transaction) {
 82    auto & base = get_base();
 83    auto & logger = *base.get_logger();
 84    logger.info("Libdnf5 template plugin: {} packages in transaction", transaction.get_transaction_packages_count());
 85}
 86
 87// Global variable to store the last exception from plugin initialization
 88// Allows capturing exceptions from C linkage functions that cannot propagate C++ exceptions
 89std::exception_ptr last_exception;
 90
 91}  // namespace
 92
 93/// Below is a block of functions with C linkage used for loading the plugin binaries from disk.
 94/// All of these are MANDATORY to implement.
 95
 96/// Return plugin's API version.
 97PluginAPIVersion libdnf_plugin_get_api_version(void) {
 98    return REQUIRED_PLUGIN_API_VERSION;
 99}
100
101/// Return plugin's name.
102const char * libdnf_plugin_get_name(void) {
103    return PLUGIN_NAME;
104}
105
106/// Return plugin's version.
107plugin::Version libdnf_plugin_get_version(void) {
108    return PLUGIN_VERSION;
109}
110
111/// Return the instance of the implemented plugin.
112plugin::IPlugin * libdnf_plugin_new_instance(
113    [[maybe_unused]] LibraryVersion library_version,
114    libdnf5::plugin::IPluginData & data,
115    libdnf5::ConfigParser & parser) try {
116    return new TemplatePlugin(data, parser);
117} catch (...) {
118    // Capture any exception that occurs during plugin instance creation
119    // std::current_exception() retrieves the current exception and stores it for later processing
120    last_exception = std::current_exception();
121
122    return nullptr;  // Return nullptr as failure indicator (C function cannot throw exceptions)
123}
124
125/// Delete the plugin instance.
126void libdnf_plugin_delete_instance(plugin::IPlugin * plugin_object) {
127    delete plugin_object;
128}
129
130// libdnf5 use this function to determine the actual failure cause
131// when libdnf_plugin_new_instance() returns nullptr
132std::exception_ptr * libdnf_plugin_get_last_exception(void) {
133    return &last_exception;
134}

Each plugin is structured in its own directory within the libdnf5-plugins folder. Review other plugins, such as actions, to understand the expected structure:

$ tree libdnf5-plugins/actions/
libdnf5-plugins/actions/
├── actions.conf
├── actions.cpp
└── CMakeLists.txt

Building the Binary

To create the plugin binary, include a CMake build script:

 1# set gettext domain for translations
 2set(GETTEXT_DOMAIN libdnf5-plugin-template)
 3add_definitions(-DGETTEXT_DOMAIN=\"${GETTEXT_DOMAIN}\")
 4
 5# add your source files
 6add_library(template_plugin MODULE template.cpp)
 7
 8# disable the 'lib' prefix in order to create template.so
 9set_target_properties(template_plugin PROPERTIES PREFIX "")
10
11# link the libdnf5 library
12target_link_libraries(template_plugin PRIVATE libdnf5)
13
14# install the plugin into the common libdnf5-plugins location
15#install(TARGETS template_plugin LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/libdnf5/plugins/")
16
17# install default plugin configuration file
18#install(FILES "template.conf" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}/dnf/libdnf5-plugins")

Unlike the DNF5 Plugins, plugins are part of the same domain as the core DNF5 functionality. Individual plugins are optionally included in the binary using created macro expressions in the spec file:

# ========== build options ==========
...
%bcond_without plugin_template
...
# ========== unpack, build, check & install ==========
...
%build
%cmake \
    ...
    -DWITH_PLUGIN_TEMPLATE=%{?with_plugin_template:ON}%{!?with_plugin_template:OFF} \
    ...

Define the connected CMake option in the dnf5/CMakeLists.txt file:

option(WITH_PLUGIN_TEMPLATE "Build a DNF5 template plugin" ON)

This sets up the default behavior to include the plugin in the build.

Include the newly created plugin in the CMakeLists.txt parent file inside libdnf5-plugins: add_subdirectory("template").

Delivering to the User

Each plugin requires a mandatory configuration file where we need to define the plugin’s name and specify when to enable it during runtime. Options include:

  • no: Plugin is disabled.

  • yes: Plugin is enabled.

  • host-only: Plugin is enabled only in configurations without installroot.

  • installroot-only: Plugin is enabled only in configurations with installroot.

Additional optional configuration options and sections can be defined and then accessed from the plugin implementation. Here’s an example configuration file:

1[main]
2name = template_plugin
3enabled = yes
4
5# We can provide optional custom keys and sections.
6custom_key = some_value
7
8[custom]
9website = https://my-hosting/template-plugin.org

In the LIBDNF5 Plugins, each plugin is delivered in a separate package. Include a new section in the spec file for this purpose:

# ========== libdnf5-plugin-template ==========

%if %{with plugin_template}
%package -n libdnf5-plugin-template
Summary:        Libdnf5 template plugin
License:        LGPL-2.1-or-later
Requires:       libdnf5%{?_isa} = %{version}-%{release}

%description -n libdnf5-plugin-template
Include a more descriptive message about your plugin here.

%files -n libdnf5-plugin-template
%{_libdir}/libdnf5/plugins/template.*
%config %{_sysconfdir}/dnf/libdnf5-plugins/template.conf
%endif

Consider including a documentation man page describing your plugin’s functionality. Ensure that you complete all the following steps:

  • doc/libdnf5_plugins/template.8.rst: Add a new man page for your plugin with the respective name.

  • doc/libdnf5_plugins/index.rst: Add a reference to the new plugin to the LIBDNF5 plugins page.

  • doc/CMakeLists.txt and doc/conf.py.in: Integrate with Sphinx documentation.

  • dnf5.spec: Include the new man page in your newly created package.