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

@@ -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.