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

@@ -37,12 +37,13 @@ dpm_open_overrides ov = {
"/path/to/conf.d",
"/path/to/modules",
"/path/to/root",
-1
-1,
"/path/to/metadata"
};
dpm_ctx* ctx = dpm_open(&ov);
```
Leaving `config_dir` NULL selects `/etc/dpm/conf.d/`; leaving `module_path` NULL selects the configured value and then the built-in default; leaving `root` NULL selects `/`; a `log_level` of -1 takes the configured value.
Leaving `config_dir` NULL selects `/etc/dpm/conf.d/`; leaving `module_path` NULL selects the configured value and then the built-in default; leaving `root` NULL selects `/`; a `log_level` of -1 takes the configured value; leaving `metadata_dir` NULL selects the configured value and then `/var/lib/dpm/metadata/`.
The root override is what makes chroot builds, image assembly, and sysroot management work: package operations act on the given tree instead of the running system. Multiple simultaneous contexts with different roots are legal.
@@ -56,6 +57,8 @@ dpm_module* mod = dpm_require(ctx, "mymodule");
libdpm-core.so validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent or invalid — `dpm_get_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle.
The name is matched against the installed modules first, then the alias table, then the module path itself, so a module answers to its own name and to any alias recorded for it. A name that reaches the module path belongs to a module nobody has installed; that is logged and the module loads anyway.
`dpm_get_module_info` reports what the library saw in the loaded module:
```
@@ -86,17 +89,23 @@ while (dpm_cursor_next(cur, &info) == 0) {
dpm_cursor_free(cur);
```
The cursor covers every valid module in the module path; invalid candidates are excluded and logged.
The cursor covers every module in the module path, reported from what installation recorded. An installed module carries its recorded version and description; one with no record carries a version of `<uninstalled>`. No module is opened, so enumerating executes nothing.
## Records and Aliases
`dpm_install_module` loads a module once, writes down what it reports, and records the alternate names it declared. `dpm_uninstall_module` removes both, leaving the module file in place. `dpm_add_module_alias`, `dpm_remove_module_alias`, and `dpm_list_module_aliases` manage alternate names directly; the alias cursor is advanced with `dpm_alias_cursor_next` and released with `dpm_alias_cursor_free`.
A name is recorded once. Adding one that is already an alias, or that belongs to an installed module, is refused rather than repointed.
## Services
- `dpm_core_version`()** — the library version; callable without a context.
- `dpm_get_module_info`(ctx, mod, out)** — the name, version, and description read from a loaded module.
- `dpm_config_get`(ctx, module, section, key)** — a value from a module's configuration namespace, or NULL if unset.
- `dpm_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
- `dpm_get_resolved_module_path`(ctx)** — the resolved module directory.
- `dpm_core_version`() — the library version; callable without a context.
- `dpm_get_module_info`(ctx, mod, out) — the name, version, and description read from a loaded module.
- `dpm_config_get`(ctx, module, section, key) — a value from a module's configuration namespace, or NULL if unset.
- `dpm_log`(ctx, level, message) — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
- `dpm_get_resolved_module_path`(ctx) — the resolved module directory.
- `dpm_set_last_error`(ctx, msg) — records a failure reason on the context; what a module calls to explain a nonzero return.
- `dpm_get_last_error`(ctx)** — a human-readable description of the most recent failure on the context, or NULL.
- `dpm_get_last_error`(ctx) — a human-readable description of the most recent failure on the context, or NULL.
## Ownership and Errors

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

View File

@@ -4,7 +4,7 @@ A DPM module is one shared object in the module directory. libdpm-core.so loads
## The Module Contract
A module includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following 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.
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.
@@ -15,6 +15,18 @@ The module's own version as an X.Y.Z string. libdpm-core.so reports this value t
`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:
```
@@ -110,7 +122,35 @@ Where your module calls a peer, put a **stub module** in the fixture module path
## Installing
Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install). libdpm-core.so discovers the module on its next scan; no registration step exists beyond the file being present and valid.
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

View File

@@ -30,16 +30,19 @@ Two properties make the routing rule real rather than a convention:
The `dpm` binary's subcommand surface is exactly the set of loadable modules. `dpm <module> <command> [args...]` loads that module and hands the command to it; `dpm <module> help` asks the module to describe itself. There is no fixed list of operations baked into the tool, because the tool contains no capability logic — it parses arguments and prints.
`dpm --list-modules` shows every module that passes validation, with its version and description. Candidates that fail are excluded from the listing and logged, so what is listed is what can actually be run.
`dpm --list-modules` shows every module in the module path with its version and description, read from what installation recorded. A module with no record is shown with a version of `<uninstalled>`. No module is opened to produce the listing, so describing the modules on a system executes none of them.
Four flags redirect the system defaults:
- `-c, --config-dir PATH` — where configuration is read from
- `-m, --module-path PATH` — which directory modules are loaded from
- `-M, --metadata-dir PATH` — where module records and the alias table live
- `-r, --root PATH` — the target root that package operations act on
- `-L, --log-level LEVEL` — FATAL, ERROR, WARN, INFO, or DEBUG
These four exist because of a rule the design imposes on itself: every field a linked program can override must also be a flag, so anything reachable from code is reachable from a shell. That rule holds for any override added in the future.
These five exist because of a rule the design imposes on itself: every field a linked program can override must also be a flag, so anything reachable from code is reachable from a shell. That rule holds for any override added in the future.
Five more operations manage what libdpm-core.so has recorded: `--install-module`, `--uninstall-module`, `--add-alias`, `--remove-alias`, and `--list-aliases`.
## Writing a Program Against the Library
@@ -51,7 +54,7 @@ The target root override is what makes chroot builds, image assembly, and sysroo
The context owns everything it hands out. Every string a caller receives stays valid until `dpm_close`, and callers never free anything.
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_get_last_error` carrying the precise reason. Modules load at most once per context, and repeated calls return the same handle.
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_get_last_error` carrying the precise reason. The name is matched against the installed modules first, then the alias table, then the module path itself; reaching the module path means the module is not installed, which is logged and does not stop the load. Modules load at most once per context, and repeated calls return the same handle.
**Reading what the library saw.** `dpm_get_module_info` fills in a module's name, version, and description. Those values are reported, and no conclusion is drawn from them.
@@ -85,7 +88,9 @@ The `ctx` a module needs is the one handed to it in its own entry point, so it r
**Configuration** lives in `/etc/dpm/conf.d/`. Each `.conf` file in that directory is one namespace named after the file: `core.conf` holds the library's own settings, and a module named `mymodule` reads `mymodule.conf`. Files are sectioned, and a value is addressed by namespace, section, and key. The library's own file carries the log level, whether to write a log file and where, and the default module directory.
**Modules** live in `/usr/lib/dpm/modules/`. A module's name is its filename without the `.so` extension, so `info.so` is the module named `info`. Discovery is the presence of a valid file in that directory — there is no registry, and no registration step.
**Modules** live in `/usr/lib/dpm/modules/`. A module's name is its filename without the `.so` extension, so `info.so` is the module named `info`.
**Records** live in `/var/lib/dpm/metadata/`. Installing a module writes `<name>.meta` there, holding the version and description the module reported, and records the alternate names it declared in `modules.aliases` beside it. Listings read those files, so describing the modules on a system opens none of them. A module in the module path with no record is reported as uninstalled and still runs when a caller names it.
**Logging** goes to the console always, and to a log file when configuration enables one; the default path is `/var/log/dpm/dpm.log`.
@@ -99,12 +104,14 @@ The `ctx` a module needs is the one handed to it in its own entry point, so it r
| `libdpm-core.so` | `/usr/lib/libdpm-core.so` |
| public header | `/usr/include/dpm/core.h` |
| modules | `/usr/lib/dpm/modules/<name>.so` |
| module records | `/var/lib/dpm/metadata/<name>.meta` |
| alias table | `/var/lib/dpm/metadata/modules.aliases` |
## The Module Contract
*What a module author implements.*
A module is one `.so` in the module directory exporting three reserved symbols: a command entry point, its own version, and a one-line description. It includes `<dpm/core.h>` for those declarations and links `-ldpm-core`, and that is its entire build dependency.
A module is one `.so` in the module directory exporting four reserved symbols: a command entry point, its own version, a one-line description, and the alternate names it answers to. It includes `<dpm/core.h>` for those declarations and links `-ldpm-core`, and that is its entire build dependency.
The entry point receives the context that dispatched the call, the command name, and an argument vector, and returns an int. Everything a module offers the rest of the system is reachable through that one function, addressed by command name — which is what keeps a caller free of any compile-time knowledge of the module it is calling.
@@ -114,7 +121,7 @@ A module is built against the system-installed `libdpm-core.so` and is responsib
libdpm-core.so validates a module completely before offering it to anyone:
1. Every reserved contract symbol resolves.
1. Every one of the four reserved contract symbols resolves.
2. The version and description probes return well-formed values.
A module failing any step is refused with an itemized reason and its library handle closed. Validation is all-or-nothing: if a handle comes back, the contract already passed. Consumers never defend against partially valid modules, because they cannot receive one.