Module records and aliases

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.
This commit is contained in:
2026-08-24 03:51:39 -04:00
parent 71d409b5c7
commit 0291e61fd8
32 changed files with 2147 additions and 290 deletions

View File

@@ -43,6 +43,7 @@ It provides:
- **Validation**: the full load-time contract enforcement described below. libdpm-core.so is the sole authority on what a valid module is; the contract definition lives inside the library as data. There is no SDK package — the interface is specified by this document and enforced by the library's validator.
- **Routing**: dispatch a command string with arguments to a named module. This is the only path into module code, and it is the same path for the `dpm` binary, for a build system, and for a module calling a peer.
- **Version reporting**: require resolves a module by name, loads it, and returns a handle, or reports precisely why it can't. The loaded module's version is readable from the handle, for the caller to judge.
- **Records**: what an installed module reports about itself, written once at installation into `/var/lib/dpm/metadata/` and read by every listing, so describing the modules on a system executes none of them. The same directory holds the alias table, which gives a module alternate names.
- **Common services**: configuration access (per-module namespaces from `/etc/dpm/conf.d/`), logging, module-path queries.
## The C API
@@ -52,7 +53,7 @@ All functions are extern "C". All returned strings are owned by the library (or
### Context Lifecycle
`dpm_ctx* dpm_open(const dpm_open_overrides* overrides)`
Creates a context. Reads configuration from `/etc/dpm/conf.d/` (or the config directory named in overrides), resolves the module path (overrides take precedence over config, config over the built-in default), and initializes logging per configuration. Performs no module loading. Returns NULL only on allocation failure or an unreadable/invalid explicit override; a missing config directory is not an error — defaults apply. `overrides` may be NULL, and may specify: config directory, module path, target root (for chroot/image/sysroot operation), and log level. Multiple simultaneous contexts with different roots are legal.
Creates a context. Reads configuration from `/etc/dpm/conf.d/` (or the config directory named in overrides), resolves the module path (overrides take precedence over config, config over the built-in default), and initializes logging per configuration. Performs no module loading. Returns NULL only on allocation failure or an unreadable/invalid explicit override; a missing config directory is not an error — defaults apply. `overrides` may be NULL, and may specify: config directory, module path, target root (for chroot/image/sysroot operation), log level, and metadata directory. Multiple simultaneous contexts with different roots are legal.
`void dpm_close(dpm_ctx* ctx)`
Releases the context: unloads every module handle it issued, closes log targets, frees all memory owned by the context. All handles and strings obtained through the context are invalid after this call. NULL is a no-op.
@@ -70,10 +71,28 @@ Dispatch: invokes the module's `dpm_module_execute` with the context, `command`,
This is the entire path into module code. A caller addresses a module by name and a capability by command string, so it holds no compile-time knowledge of the module it is calling — no headers, no struct layouts, no symbols. That is what allows a module to be developed, built, and tested with no peer present.
### Installation
Installation is what lets libdpm-core.so describe a module without opening it. It loads the module once, writes down the version, description, and aliases the module reported, and from then on every listing reads that record. It is the only operation that opens a module for bookkeeping rather than to run it.
Uninstallation removes the record and the module's aliases, leaving the `.so` in place. A module that has never been installed, or whose record has been removed, is reported as uninstalled and still loads when a caller requires it by name. Bookkeeping describes the system; it never decides what may run.
Aliases give a module alternate names. A module declares its own, an operator adds more, and once recorded the two are the same thing. A name is recorded once: adding a name already serving as an alias, or already belonging to an installed module, is refused rather than repointed, so an existing route to a module is never taken over by a later one.
### Name Resolution
`dpm_require` resolves a name in three steps, taking the first that answers:
1. An installed module of that name, from the records
2. An alias of that name, from the alias table
3. `<module path>/<name>.so`
Reaching the third step means the module is not installed. That is logged and the load proceeds, so a module placed by hand runs without an installation step standing between an operator and the system.
### Enumeration
`dpm_cursor* dpm_list_modules(dpm_ctx* ctx)`
Scans the module path and returns a cursor over all *valid* modules (each candidate .so is validated on first scan; failures are logged and excluded). Returns NULL on an unreadable module path.
Returns a cursor over the modules in the module path, reported from their records. An installed module carries its recorded version and description; a module with no record carries a version of `<uninstalled>` and an empty description. No module is opened, so listing the modules on a system executes none of them. Returns NULL on an unreadable module path.
`int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out)`
Advances the cursor. Fills `out` with the next module's name, version, and description (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end.
@@ -103,7 +122,7 @@ Returns a human-readable description of the most recent failure recorded on this
## Module Contract
A module is one .so in the module directory. It includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following reserved symbols as extern "C". Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; the library and consumers never free them.
A module is one .so in the module directory. It includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following four reserved symbols as extern "C". Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; the library and consumers never free them.
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
The module's command entry point, and the only entry through which it 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); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
@@ -114,6 +133,9 @@ Returns the module's own version as an X.Y.Z string. Must be constant for the li
`const char* dpm_module_description(void)`
Returns a one-line human-readable description, used in module listings.
`const char* dpm_module_aliases(void)`
Returns a comma-separated list of alternate names the module answers to, or NULL to declare none. NULL is an answer; the symbol's absence is a contract violation and the module is refused. libdpm-core.so reads the list at load and records the names at installation.
**Version compatibility with the library is the module's own to determine.** A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where it needs to act on what it is running under, `dpm_core_version()` reports the running version and the module decides what to do with that.
**Symbol naming**: functional exports are prefixed with the module's name (raw_\*, pkg_\*); the dpm_ prefix is reserved for the contract and for libdpm-core.so.
@@ -124,8 +146,8 @@ Returns a one-line human-readable description, used in module listings.
libdpm-core.so is the sole authority on module validity; the contract above is enforced by its validator, not by any SDK. Validation is all-or-nothing; a module is registered only after passing every step:
1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`.
2. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse.
1. **Resolve all four reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`.
2. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse. `dpm_module_aliases`() is invoked and its answer recorded; NULL declares no alternate names and is accepted.
Failures happen at install/load time, loudly and itemized. Consumers never receive a partially valid module: if a handle was handed out, the contract already validated.
@@ -211,6 +233,8 @@ Terminology: **the `dpm` binary** names the command-line tool; **libdpm-core.so*
| `core.h` | `/usr/include/dpm/core.h` |
| `info.so` | `/usr/lib/dpm/modules/info.so` |
| modules (`raw.so`, `pkg.so`, `repo.so`, `source.so`, ...) | `/usr/lib/dpm/modules/<name>.so` |
| module records | `/var/lib/dpm/metadata/<name>.meta` |
| alias table | `/var/lib/dpm/metadata/modules.aliases` |
## Development and Testing