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

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# setup conditional build
 2if(NOT WITH_PLUGIN_TEMPLATE)
 3    return()
 4endif()
 5
 6# set gettext domain for translations
 7add_definitions(-DGETTEXT_DOMAIN=\"libdnf5\")
 8
 9# add your source files
10add_library(template MODULE template.cpp)
11
12# disable the 'lib' prefix in order to create template.so
13set_target_properties(template PROPERTIES PREFIX "")
14
15# link the libdnf5 library
16target_link_libraries(template PRIVATE libdnf5)
17
18# install the plugin into the common libdnf5-plugins location
19install(TARGETS template LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/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
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.