DNF5 Plugins

This is the way for adding a new command to DNF5, allowing users to simply type dnf5 new-command and access the implemented functionality.

Command vs Plugin

The reason why you can’t directly implement a new DNF5 command is the following.

The core commands in dnf5/commands implement essential package manager functionality, primarily focused on installing and managing packages. They are integral to the DNF5 package.

Plugin commands implement additional features that extend the capabilities of DNF5. They are packaged separately for optional installation by users who need these additional features.

Thus, core commands and plugins differ by logic and implementation.

Writing an Active Plugin

A DNF5 plugin comprises one or more commands. Visit the DNF5 Command Template to see how to implement a command.

Note that for plugin purposes, we don’t want to register new commands in the dnf5/main.cpp file. Instead, we will implement the dnf5::IPlugin interface by reusing the existing boilerplate code, as shown below (refer to the comments in the provided code for the expected input locations):

 1#include "template.hpp"
 2
 3#include <dnf5/iplugin.hpp>
 4
 5using namespace dnf5;
 6
 7namespace {
 8
 9constexpr const char * PLUGIN_NAME{"template"};
10
11constexpr PluginVersion PLUGIN_VERSION{.major = 1, .minor = 0, .micro = 0};
12
13constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr};
14constexpr const char * attrs_value[]{"Fred Fedora", "dummy@email.com", "Plugin description."};
15
16class TemplateCmdPlugin : public IPlugin {
17public:
18    using IPlugin::IPlugin;
19
20    /// Fill in the API version of your plugin.
21    /// This is used to check if the provided plugin API version is compatible with the application's plugin API version.
22    /// MANDATORY to override.
23    PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; }
24
25    /// Enter the name of your new plugin.
26    /// This is used in log messages when an action or error related to the plugin occurs.
27    /// MANDATORY to override.
28    const char * get_name() const noexcept override { return PLUGIN_NAME; }
29
30    /// Fill in the version of your plugin.
31    /// This is utilized in informative and debugging log messages.
32    /// MANDATORY to override.
33    PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; }
34
35    /// Add custom attributes, such as information about yourself and a description of the plugin.
36    /// These can be used to query plugin-specific data through the API.
37    /// Optional to override.
38    const char * const * get_attributes() const noexcept override { return attrs; }
39    const char * get_attribute(const char * attribute) const noexcept override {
40        for (size_t i = 0; attrs[i]; ++i) {
41            if (std::strcmp(attribute, attrs[i]) == 0) {
42                return attrs_value[i];
43            }
44        }
45        return nullptr;
46    }
47
48    /// Export all the commands that plugin is implementing.
49    /// MANDATORY to override.
50    std::vector<std::unique_ptr<Command>> create_commands() override;
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
61std::vector<std::unique_ptr<Command>> TemplateCmdPlugin::create_commands() {
62    std::vector<std::unique_ptr<Command>> commands;
63    commands.push_back(std::make_unique<TemplateCommand>(get_context()));
64    return commands;
65}
66
67
68}  // namespace
69
70/// Below is a block of functions with C linkage used for loading the plugin binaries from disk.
71/// All of these are MANDATORY to implement.
72
73/// Return plugin's API version.
74PluginAPIVersion dnf5_plugin_get_api_version(void) {
75    return PLUGIN_API_VERSION;
76}
77
78/// Return plugin's name.
79const char * dnf5_plugin_get_name(void) {
80    return PLUGIN_NAME;
81}
82
83/// Return plugin's version.
84PluginVersion dnf5_plugin_get_version(void) {
85    return PLUGIN_VERSION;
86}
87
88/// Return the instance of the implemented plugin.
89IPlugin * dnf5_plugin_new_instance([[maybe_unused]] ApplicationVersion application_version, Context & context) try {
90    return new TemplateCmdPlugin(context);
91} catch (...) {
92    return nullptr;
93}
94
95/// Delete the plugin instance.
96void dnf5_plugin_delete_instance(IPlugin * plugin_object) {
97    delete plugin_object;
98}

Note

During the startup of the DNF5 application, the plugin library scans the files in the configured plugins directory to check for the presence of any plugins implementing the common dnf5::IPlugin interface. If such plugins are found, they are loaded into memory along with all the commands they implement.

Each plugin source is structured in its own directory within the dnf5-plugins folder. See other plugins, such as builddep_plugin:

$ tree dnf5-plugins/builddep_plugin/
dnf5-plugins/builddep_plugin/
├── builddep_cmd_plugin.cpp
├── builddep.cpp
├── builddep.hpp
└── CMakeLists.txt

Building the Binary

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

 1# set gettext domain for translations
 2add_definitions(-DGETTEXT_DOMAIN=\"dnf5_cmd_template\")
 3
 4# add your source files
 5add_library(template_cmd_plugin MODULE template.cpp template_cmd_plugin.cpp)
 6
 7# disable the 'lib' prefix in order to create template_cmd_plugin.so
 8set_target_properties(template_cmd_plugin PROPERTIES PREFIX "")
 9
10# optionally, add your dependencies and link them to the plugin
11# pkg_check_modules(RPM REQUIRED rpm)
12# target_link_libraries(template_cmd_plugin PRIVATE ${RPM_LIBRARIES})
13
14# link the default dnf libraries
15target_link_libraries(template_cmd_plugin PRIVATE dnf5 libdnf5 libdnf5-cli)
16
17# install the plugin into the common dnf5-plugins location
18install(TARGETS template_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/)

Include the newly created plugin in the CMakeLists.txt parent file inside dnf5-plugins like this: add_subdirectory("template_plugin").

Delivering to the User

We still need to set up the deployment process so that users can install the new plugin into DNF5 through the package manager.

Add a new provide in the dnf5-plugins section of the dnf5.spec file: Provides: dnf5-command(template).

You should also include a documentation man page describing the usage of your plugin in detail. There are several places which need to be interconnected, make sure to complete all the following steps:

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

  • doc/dnf5_plugins/index.rst: Reference the new plugin in the DNF5 plugins page.

  • doc/dnf5.8.rst: Reference the new plugin in the main DNF5 man page.

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

  • dnf5.spec: Include the new man page in the dnf5-plugins package.