Installation records what a module reports about itself. dpm_install_module opens a module once, writes its version and description to a .meta file in /var/lib/dpm/metadata/, and records the alternate names it declared in modules.aliases beside it. dpm_uninstall_module removes both, leaving the module file in place. dpm_list_modules reads those records and opens no module. A module with no record lists with a version of <uninstalled> and still loads when a caller names it. An unreadable or absent metadata directory costs the listing its detail and costs nothing else. Aliases give a module alternate names, declared through the new dpm_module_aliases contract symbol or added with dpm_add_module_alias. A name is recorded once: one already serving as an alias, or belonging to an installed module, is refused rather than repointed. dpm_require matches a name against the installed modules, then the alias table, then the module path. The metadata directory is a fifth override field and the -M flag, and [modules] metadata in core.conf. The test suite is four binaries covering context, modules, records, and aliases, each a ctest case of its own, alongside the CLI cases.
158 lines
8.3 KiB
Markdown
158 lines
8.3 KiB
Markdown
# Developing DPM Modules {#modules}
|
|
|
|
A DPM module is one shared object in the module directory. libdpm-core.so loads it, validates it completely, and dispatches commands to it on behalf of whatever asked — the `dpm` binary, a build system, or another module. This document covers writing, building, testing, and installing a module.
|
|
|
|
## The Module Contract
|
|
|
|
A module includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following four symbols as extern "C". All returned strings must be non-NULL, static or module-owned, and valid for the lifetime of the loaded module; callers never free them.
|
|
|
|
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
|
|
The command entry point, and the only entry through which the module performs work. `ctx` is the host context that dispatched the call; the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_get_resolved_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name, equal to argv[0] when argc > 0. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. Where a nonzero return needs explaining, record the reason with `dpm_set_last_error` immediately before returning, and the caller reads it back with `dpm_get_last_error`. It must be callable immediately after load with no other setup.
|
|
|
|
`const char* dpm_module_version(void)`
|
|
The module's own version as an X.Y.Z string. libdpm-core.so reports this value to consumers, and each consumer decides for itself whether the version suits it.
|
|
|
|
`const char* dpm_module_description(void)`
|
|
A one-line human-readable description, shown in module listings.
|
|
|
|
`const char* dpm_module_aliases(void)`
|
|
A comma-separated list of alternate names your module answers to, or NULL for none:
|
|
|
|
```
|
|
extern "C" const char* dpm_module_aliases(void)
|
|
{
|
|
return "installer, files";
|
|
}
|
|
```
|
|
|
|
NULL is a complete answer. The symbol itself is required, and a module that does not export it is refused at load with the other contract failures.
|
|
|
|
The `dpm_ctx` type and the service declarations all come from the installed public header:
|
|
|
|
```
|
|
#include <dpm/core.h>
|
|
```
|
|
|
|
## You Determine Your Own Compatibility With the Library
|
|
|
|
Your module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where you need to act on what you are running under, `dpm_core_version()` reports the running version and you decide what to do:
|
|
|
|
```
|
|
const char* running = dpm_core_version();
|
|
```
|
|
|
|
Check it, proceed or fail on your own terms, and report through `dpm_log` and your return code.
|
|
|
|
## Your Interface Is Your Command Vocabulary
|
|
|
|
A module publishes no headers, no struct layouts, and no symbols to anything that calls it. Everything it offers is reached through `dpm_module_execute`, addressed by command string, with arguments passed as an argument vector and a status returned as an int.
|
|
|
|
That is what a caller compiles against: a module name and a command name, both strings. Document your commands, their arguments, and their return codes — that documentation is your interface, and it is the only thing a consumer can depend on.
|
|
|
|
**Symbol naming**: every functional export is prefixed with the module's name (mymodule_\*). The dpm_ prefix is reserved for the contract symbols and for libdpm-core.so.
|
|
|
|
## Calling Another Module
|
|
|
|
A module reaches a peer by performing the same two steps its own caller performed — ask libdpm-core.so for the module by name, then ask libdpm-core.so to invoke it:
|
|
|
|
```
|
|
int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)
|
|
{
|
|
dpm_module* peer = dpm_require(ctx, "othermodule");
|
|
if (!peer) {
|
|
dpm_log(ctx, DPM_LOG_ERROR, dpm_get_last_error(ctx));
|
|
return 1;
|
|
}
|
|
|
|
return dpm_execute(ctx, peer, "somecommand", argc, argv);
|
|
}
|
|
```
|
|
|
|
The `ctx` is the one handed to your entry point. Nothing else is needed to reach the library.
|
|
|
|
**Never link, include, or hardcode anything belonging to a peer.** No peer headers, no shared types, no peer symbols. Modules are loaded with `RTLD_LOCAL`, so a peer's symbols are not reachable from your module even if you tried — libdpm-core.so is the only path, and the only knowledge you hold about a peer is its name and the commands it documents.
|
|
|
|
A module that depends on a peer is the party that judges the peer's version. Require it, read its reported version with `dpm_get_module_info`, and decide whether it is suitable for the commands you intend to issue. libdpm-core.so reports; it does not rule.
|
|
|
|
## Validation at Load
|
|
|
|
libdpm-core.so is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, it verifies that every reserved contract symbol resolves and that the version and description probes return well-formed values. A module failing either step is refused with an itemized reason, visible in the load-failure output. A module that loads is fully valid — consumers never defend against partial states.
|
|
|
|
## Building
|
|
|
|
A module repository builds with CMake:
|
|
|
|
```
|
|
cmake_minimum_required(VERSION 3.22)
|
|
project(mymodule)
|
|
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
|
|
add_library(mymodule MODULE mymodule.cpp)
|
|
|
|
set_target_properties(mymodule PROPERTIES
|
|
PREFIX ""
|
|
SUFFIX ".so"
|
|
)
|
|
|
|
target_link_libraries(mymodule PRIVATE dpm-core)
|
|
|
|
install(TARGETS mymodule LIBRARY DESTINATION lib/dpm/modules)
|
|
```
|
|
|
|
```
|
|
cmake -B <build-dir>
|
|
cmake --build <build-dir>
|
|
```
|
|
|
|
libdpm-core.so is the only DPM link dependency a module ever has. Dependencies on other modules are runtime concerns, resolved by name through require and dispatch — a peer module is never linked, never included, and never needs to be present to build or to test.
|
|
|
|
## Running and Testing Locally
|
|
|
|
Load the freshly built module through a locally run `dpm` binary without installing anything:
|
|
|
|
```
|
|
dpm --module-path <build-dir> mymodule <command>
|
|
```
|
|
|
|
libdpm-core.so runs the full validation sequence on every load, so a contract mistake surfaces here, immediately and itemized, rather than after installation. The `--config-dir` flag points the module's configuration namespace at local files during development, and `--root` directs package operations at a scratch tree.
|
|
|
|
Where your module calls a peer, put a **stub module** in the fixture module path: a small .so exporting the three reserved symbols and answering the commands your module issues. libdpm-core.so validates and dispatches to it exactly as it would the real peer. Because a peer is addressed only by name and command string, the stub is a complete substitute — there is nothing else about the real peer your module could have depended on.
|
|
|
|
## Installing
|
|
|
|
Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install), and the package's install step then records the module:
|
|
|
|
```
|
|
dpm --install-module mymodule
|
|
```
|
|
|
|
That loads the module once, writes its version and description into `/var/lib/dpm/metadata/mymodule.meta`, and records each alias it declared whose name is free. From then on `dpm --list-modules` reports your module from that record and opens nothing, which is what keeps listing the modules on a system from executing them.
|
|
|
|
Removal is the counterpart:
|
|
|
|
```
|
|
dpm --uninstall-module mymodule
|
|
```
|
|
|
|
which deletes the record and every alias resolving to the module, leaving the `.so` where it is.
|
|
|
|
A module present in the module path with no record is reported in listings with a version of `<uninstalled>`. It still loads and runs when a caller requires it by name, with a warning, so a module put in place by hand works before anyone has installed it.
|
|
|
|
## Aliases
|
|
|
|
An alias is a second name a module answers to. Your declared aliases are recorded at installation; an operator adds more with:
|
|
|
|
```
|
|
dpm --add-alias mymodule mm
|
|
dpm --remove-alias mm
|
|
dpm --list-aliases [mymodule]
|
|
```
|
|
|
|
A name is recorded once. Adding a name that is already an alias, or that is an installed module's own name, is refused rather than repointed, so an existing route to a module is never taken over silently. Module names resolve before aliases, so a module's own name always reaches that module.
|
|
|
|
## A Working Example
|
|
|
|
The info module bundled with the `dpm` binary and libdpm-core.so, at `src/bundled-modules/info/`, tests and demonstrates full DPM system functionality, and in doing so shows the contract, command routing, and this build structure in working form.
|