Route all module interaction through dispatch

A module is addressed by name and command string, and nothing else.
Typed API access handed a caller a pointer into the callee's function
table, which meant compiling against that module's struct layout — a
build-time dependency between modules that the design does not permit.
Removing it also removes the manifest, the table magic constant, and
the table size field, which existed only to describe and validate
those tables.

Load validation is now three steps: reserved contract symbols resolve,
the minimum-version handshake passes, and the version and description
probes return well-formed values. The contract is four reserved
symbols, and a module's interface is the command vocabulary it
documents.

Documentation is brought in line, and artifacts are named exactly:
libdpm-core.so for the library, <dpm/core.h> for the header, the dpm
binary for the command-line tool.
This commit is contained in:
2026-08-15 02:15:18 -04:00
parent 267529bee3
commit 97b39cac6c
17 changed files with 263 additions and 702 deletions

View File

@@ -82,7 +82,7 @@ set_target_properties(info PROPERTIES
add_dependencies(dpm info) add_dependencies(dpm info)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Test fixture modules: one known-good stub, five deliberately broken # Test fixture modules: one known-good stub, three deliberately broken
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
enable_testing() enable_testing()
@@ -95,7 +95,7 @@ file(WRITE ${CMAKE_BINARY_DIR}/DartConfiguration.tcl
set(FIXTURE_MODULE_DIR ${CMAKE_BINARY_DIR}/tests/fixtures) set(FIXTURE_MODULE_DIR ${CMAKE_BINARY_DIR}/tests/fixtures)
foreach(fixture good missing_symbols bad_magic lying_manifest core_too_new bad_version) foreach(fixture good missing_symbols core_too_new bad_version)
add_library(fixture_${fixture} MODULE tests/fixtures/src/${fixture}.cpp) add_library(fixture_${fixture} MODULE tests/fixtures/src/${fixture}.cpp)
set_target_properties(fixture_${fixture} PROPERTIES set_target_properties(fixture_${fixture} PROPERTIES
PREFIX "" PREFIX ""
@@ -123,7 +123,7 @@ set_target_properties(test_core PROPERTIES
BUILD_RPATH "$ORIGIN/../lib" BUILD_RPATH "$ORIGIN/../lib"
) )
foreach(fixture good missing_symbols bad_magic lying_manifest core_too_new bad_version) foreach(fixture good missing_symbols core_too_new bad_version)
add_dependencies(test_core fixture_${fixture}) add_dependencies(test_core fixture_${fixture})
endforeach() endforeach()

View File

@@ -1,4 +1,4 @@
# Building libdpm-core and the dpm binary # Building libdpm-core.so and the dpm binary
## Prerequisites ## Prerequisites
@@ -22,7 +22,7 @@ cmake --build <build-dir>
Artifacts land in: Artifacts land in:
``` ```
<build-dir>/bin/dpm the CLI <build-dir>/bin/dpm the dpm binary
<build-dir>/lib/libdpm-core.so the library <build-dir>/lib/libdpm-core.so the library
<build-dir>/modules/info.so the bundled info module <build-dir>/modules/info.so the bundled info module
``` ```
@@ -39,7 +39,7 @@ This runs the API test binary (which exercises the full load-time validation mat
### Manual Execution ### Manual Execution
The build tree plus the test fixtures form a complete self-contained environment; no installation is required. Point the CLI at local paths with its override flags: The build tree plus the test fixtures form a complete self-contained environment; no installation is required. Point the `dpm` binary at local paths with its override flags:
``` ```
<build-dir>/bin/dpm --config-dir ./tests/fixtures/conf --module-path <build-dir>/modules info version <build-dir>/bin/dpm --config-dir ./tests/fixtures/conf --module-path <build-dir>/modules info version
@@ -79,10 +79,10 @@ cmake --install <build-dir>
Omit `-DCMAKE_INSTALL_PREFIX=/usr` at configure time for a `/usr/local` install. Under the install prefix this installs: Omit `-DCMAKE_INSTALL_PREFIX=/usr` at configure time for a `/usr/local` install. Under the install prefix this installs:
``` ```
bin/dpm the CLI bin/dpm the dpm binary
lib/libdpm-core.so the library lib/libdpm-core.so the library
lib/dpm/modules/info.so the bundled info module lib/dpm/modules/info.so the bundled info module
include/dpm/ the public header include/dpm/ the public header
``` ```
The libdpm-core configuration installs to `/etc/dpm/conf.d/core.conf` regardless of prefix. The library's own configuration installs to `/etc/dpm/conf.d/core.conf` regardless of prefix.

View File

@@ -1,10 +1,10 @@
# Consuming libdpm-core # Consuming libdpm-core.so
Programs link libdpm-core to operate the package manager directly: build systems, installers, image builders, system tooling, and foreign-language bindings all use the same library the `dpm` CLI is built on. A program holding a default context is operating the installed package manager itself — system configuration, system module path, system tree, system locking — identically to invoking the installed `dpm` command. Programs link libdpm-core.so as an ordinary shared library dependency — the same way they link any other library — to reach the package manager in-process: build systems, installers, image builders, system tooling, and foreign-language bindings all use the library the `dpm` binary is built on. A program holding a default context is working against system configuration, the system module path, the system tree, and system locking, the same environment the installed `dpm` binary sees.
## Compiling and linking ## Compiling and linking
With libdpm-core installed, include the public header and link the library: With the library installed, include the public header and link it:
``` ```
#include <dpm/core.h> #include <dpm/core.h>
@@ -16,6 +16,8 @@ g++ myprog.cpp -ldpm-core
The header installs to the standard include path and the library to the standard lib path, so no additional flags are required. The interface is a C ABI: every function is extern "C", every type crossing the boundary is a C type, and state passes through opaque handles — callable from C, C++, or any language with C FFI. The header installs to the standard include path and the library to the standard lib path, so no additional flags are required. The interface is a C ABI: every function is extern "C", every type crossing the boundary is a C type, and state passes through opaque handles — callable from C, C++, or any language with C FFI.
`<dpm/core.h>` names no module and carries no module-specific type. It offers two things: discovery of modules, and interaction with them.
## The context ## The context
All work happens through a context handle: All work happens through a context handle:
@@ -50,30 +52,24 @@ The root override is what makes chroot builds, image assembly, and sysroot manag
dpm_module* mod = dpm_require(ctx, "mymodule"); dpm_module* mod = dpm_require(ctx, "mymodule");
``` ```
libdpm-core 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_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle. 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_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle.
**`dpm_module_info_of`** reports what libdpm-core saw in the loaded module: **`dpm_module_info_of`** reports what the library saw in the loaded module:
``` ```
dpm_module_info info; dpm_module_info info;
dpm_module_info_of(ctx, mod, &info); /* info.name, .version, .description, .core_min */ dpm_module_info_of(ctx, mod, &info); /* info.name, .version, .description, .core_min */
``` ```
Deciding whether that version is too new or too old is yours. libdpm-core applies no version criterion of its own — a handle means the module is valid, not that it suits you. Deciding whether that version is suitable is yours. libdpm-core.so applies no version criterion of its own — a handle means the module is valid, not that it suits you.
**`dpm_execute`** drives a module the way the CLI does — a command name and arguments: **`dpm_execute`** invokes the module — a command name and arguments:
``` ```
int rc = dpm_execute(ctx, mod, "command", argc, argv); int rc = dpm_execute(ctx, mod, "command", argc, argv);
``` ```
**`dpm_get_api`** returns a module's typed function table for direct calls: This is the only path into module code, and it is the same path the `dpm` binary uses and the same path a module uses to reach a peer. You address a module by name and a capability by command string, so your program compiles against no module header, no struct layout, and no module symbol. What a module accepts as commands and arguments, and what its return codes mean, is documented by that module.
```
const mymodule_api_v1_s* api = (const mymodule_api_v1_s*)dpm_get_api(ctx, mod, "mymodule", 1);
```
The returned table was validated at load and is usable for the life of the context. NULL means the module does not provide that API at that version.
## Enumerating modules ## Enumerating modules
@@ -90,8 +86,8 @@ The cursor covers every valid module in the module path; invalid candidates are
## Services ## Services
- **`dpm_core_version`()** — the libdpm-core version; callable without a context. - **`dpm_core_version`()** — the library version; callable without a context.
- **`dpm_module_info_of`(ctx, mod, out)** — the name, version, description, and minimum-libdpm-core version read from a loaded module. - **`dpm_module_info_of`(ctx, mod, out)** — the name, version, description, and minimum-library version 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_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_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
- **`dpm_module_path`(ctx)** — the resolved module directory. - **`dpm_module_path`(ctx)** — the resolved module directory.

View File

@@ -4,59 +4,55 @@
- Must be able to operate in a barren environment providing libc and libstdc++ — the standard build runs there as-is, no special variants. On a fully populated system the same binaries simply have more modules loadable. - Must be able to operate in a barren environment providing libc and libstdc++ — the standard build runs there as-is, no special variants. On a fully populated system the same binaries simply have more modules loadable.
- Capability grows in layers: each layer installs the dependencies of the next using only what already works. - Capability grows in layers: each layer installs the dependencies of the next using only what already works.
- Every implementation exists exactly once, and is consumable by the CLI, by other layers, and by external programs (build systems, Dark Horse tooling) through C interfaces. - Every implementation exists exactly once, and is consumable by the `dpm` binary, by other layers, and by external programs (build systems, Dark Horse tooling) through C interfaces.
- No module ever links, includes, or hardcodes anything belonging to another module. Every module-to-module interaction passes through libdpm-core.so.
## Architecture overview ## Architecture overview
``` ```
dpm CLI build systems / DHL tools / other languages the dpm binary -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
\ / a build system -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
v v
libdpm-core.so
(discovery, validation, routing,
version reporting, config, logging)
| |
v v
raw module pkg module ... future modules (repo, source, ...)
(one .so) (one .so)
| |
v v
backing tree sqlite3
(source of truth) (derived cache)
``` ```
`libdpm-core.so` is the single entry point for everything. Modules are shared objects that implement package functionality. All routing — library-to-module and module-to-module — passes through libdpm-core. No consumer touches `dlopen`, `dlsym`, or module discovery itself. `<dpm/core.h>` is the declared interface; `libdpm-core.so` implements it. A caller includes the header, links `-ldpm-core`, and asks the library to discover and invoke modules by name. A module reaching a peer performs the identical two steps its own caller performed, which is why the chain above repeats rather than branching into a second mechanism.
The header names no module and carries no module-specific type. It offers discovery and interaction, and that is the entire vocabulary — which is what allows the `dpm` binary, a build system, and a module to all use it without any of them being privileged.
### One process, one library instance
The whole chain executes in the caller's address space. The `dpm` binary (or the build system) is the process; libdpm-core.so is mapped into it; every module the library loads is mapped into it as well. Calls are direct function calls — no subprocess, no serialization, no output parsing.
A module links libdpm-core.so like any other consumer, and when the module is loaded the dynamic linker binds it to the copy already mapped in the process. There is one instance, so a module drives the same context, module registry, configuration, and log targets the original caller opened.
Modules are loaded with `RTLD_LOCAL`, so a module's symbols never enter the global namespace. The only symbols a module can resolve are libdpm-core.so's. No path from one module to another exists that does not pass through the library.
## Versioning model ## Versioning model
Compatibility is directional, and the module is the one that declares it: Compatibility is directional, and the module is the one that declares it:
- **Each module reports the minimum libdpm-core version it supports** (a reserved contract symbol). At load, the running library compares its own version against that minimum: if it is older → refuse with an explicit "libdpm-core too old for this module" report; otherwise load. - **Each module reports the minimum library version it supports** (a reserved contract symbol). At load, the running library compares its own version against that minimum: if it is older → refuse with an explicit "library too old for this module" report; otherwise load.
- **Consuming modules judge module versions; libdpm-core does not.** libdpm-core reports the version it saw at load and draws no conclusion from it. A module that depends on another requires it by name, reads the reported version, and decides for itself whether that version is too new or too old for what it intends to call. A load is a statement that the module is valid, never that it is compatible with a particular caller. - **Consuming modules judge module versions; libdpm-core.so does not.** The library reports the version it saw at load and draws no conclusion from it. A module that depends on another requires it by name, reads the reported version, and decides for itself whether that version is suitable for the commands it intends to issue. A load is a statement that the module is valid, never that it is compatible with a particular caller.
- **An API is identified by name and table version.** A consumer asks for the exact pair it was written against, and a module that no longer carries that pair fails the request. Retiring a table is a version change in the module that retired it, judged by every consumer that requires it.
## libdpm-core.so ## libdpm-core.so
Dependencies: libc, libstdc++, libdl. Never more — libdpm-core must remain loadable in the barren case forever, so package logic never leaks into it. libdpm-core routes and hosts; modules implement. Dependencies: libc, libstdc++, libdl. Never more — the library must remain loadable in the barren case forever, so package logic never leaks into it. libdpm-core.so routes and hosts; modules implement.
libdpm-core provides: It provides:
- **Discovery**: module path resolution, enumeration of installed module .so's. - **Discovery**: module path resolution, enumeration of installed module .so's.
- **Validation**: the full load-time contract enforcement described below. libdpm-core 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. - **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**: - **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.
- generic dispatch — execute a command string with arguments against a named module (what the CLI uses);
- typed access — a consumer requests a module's API at a version and receives a C function table (what modules and external programs use).
- **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. - **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.
- **Common services**: configuration access (per-module namespaces from `/etc/dpm/conf.d/`), logging, module-path queries. - **Common services**: configuration access (per-module namespaces from `/etc/dpm/conf.d/`), logging, module-path queries.
## The libdpm-core C API ## The C API
All functions are extern "C". All returned strings are owned by libdpm-core (or by the module that produced them), are valid until the context is closed, and are never freed by the caller. All functions returning int use 0 for success and nonzero error codes; details of the most recent failure are retrievable per-context. All functions are extern "C". All returned strings are owned by the library (or by the module that produced them), are valid until the context is closed, and are never freed by the caller. All functions returning int use 0 for success and nonzero error codes; details of the most recent failure are retrievable per-context.
### Context lifecycle ### Context lifecycle
**`dpm_ctx* dpm_open(const dpm_open_overrides* overrides)`** **`dpm_ctx* dpm_open(const dpm_open_overrides* overrides)`**
Creates a libdpm-core 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), and log level. Multiple simultaneous contexts with different roots are legal.
**`void dpm_close(dpm_ctx* ctx)`** **`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. 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.
@@ -67,13 +63,12 @@ Releases the context: unloads every module handle it issued, closes log targets,
Resolves the module `name` in the module path and runs the full load-time validation sequence (see Load-time enforcement) if the module is not already loaded in this context. On success returns a module handle owned by the context (repeated calls return the same handle — modules are loaded at most once per context). On failure returns NULL and records the precise reason: not found, or validation step failed with the step and detail. No version criterion is applied here; compatibility is the caller's to determine from the reported version. Resolves the module `name` in the module path and runs the full load-time validation sequence (see Load-time enforcement) if the module is not already loaded in this context. On success returns a module handle owned by the context (repeated calls return the same handle — modules are loaded at most once per context). On failure returns NULL and records the precise reason: not found, or validation step failed with the step and detail. No version criterion is applied here; compatibility is the caller's to determine from the reported version.
**`int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`** **`int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`**
Fills `out` with the loaded module's name, version, description, and minimum-libdpm-core version exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. libdpm-core attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on. Fills `out` with the loaded module's name, version, description, and minimum-library version exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. The library attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on.
**`const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod, const char* api_name, int table_version)`**
Returns the API table `api_name` at `table_version` from a loaded module — the pointer the module exported for that table, already validated (manifest cross-check, magic, minimum size) at load. The caller casts it to the table struct type for that API and version as defined in the module's documented API. Returns NULL if the module does not provide that api/version pair; that fact is known from the manifest without further probing. The table is valid for the life of the context.
**`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv)`** **`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv)`**
Generic dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). libdpm-core adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define. Dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). The library adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define.
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.
### Enumeration ### Enumeration
@@ -81,7 +76,7 @@ Generic dispatch: invokes the module's `dpm_module_execute` with the context, `c
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. 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.
**`int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out)`** **`int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out)`**
Advances the cursor. Fills `out` with the next module's name, version, description, and minimum-libdpm-core version (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end. Advances the cursor. Fills `out` with the next module's name, version, description, and minimum-library version (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end.
**`void dpm_cursor_free(dpm_cursor* cur)`** **`void dpm_cursor_free(dpm_cursor* cur)`**
Releases the cursor. NULL is a no-op. Releases the cursor. NULL is a no-op.
@@ -89,7 +84,7 @@ Releases the cursor. NULL is a no-op.
### Services (available to modules and external consumers alike) ### Services (available to modules and external consumers alike)
**`const char* dpm_core_version(void)`** **`const char* dpm_core_version(void)`**
Returns the libdpm-core version as a static X.Y.Z string. Callable without a context. Returns the library version as a static X.Y.Z string. Callable without a context.
**`const char* dpm_config_get(dpm_ctx* ctx, const char* module, const char* section, const char* key)`** **`const char* dpm_config_get(dpm_ctx* ctx, const char* module, const char* section, const char* key)`**
Returns the configured value for `key` in `section` of the named module's config namespace (`/etc/dpm/conf.d/`&lt;module&gt;.conf; the namespace "core", from `core.conf`, is the library's own). Returns NULL if unset. String valid until context close. Returns the configured value for `key` in `section` of the named module's config namespace (`/etc/dpm/conf.d/`&lt;module&gt;.conf; the namespace "core", from `core.conf`, is the library's own). Returns NULL if unset. String valid until context close.
@@ -105,71 +100,65 @@ Returns a human-readable description of the most recent failure recorded on this
## Module contract ## Module contract
A module is one .so in the module directory. It exports, as extern "C", the following reserved symbols. Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; libdpm-core 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 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)`** **`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`**
The module's generic command entry point. `ctx` is the host context that dispatched the call — the module reaches every libdpm-core service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it. `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. This is the only entry the CLI path ever uses; it must be callable immediately after load with no other setup. The module's command entry point, and its entire functional surface. `ctx` is the host context that dispatched the call — the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_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.
**`const char* dpm_module_version(void)`** **`const char* dpm_module_version(void)`**
Returns the module's own version as an X.Y.Z string. Must be constant for the life of the module. This is the value libdpm-core reports to consumers, and the value they judge compatibility against. Returns the module's own version as an X.Y.Z string. Must be constant for the life of the module. This is the value the library reports to consumers, and the value they judge compatibility against.
**`const char* dpm_module_description(void)`** **`const char* dpm_module_description(void)`**
Returns a one-line human-readable description, used in module listings. Returns a one-line human-readable description, used in module listings.
**`const char* dpm_module_core_min(void)`** **`const char* dpm_module_core_min(void)`**
Returns the minimum libdpm-core version (X.Y.Z) this module supports — the oldest one whose contract and services the module was written against. A libdpm-core older than that refuses to load the module, and says so. Returns the minimum library version (X.Y.Z) this module supports — the oldest one whose contract and services the module was written against. A library older than that refuses to load the module, and says so.
**`const dpm_manifest* dpm_module_manifest(void)`** **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.
Returns a pointer to a static manifest table declaring the module's entire functional surface: an entry count and, per entry, the API name, its table version, and the exact exported symbol that carries the table (e.g. { "raw", 1, "raw_api_v1" }). libdpm-core trusts nothing it doesn't verify: every declared symbol is resolved at load, and only manifest-declared tables are ever handed to consumers. An API absent from the manifest does not exist, even if its symbol does.
**API tables** (the module's functional surface): for each API version, one exported symbol (e.g. `raw_api_v1`) pointing to a plain C struct of function pointers. Every table opens with two fixed members: a magic constant (a fixed value defined by this spec, confirming the exporter agrees on table layout conventions) and the struct size in bytes (populated by the module, letting consumers accept tail-extended revisions of the same version). All parameters and returns are C types only; state passes through opaque handles; errors are int codes. **A module publishes no headers to other modules.** Its capabilities are addressed by command string through `dpm_execute`, so nothing about its internals — types, struct layouts, symbol names — is ever compiled into a caller. A module's documented command vocabulary is its interface.
**Symbol naming**: functional exports are prefixed with the module's name (raw_\*, pkg_\*); the dpm_ prefix is reserved for the contract and libdpm-core.
## Load-time enforcement ## Load-time enforcement
libdpm-core 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: 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`. 1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`.
2. **Minimum-version handshake.** `dpm_module_core_min`() must be ≤ the running libdpm-core version. If the library is too old, refuse and say so — the remedy is updating libdpm-core, and the message names it. Old modules on a newer libdpm-core always pass. 2. **Minimum-version handshake.** `dpm_module_core_min`() must be ≤ the running library version. If the library is too old, refuse and say so — the remedy is updating the library, and the message names it.
3. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse. 3. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse.
4. **Cross-check the manifest.** Every API the module declares must actually resolve via `dlsym`. A module advertising an API it doesn't export is refused. libdpm-core validates the module's entire declared surface at load, before offering any of it.
5. **Table sanity.** Check each declared table's magic constant (catches modules built against a stale or wrong layout) and minimum size for its version.
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. The residual C-ABI limit — `dlsym` cannot verify signatures — is covered in practice by the magic, size, minimum-version handshake, and probes; defeating those requires deliberate lying, which is a package-signing concern upstream of the loader. 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.
## Module: raw — the file-based installer ## Module: raw — the file-based installer
Ships with the base system alongside libdpm-core. Depends on the baseline only; archive decompression is vendored in, and the package format is chosen to keep that small. This is what makes barren-environment operation possible: libdpm-core plus raw function with nothing else present. Ships with the base system alongside libdpm-core.so. Depends on the baseline only; archive decompression is vendored in, and the package format is chosen to keep that small. This is what makes barren-environment operation possible: libdpm-core.so plus raw function with nothing else present.
- **Owns the backing tree** (`/var/lib/dpm/`): one directory per installed package holding manifest, metadata, and hooks. The tree is the database at this layer. - **Owns the backing tree** (`/var/lib/dpm/`): one directory per installed package holding manifest, metadata, and hooks. The tree is the database at this layer.
- **Operations** (exposed both as commands and in `raw_api_v1`): install a package file, remove, verify, and queries answered by walking the tree — slow but always correct, zero dependencies. - **Operations**, addressed as commands: install a package file, remove, verify, and queries answered by walking the tree — slow but always correct, zero dependencies.
- **Owns the lock file and an append-only transaction journal** with a generation counter. Every mutation in the entire system ultimately passes through raw, so locking and journaling are implemented exactly once and inherited by every layer above. - **Owns the lock file and a transaction journal** with a generation counter. Every mutation in the entire system ultimately passes through raw, so locking and journaling are implemented exactly once and inherited by every layer above.
## Module: pkg — the full package manager ## Module: pkg — the full package manager
Ships as a package, installed by raw once sqlite3 is installed. Requires raw (via libdpm-core, judging raw's reported version itself) and libsqlite3. Ships as a package, installed by raw once sqlite3 is installed. Requires raw — by name, through libdpm-core.so, judging raw's reported version itself and libsqlite3.
- Never touches the tree directly: every filesystem mutation is a call into raw's API table, obtained from libdpm-core, in-process — shared locking, real error propagation, no output parsing. - Never touches the tree directly: every filesystem mutation is a dispatch to raw through the library, in-process — shared locking, real error propagation, no output parsing. pkg is built with no knowledge of raw beyond its name and its command vocabulary.
- **The sqlite database is a derived cache** under one invariant: it contains nothing that cannot be rebuilt by scanning the tree. It records the last journal generation it applied; on open, if the tree is ahead (someone used raw directly — explicitly allowed, that's the escape hatch for broken systems), it replays or rebuilds. Self-healing by construction. - **The sqlite database is a derived cache** under one invariant: it contains nothing that cannot be rebuilt by scanning the tree. It records the last journal generation it applied; on open, if the tree is ahead (someone used raw directly — explicitly allowed, that's the escape hatch for broken systems), it replays or rebuilds. Self-healing by construction.
- Adds what the cache enables: fast queries, dependency resolution against the installed set, multi-package transactions with rollback. - Adds what the cache enables: fast queries, dependency resolution against the installed set, multi-package transactions with rollback.
- Exposes `pkg_api_v1` for consumers that want dependency-aware operations; they transitively get raw's guarantees because there is no second code path to the tree. - Consumers that want dependency-aware operations dispatch to pkg by name; they transitively get raw's guarantees because there is no second code path to the tree.
## The CLI ## The dpm binary
`dpm` is argument parsing and printing. It links libdpm-core, enumerates modules, and forwards subcommands through generic dispatch. Its command surface is exactly the set of loadable modules — in the barren case that's raw's commands; on a full system, everything installed. No capability logic lives in the CLI. `dpm` is argument parsing and printing. It includes `<dpm/core.h>`, links `-ldpm-core`, enumerates modules, and forwards subcommands through dispatch. Its command surface is exactly the set of loadable modules — in the barren case that's raw's commands; on a full system, everything installed. No capability logic lives in it.
## External consumers ## External consumers
Build systems and Dark Horse components link `libdpm-core.so` — the same library, the same path as everything else: Build systems and Dark Horse components link `libdpm-core.so` as an ordinary shared library dependency — `#include <dpm/core.h>`, `-ldpm-core`, the same as any other library they link:
- open a context (optionally against an alternate root), - open a context (optionally against an alternate root),
- require the layer they need, - require the module they need,
- read its reported version and decide whether it suits them, - read its reported version and decide whether it suits them,
- fetch its API table, - dispatch commands to it.
- call C functions directly.
libdpm-core installs its header to the standard include path and its library to the standard lib path: a consumer writes #include &lt;`dpm/core.h`&gt;, links `-ldpm-core`, and calls package manager module functions. A consumer that opens a default context (no overrides) is operating the installed package manager itself — system configuration, system module path, system tree, system locking — exactly as if it were invoking the installed `dpm` command, because the CLI is just another caller of the same library. Overrides redirect individual paths only when a caller explicitly sets them. Whether a consumer targets raw only (image builders that just deploy trees) or pkg (dependency-aware tooling) is their choice of require(); behavior is identical to the CLI's because it is the same implementation. The header installs to the standard include path and the library to the standard lib path. A consumer that opens a default context (no overrides) is working against system configuration, the system module path, the system tree, and system locking — the same environment the installed `dpm` binary sees, because that binary is just another caller of the same library. Overrides redirect individual paths only when a caller explicitly sets them. Whether a consumer addresses raw only (image builders that just deploy trees) or pkg (dependency-aware tooling) is their choice of require(); behavior is identical to the `dpm` binary's because it is the same implementation.
## Bootstrap chain ## Bootstrap chain
@@ -177,22 +166,22 @@ libdpm-core installs its header to the standard include path and its library to
minimal start: dpm + libdpm-core.so + raw module (baseline deps only) minimal start: dpm + libdpm-core.so + raw module (baseline deps only)
raw installs: sqlite3 package raw installs: sqlite3 package
raw installs: dpm-pkg package (drops the pkg module .so) raw installs: dpm-pkg package (drops the pkg module .so)
now: libdpm-core discovers pkg, validates it, full management is live now: libdpm-core.so discovers pkg, validates it, full management is live
``` ```
Every layer is a package installed and upgraded by the layer beneath it; the package manager maintains itself with the same mechanism it offers the OS. Future modules follow the identical pattern — a repo/network module declares its requirements (pkg, a TLS library), lands as a package, and the capability appears on next discovery. Every layer is a package installed and upgraded by the layer beneath it; the package manager maintains itself with the same mechanism it offers the OS. Future modules follow the identical pattern — a repo/network module declares its requirements (pkg, a TLS library), lands as a package, and the capability appears on next discovery.
## Repository structure ## Repository structure
Modules are developed independently from each other and independently from libdpm-core — one repository per module, plus the libdpm-core repository. Each repo owns its source, build, and tests, and produces exactly one artifact: Modules are developed independently from each other and independently from the library — one repository per module, plus the libdpm-core.so repository. Each repo owns its source, build, and tests, and produces exactly one artifact:
- **The libdpm-core repository**: `libdpm-core.so` and the `dpm` binary. Contains no module code. Its test fixtures include deliberately broken stub modules for validating the loader, and one known-good stub — never a real package module. - **The libdpm-core.so repository**: `libdpm-core.so` and the `dpm` binary. Contains no module code. Its test fixtures include deliberately broken stub modules for validating the loader, and one known-good stub — never a real package module.
- **The exception**: an info module that bundles with the `dpm` binary and libdpm-core, used to test and demonstrate full DPM system functionality where appropriate. - **The exception**: an info module that bundles with the `dpm` binary and libdpm-core.so, used to test and demonstrate full DPM system functionality where appropriate.
- **One repository per module** (raw, pkg, and every future module): produces that module's .so. Links `libdpm-core.so` — the only cross-repo build dependency in the system — and nothing else from DPM. Peer modules never appear in a module's repository, build, or test environment; peers are runtime concerns, faked at test time (contract fakes and stub modules) and real only at distribution-level integration. - **One repository per module** (raw, pkg, and every future module): produces that module's .so. Links `libdpm-core.so` — the only cross-repo build dependency in the system — and nothing else from DPM. Peer modules never appear in a module's repository, build, or test environment; peers are runtime concerns, faked at test time with stub modules and real only at distribution-level integration.
No repository can block another's development: a module builds and its full pre-integration test surface (unit, contract, module-hosting) runs with nothing present but its own checkout and an installed or vendored libdpm-core. Release coordination happens through the versioning model — each consumer judging the versions it is handed — rather than through lockstep builds. No repository can block another's development: a module builds and its full pre-integration test surface runs with nothing present but its own checkout and an installed or vendored libdpm-core.so. Release coordination happens through the versioning model — each consumer judging the versions it is handed — rather than through lockstep builds.
### Layout of the libdpm-core repository ### Layout of the libdpm-core.so repository
``` ```
include/dpm/ public headers — installed to the system include path; the include/dpm/ public headers — installed to the system include path; the
@@ -200,7 +189,7 @@ include/dpm/ public headers — installed to the system include path; the
consumer writes #include <dpm/core.h> consumer writes #include <dpm/core.h>
include/internal/ library-private headers — used only by src/, never installed include/internal/ library-private headers — used only by src/, never installed
src/ implementations of the library src/ implementations of the library
src/cli/ the dpm CLI entry point src/cli/ the dpm binary's entry point
src/bundled-modules/info/ the bundled info module src/bundled-modules/info/ the bundled info module
data/ files installed as-is (core.conf) data/ files installed as-is (core.conf)
tests/ fixture modules, the API test binary, CLI tests tests/ fixture modules, the API test binary, CLI tests
@@ -211,51 +200,49 @@ Every header lives under include/: include/dpm/ is the published API surface and
## Artifacts ## Artifacts
Terminology: **the `dpm` binary** names the command-line tool; **libdpm-core** names the library. Terminology: **the `dpm` binary** names the command-line tool; **libdpm-core.so** names the library; **`<dpm/core.h>`** names its header.
| Artifact | Location on system | | Artifact | Location on system |
|---|---| |---|---|
| `dpm` | `/usr/bin/dpm` | | `dpm` | `/usr/bin/dpm` |
| `libdpm-core.so` | `/usr/lib/libdpm-core.so` | | `libdpm-core.so` | `/usr/lib/libdpm-core.so` |
| `core.h` | `/usr/include/dpm/core.h` |
| `info.so` | `/usr/lib/dpm/modules/info.so` | | `info.so` | `/usr/lib/dpm/modules/info.so` |
| modules (`raw.so`, `pkg.so`, `repo.so`, `source.so`, ...) | `/usr/lib/dpm/modules/<name>.so` | | modules (`raw.so`, `pkg.so`, `repo.so`, `source.so`, ...) | `/usr/lib/dpm/modules/<name>.so` |
## Development and testing ## Development and testing
Development works because the design has no build-time coupling between peers: nothing links against a peer module, ever. "Not all the pieces are there" is the normal, permanent condition at build time. What remains resolves into four test layers, each needing strictly less than the full system. Development works because the design has no build-time coupling between peers: nothing links against a peer module, ever, and nothing includes a peer's headers, ever. "Not all the pieces are there" is the normal, permanent condition at build time. What remains resolves into three test layers, each needing strictly less than the full system.
### What a build requires ### What a build requires
- A module compiles against its own declarations (written to the spec — the externs it exports, the table structs it consumes) plus **`libdpm-core.so`, the one real link dependency** — and libdpm-core is by definition the stable, always-present, baseline-only piece. Cheap to have in every dev environment, trivially vendorable as a checkout. A module compiles against `<dpm/core.h>` and links `libdpm-core.so` the one real link dependency, and by definition the stable, always-present, baseline-only piece. Cheap to have in every dev environment, trivially vendorable as a checkout.
- Peer modules are reached at runtime through the require/get_api pair. Building pkg does not require raw to exist anywhere. The compile-time knowledge of raw is just the `raw_api_v1` struct layout, which is spec, not artifact.
A module repo therefore builds self-contained, always. Peers are reached at runtime by name and command string. Building pkg does not require raw to exist anywhere, and there is no compile-time knowledge of raw to acquire — not a header, not a struct, not a symbol. A module repo therefore builds self-contained, always.
### Test layers ### Test layers
**1. Unit tests — need nothing.** The module's implementation compiles once as an object library, linked into both the .so and a test binary. Pure logic, error paths, parsing — no libdpm-core, no peers. **1. Unit tests — need nothing.** The module's implementation compiles once as an object library, linked into both the .so and a test binary. Pure logic, error paths, parsing — no library, no peers.
**2. Contract tests — need a struct, not a module.** Because every dependency is an API table — a plain struct of function pointers — a fake is just a struct the test fills in with functions that record calls and return canned results. pkg's logic is exercised against a fake `raw_api_v1` (verifying it calls install/remove/query correctly, handles raw's error codes, honors the journal-generation protocol) with raw nowhere on the machine. Injection is built into the architecture; no linker seams required. **2. Module-hosting tests — need libdpm-core.so only.** A harness links the real library, points the module path at the build output plus fixtures, and has it load the just-built .so exactly as production would — full validation included, so contract violations fail here, in CI, not on a user's system. Where the module needs a peer, the fixture directory contains a **stub module**: a tiny .so exporting the reserved symbols and answering the commands the module under test issues, which the library validates and dispatches to like the real thing. The harness then drives `dpm_module_execute` end to end against fixture config and data. Because a peer is addressed by name and command, the stub is a complete substitute — there is nothing else about the real peer that the module under test could have depended on. This layer runs on a bare builder with nothing installed.
**3. Module-hosting tests — need libdpm-core only.** A harness links the real libdpm-core, points the module path at the build output plus fixtures, and has it load the just-built .so exactly as production would — full five-step validation included, so contract violations fail here, in CI, not on a user's system. Where the module needs a peer, the fixture directory contains a **stub module**: a tiny .so exporting the reserved symbols and a fake table, which libdpm-core validates and serves like the real thing. The harness then drives `dpm_module_execute` end to end against fixture config and data. This layer runs on a bare builder with nothing installed. **3. Integration — the only layer that needs everything, and it builds itself.** Real libdpm-core.so plus real raw, then the actual bootstrap chain into a scratch root: `dpm_open` against an alternate root, raw installs sqlite3 and the pkg package into it, the library discovers pkg, real operations run against the throwaway tree. Because alternate roots are first-class in the API, this needs a directory, not a VM. Full-distribution CI does the same with real packages.
**4. Integration — the only layer that needs everything, and it builds itself.** Real libdpm-core plus real raw, then the actual bootstrap chain into a scratch root: `dpm_open` against an alternate root, raw installs sqlite3 and the pkg package into it, libdpm-core discovers pkg, real operations run against the throwaway tree. Because alternate roots are first-class in the API, this needs a directory, not a VM. Full-distribution CI does the same with real packages.
### Day-to-day workflow ### Day-to-day workflow
- Working on **pkg**: edit, run unit + contract tests (instant, zero environment), harness run before merge. A real raw is never needed, or even possessed, until integration. - Working on **pkg**: edit, run unit tests (instant, zero environment), harness run with a raw stub before merge. A real raw is never needed, or even possessed, until integration.
- Working on **raw**: same, except its fakes point the other way — its tests need only fixture package files and a scratch tree. - Working on **raw**: same, except its tests need only fixture package files and a scratch tree.
- Working on **libdpm-core**: its test fixtures are deliberately broken modules — missing symbols, wrong magic, lying manifests, a too-new minimum-version declaration — plus one known-good stub. Development never needs any real package module. - Working on **libdpm-core.so**: its test fixtures are deliberately broken modules — missing symbols, a too-new minimum-version declaration, a malformed version — plus one known-good stub. Development never needs any real package module.
- **Debugging** is the layer-3 harness under a debugger — it is the "run the module without the system" mechanism, so no separate standalone build exists or is maintained. - **Debugging** is the layer-2 harness under a debugger — it is the "run the module without the system" mechanism, so no separate standalone build exists or is maintained.
The discipline that keeps this honest: fakes and stubs are written to the spec, and layer-3 validation plus the layer-4 bootstrap run in CI, so a fake that drifts from reality is caught by the first integration pass rather than shipped. The discipline that keeps this honest: stubs are written to the command vocabulary the real peer documents, and layer-2 validation plus the layer-3 bootstrap run in CI, so a stub that drifts from reality is caught by the first integration pass rather than shipped.
## Development capabilities ## Development capabilities
During development the CLI must be pointable at a local libdpm-core, and that library must be configurable to local paths (module path, config dir, etc.). Two mechanisms provide this: During development the `dpm` binary must be pointable at a locally built library, and that library must be configurable to local paths (module path, config dir, etc.). Two mechanisms provide this:
- **Pointing the CLI at a local libdpm-core** is dynamic-linker territory, needing no DPM mechanism: development builds of the CLI carry an rpath to their own build tree's lib/ directory, so the locally built binary resolves the locally built library (`LD_LIBRARY_PATH` pointed at that lib/ directory achieves the same). The system libdpm-core is never touched. - **Pointing the binary at a local library** is dynamic-linker territory, needing no DPM mechanism: development builds carry an rpath to their own build tree's lib/ directory, so the locally built binary resolves the locally built library (`LD_LIBRARY_PATH` pointed at that lib/ directory achieves the same). The system-installed library is never touched.
- **Pointing that library at local paths** is what the `dpm_open` overrides exist for: config directory, module path, and target root are all fields of the overrides struct, and the CLI exposes them as flags. A dev invocation: - **Pointing that library at local paths** is what the `dpm_open` overrides exist for: config directory, module path, and target root are all fields of the overrides struct, and the `dpm` binary exposes them as flags. A dev invocation:
``` ```
./build/bin/dpm --config-dir ./tests/fixtures/conf --module-path ./build/modules raw install ./fixture.dpm ./build/bin/dpm --config-dir ./tests/fixtures/conf --module-path ./build/modules raw install ./fixture.dpm
@@ -263,18 +250,20 @@ During development the CLI must be pointable at a local libdpm-core, and that li
The config-dir override matters most: once the context reads config from the local conf dir, everything configurable — log file, module path defaults, per-module settings — resolves locally, so a checkout plus its fixtures is a complete self-contained environment. Adding `--root` at a scratch directory makes even real install operations land in a throwaway tree. The config-dir override matters most: once the context reads config from the local conf dir, everything configurable — log file, module path defaults, per-module settings — resolves locally, so a checkout plus its fixtures is a complete self-contained environment. Adding `--root` at a scratch directory makes even real install operations land in a throwaway tree.
**Rule**: every field of the `dpm_open` overrides struct must be exposed as a CLI flag, so anything a linked consumer can redirect, a developer at the shell can redirect too. This holds for any future override field — nothing ships reachable from code but not from the command line. **Rule**: every field of the `dpm_open` overrides struct must be exposed as a flag on the `dpm` binary, so anything a linked consumer can redirect, a developer at the shell can redirect too. This holds for any future override field — nothing ships reachable from code but not from the command line.
## Evolution rules ## Evolution rules
- **Breaking a module API means a new table version**: `raw_api_v2` ships as its own symbol, and `raw_api_v1` is retired on raw's schedule, published in raw's own version. A consumer that requires a retired table finds out when it asks for it, by name and version, before it does any work. - **A module's interface is its command vocabulary.** Retiring or changing the meaning of a command is a version change in the module that owns it, judged by every consumer that dispatches to it.
- **Breaking the library's exported ABI means a new symbol version node**: the version script pins the export set, and a break ships as a new node above the existing one, so binaries already linked keep resolving the node they were built against. - **Breaking the library's exported ABI means a new symbol version generation**: the export set is what consumers link against, so a break is a deliberate, versioned event rather than an incidental one.
- **Compatibility is decided by the consumer**: modules declare the minimum libdpm-core they support, and the library enforces that one handshake because it is the host. Everything else is reported, not enforced — libdpm-core hands a consumer the version it saw and the consumer decides whether to proceed. - **Compatibility is decided by the consumer**: modules declare the minimum library version they support, and libdpm-core.so enforces that one handshake because it is the host. Everything else is reported, not enforced — the library hands a consumer the version it saw and the consumer decides whether to proceed.
## Invariants ## Invariants
1. libdpm-core routes and hosts; modules implement. No package logic in the library, ever. 1. libdpm-core.so routes and hosts; modules implement. No package logic in the library, ever.
2. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through libdpm-core-mediated APIs. 2. `<dpm/core.h>` names no module and carries no module-specific type. It offers discovery and interaction only.
3. Truth lives in the tree; everything above is regenerable cache or convenience. 3. A module never links, includes, or hardcodes anything belonging to another module. All module-to-module interaction passes through libdpm-core.so.
4. Dropping down a layer by hand is always legal; layers above detect it and reconcile. 4. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through library-mediated dispatch.
5. A module is either fully valid or not loaded — no partial states, no consumer-side defense. 5. Truth lives in the tree; everything above is regenerable cache or convenience.
6. Dropping down a layer by hand is always legal; layers above detect it and reconcile.
7. A module is either fully valid or not loaded — no partial states, no consumer-side defense.

View File

@@ -1,43 +1,63 @@
# Developing DPM Modules # Developing DPM Modules
A DPM module is one shared object in the module directory. libdpm-core loads it, validates it completely, routes commands to it, and serves its functions to other modules and to programs linked against libdpm-core. This document covers writing, building, testing, and installing a module. 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 ## The module contract
Every module 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 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)`** **`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`**
The generic command entry point. `ctx` is the host context that dispatched the call; the module reaches every libdpm-core service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it. `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. It must be callable immediately after load with no other setup. The command entry point, and the module's entire functional surface. `ctx` is the host context that dispatched the call; the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_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. It must be callable immediately after load with no other setup.
**`const char* dpm_module_version(void)`** **`const char* dpm_module_version(void)`**
The module's own version as an X.Y.Z string. libdpm-core reports this value to consumers, and each consumer decides for itself whether the version suits it. 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)`** **`const char* dpm_module_description(void)`**
A one-line human-readable description, shown in module listings. A one-line human-readable description, shown in module listings.
**`const char* dpm_module_core_min(void)`** **`const char* dpm_module_core_min(void)`**
The minimum libdpm-core version (X.Y.Z) the module supports — the oldest one whose contract and services it was written against. A libdpm-core older than that refuses to load the module, and the refusal message says so. The minimum library version (X.Y.Z) the module supports — the oldest one whose contract and services it was written against. A libdpm-core.so older than that refuses to load the module, and the refusal message says so.
**`const dpm_manifest* dpm_module_manifest(void)`** The `dpm_ctx` type and the service declarations all come from the installed public header:
A static table declaring the module's entire functional surface: an entry count and, per entry, the API name, its table version, and the exact exported symbol carrying the table — for example { "mymodule", 1, "mymodule_api_v1" }. Only manifest-declared tables are ever handed to consumers; an API absent from the manifest does not exist, even if its symbol does. A module providing no API tables returns a manifest with a count of zero.
The `dpm_manifest` and `dpm_manifest_entry` types, the table header, and the service declarations all come from the installed public header:
``` ```
#include <dpm/core.h> #include <dpm/core.h>
``` ```
## API tables ## Your interface is your command vocabulary
A module's functions are published to consumers as API tables: one exported symbol per API version (`mymodule_api_v1`), pointing to a plain C struct of function pointers. Every table opens with a `dpm_api_table_header` — the `DPM_API_TABLE_MAGIC` constant, then the table struct's size in bytes as the module compiled it. A consumer compares that size against the layout it was built against to see what it has been handed. 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.
All parameters and return values crossing a table are C types only. State passes through opaque handles; errors are int codes. 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 libdpm-core. **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_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_module_info_of`, and decide whether it is suitable for the commands you intend to issue. libdpm-core.so reports; it does not rule.
## Validation at load ## Validation at load
libdpm-core is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, it verifies, in order: every reserved contract symbol resolves; the minimum-version handshake passes; the version and description probes return well-formed values; every manifest-declared symbol resolves; and every declared table carries the correct magic and a sane size. A module failing any 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. 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, in order: every reserved contract symbol resolves; the minimum-version handshake passes; and the version and description probes return well-formed values. A module failing any 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 ## Building
@@ -67,24 +87,24 @@ cmake -B <build-dir>
cmake --build <build-dir> cmake --build <build-dir>
``` ```
libdpm-core is the only DPM link dependency a module ever has. Dependencies on other modules are runtime concerns, resolved through the require/get_api pair — a peer module is never linked and never needs to be present to build. 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.
A module that depends on a peer is the party that judges the peer's version. Require it, read its reported version with `dpm_module_info_of`, and decide whether it is too new or too old for the calls you intend to make. libdpm-core reports; it does not rule.
## Running and testing locally ## Running and testing locally
Load the freshly built module through a locally run `dpm` without installing anything: Load the freshly built module through a locally run `dpm` binary without installing anything:
``` ```
dpm --module-path <build-dir> mymodule <command> dpm --module-path <build-dir> mymodule <command>
``` ```
libdpm-core 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. 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 four 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 ## Installing
Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install). libdpm-core 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). libdpm-core.so discovers the module on its next scan; no registration step exists beyond the file being present and valid.
## A working example ## A working example
The info module bundled with the `dpm` binary and libdpm-core, at `src/bundled-modules/info/`, tests and demonstrates full DPM system functionality, and in doing so shows the contract, an API table, and this build structure in working form. 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.

View File

@@ -2,28 +2,29 @@
## What DPM is ## What DPM is
DPM is the package manager for Dark Horse Linux. At its center is **libdpm-core**, a shared library that discovers modules, validates them, routes calls to them, reports their versions, and provides configuration and logging. Modules — shared objects in the module directory — implement package functionality. DPM is the package manager for Dark Horse Linux. At its center is **libdpm-core.so**, a shared library that discovers modules, validates them, and routes calls to them, and that provides configuration and logging to whatever it has loaded. Modules — shared objects in the module directory — implement package functionality.
**The `dpm` binary** is the command-line tool, and it is one consumer of that library. Build systems, image builders, distribution tooling, and programs in any language with C FFI are equal consumers of the same library through the same interface. **The `dpm` binary** is the command-line tool, and it is one consumer of that library. Build systems, image builders, distribution tooling, and programs in any language with C FFI are equal consumers of the same library through the same interface.
libdpm-core implements no package operations. It routes and hosts. libdpm-core.so implements no package operations. It routes and hosts.
## The shape of the system ## The shape of the system
Everything reaches a module the same way, and a module reaching another module is the same step repeated:
``` ```
the dpm binary build systems / DHL tools / other languages the dpm binary -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
\ / a build system -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
v v
libdpm-core.so
(discovery, validation, routing,
version reporting, config, logging)
|
v
modules
(one .so each)
``` ```
Everything passes through libdpm-core: binary-to-module, module-to-module, program-to-module. No consumer performs module discovery or dynamic loading itself. `<dpm/core.h>` is the declared interface; libdpm-core.so is what implements it. A caller includes the header, links `-ldpm-core`, and asks the library to discover and invoke modules by name.
It all happens in one address space. The `dpm` binary (or the build system) is the process; libdpm-core.so is mapped into it; every module the library loads is mapped into it as well. Calls are direct function calls with no subprocess, no serialization, and no output parsing anywhere in the chain.
Two properties make the routing rule real rather than a convention:
- **There is one instance of libdpm-core.so in the process.** A module links against the library like any other consumer, and when the module is loaded the dynamic linker binds it to the copy already mapped. A module therefore drives the same context, the same module registry, and the same configuration the original caller opened.
- **Modules cannot see each other.** They are loaded with `RTLD_LOCAL`, so a module's symbols never enter the global namespace. The only symbols a module can resolve are libdpm-core.so's, so there is physically no path from one module to another that does not pass through the library.
## Running the dpm binary ## Running the dpm binary
@@ -40,11 +41,11 @@ Four flags redirect the system defaults:
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 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.
## Writing a program against libdpm-core ## Writing a program against the library
A program links libdpm-core and includes `<dpm/core.h>`. Everything it does happens through a **context**, an opaque handle obtained from `dpm_open`. A program includes `<dpm/core.h>` and links `-ldpm-core`, the same as it would link any other shared library. Everything it does happens through a **context**, an opaque handle obtained from `dpm_open`.
Opening a context reads the configuration files, resolves which directory modules will be loaded from, and initializes logging. It loads no modules. The same four things the `dpm` binary exposes as flags are the fields of the overrides struct passed to `dpm_open`, and passing NULL accepts the system defaults — which is what makes a default context equivalent to invoking the installed `dpm` binary, since that binary is just another caller doing the same thing. Opening a context reads the configuration files, resolves which directory modules will be loaded from, and initializes logging. It loads no modules. The same four things the `dpm` binary exposes as flags are the fields of the overrides struct passed to `dpm_open`, and passing NULL accepts the system defaults — which is what makes a default context equivalent to what the installed `dpm` binary sees, since that binary is just another caller doing the same thing.
The target 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 contexts with different roots may be open at once. The target 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 contexts with different roots may be open at once.
@@ -52,16 +53,33 @@ The context owns everything it hands out. Every string a caller receives stays v
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_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_last_error` carrying the precise reason. Modules load at most once per context, and repeated calls return the same handle.
**Reading what libdpm-core saw.** `dpm_module_info_of` fills in a module's name, version, description, and minimum-libdpm-core version. Those values are reported, and no conclusion is drawn from them. **Reading what the library saw.** `dpm_module_info_of` fills in a module's name, version, description, and minimum-library version. Those values are reported, and no conclusion is drawn from them.
**Calling into a module.** Two paths: **Calling into a module.** `dpm_execute` passes a command name and an argument vector to the module's entry point and returns its result. This is the only path into module code, and it is the same one the `dpm` binary takes.
- `dpm_execute` passes a command name and an argument vector to the module's entry point. This is the path the `dpm` binary takes.
- `dpm_get_api` returns a plain C struct of function pointers for a named API at a stated version. This is the path modules and external programs take to call functions directly.
**Enumerating.** `dpm_list_modules` yields a cursor over every valid module, which is what backs the listing the `dpm` binary prints. **Enumerating.** `dpm_list_modules` yields a cursor over every valid module, which is what backs the listing the `dpm` binary prints.
**Services.** A module reaches libdpm-core through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_module_path` to learn where modules live, `dpm_core_version` to learn the library's version. A module needs no file handling and no logging machinery of its own. **Services.** A module reaches the library through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_module_path` to learn where modules live, `dpm_core_version` to learn the library's version. A module needs no file handling and no logging machinery of its own.
## How a module reaches another module
A module is a consumer of `<dpm/core.h>` exactly like the `dpm` binary is. To reach a peer it performs the identical two steps its own caller performed: ask the library for the module by name, then ask the library 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_last_error(ctx));
return 1;
}
return dpm_execute(ctx, peer, "somecommand", argc, argv);
}
```
The `ctx` a module needs is the one handed to it in its own entry point, so it requires nothing else to reach the library.
**A module never links, includes, or hardcodes anything belonging to another module.** No peer headers, no shared struct layouts, no peer symbols. The only build dependency a module has is libdpm-core.so, and the only knowledge it holds about a peer is the peer's name and the command it wants to run. That is what allows every module to live in its own repository and be built with no peer present anywhere on the machine.
## What DPM reads and writes on disk ## What DPM reads and writes on disk
@@ -86,19 +104,17 @@ The context owns everything it hands out. Every string a caller receives stays v
*What a module author implements.* *What a module author implements.*
A module is one `.so` in the module directory exporting five reserved symbols: a command entry point, its own version, a one-line description, the minimum libdpm-core version it supports, and a manifest. The manifest declares the module's entire functional surface — for each API, its name, table version, and the exact exported symbol carrying the table. 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 minimum library version it supports. It includes `<dpm/core.h>` for those declarations and links `-ldpm-core`, and that is its entire build dependency.
Beyond those five, a module exports one symbol per API table. A table is a C struct of function pointers opening with a magic constant and the struct's size in bytes as the module compiled it, which lets a consumer accept a tail-extended revision of the same version. Everything crossing a table is a C type; state passes through opaque handles; errors are int codes. 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.
## Load-time enforcement ## Load-time enforcement
libdpm-core validates a module completely before offering it to anyone, in five steps: libdpm-core.so validates a module completely before offering it to anyone:
1. Every reserved contract symbol resolves. 1. Every reserved contract symbol resolves.
2. The module's minimum libdpm-core version is not newer than the running library's own. 2. The module's minimum library version is not newer than the running library's own.
3. The version and description probes return well-formed values. 3. The version and description probes return well-formed values.
4. Every symbol the manifest declares actually resolves.
5. Every declared table carries the correct magic constant and a sane size.
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. 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.
@@ -106,17 +122,15 @@ From the command line this shows up as a module missing from `--list-modules` wi
## Versioning ## Versioning
libdpm-core enforces exactly one version rule, the one where it is the host: each module states the oldest libdpm-core it supports, and a library older than that refuses to load the module, with a message naming the remedy. libdpm-core.so enforces exactly one version rule, the one where it is the host: each module states the oldest library version it supports, and a library older than that refuses to load the module, with a message naming the remedy.
Every other version question belongs to the consumer. A module that depends on another requires it, reads the version reported back, and decides for itself whether that version is too new or too old for the calls it intends to make. libdpm-core reports what it saw and draws no conclusion from it, so a handle means the module is valid rather than that it suits any particular caller. Every other version question belongs to the consumer. A module that depends on another requires it, reads the version reported back, and decides for itself whether that version is suitable for the commands it intends to issue. libdpm-core.so reports what it saw and draws no conclusion from it, so a handle means the module is valid rather than that it suits any particular caller.
A breaking change to a module's API means a new table version under its own symbol, and the module that owns it decides when the old version is retired. Because a consumer asks for an API by name and version, a retired table surfaces as a refused request at load, before the consumer has done any work.
## Why it works this way ## Why it works this way
### Because it must run on a barren system ### Because it must run on a barren system
The founding constraint is that DPM has to work in an environment providing nothing but libc and libstdc++, using the same binaries that run on a fully populated system. That forbids libdpm-core from taking on any dependency beyond the baseline, which forbids package logic from living inside it, which produces the division the whole architecture rests on: **libdpm-core routes and hosts, modules implement.** Anything needing a database, compression, or TLS is a module that arrives later. The founding constraint is that DPM has to work in an environment providing nothing but libc and libstdc++, using the same binaries that run on a fully populated system. That forbids libdpm-core.so from taking on any dependency beyond the baseline, which forbids package logic from living inside it, which produces the division the whole architecture rests on: **libdpm-core.so routes and hosts, modules implement.** Anything needing a database, compression, or TLS is a module that arrives later.
### Because capability has to grow in layers ### Because capability has to grow in layers
@@ -124,48 +138,48 @@ Each layer of the system installs the dependencies of the next using only what a
### Because one implementation must serve every caller ### Because one implementation must serve every caller
The requirement that every implementation exist exactly once, and be consumable by the `dpm` binary, by other layers, and by external programs, is what makes libdpm-core a C ABI library rather than an application with a library carved out of it. It is also why the command line and the C API stay in step: the flags are the override fields, one for one, so a program and a person redirect the same things by the same names. The requirement that every implementation exist exactly once, and be consumable by the `dpm` binary, by other layers, and by external programs, is what makes libdpm-core.so a C ABI library rather than an application with a library carved out of it. It is also why the command line and the C API stay in step: the flags are the override fields, one for one, so a program and a person redirect the same things by the same names.
### Because nothing may be trusted that has not been verified ### Because nothing may be trusted that has not been verified
libdpm-core is the sole authority on module validity, and the contract is enforced by its validator rather than by an SDK a module author might skip, patch, or fall behind. That is why validation is all-or-nothing and why it happens at load: failures surface loudly and itemized at install time instead of halfway through an operation on a user's system. The manifest exists so a module's entire declared surface can be checked before any of it is offered — an API absent from the manifest does not exist, even if its symbol does. libdpm-core.so is the sole authority on module validity, and the contract is enforced by its validator rather than by an SDK a module author might skip, patch, or fall behind. That is why validation is all-or-nothing and why it happens at load: failures surface loudly and itemized at install time instead of halfway through an operation on a user's system.
The residual limit is that dynamic symbol lookup cannot verify a function signature. The magic constant, the size field, the minimum-version handshake, and the probes cover this in practice; defeating them takes deliberate lying, which is a package-signing concern upstream of the loader.
### Because coordination happens at load, not at build ### Because coordination happens at load, not at build
A consumer states the module and API version it needs and is told at load whether it is there. That is what allows modules to be released independently: agreement is reached when the pieces meet, through declared versions each consumer judges for itself, instead of through lockstep builds. A caller states the module name and command it needs and is told at load whether the module is there. That is what allows modules to be released independently: agreement is reached when the pieces meet, through declared versions each consumer judges for itself, instead of through lockstep builds.
### Because modules are developed independently ### Because modules are developed independently
One repository per module, plus the libdpm-core repository. A module links libdpm-core and nothing else from DPM; peer modules never appear in its repository, build, or test environment. "Not all the pieces are there" is the normal, permanent condition at build time, so the architecture is arranged to make that a non-event: One repository per module, plus the libdpm-core.so repository. A module links libdpm-core.so and nothing else from DPM; peer modules never appear in its repository, build, or test environment. "Not all the pieces are there" is the normal, permanent condition at build time, so the architecture is arranged to make that a non-event:
- A module compiles against its own declarations plus libdpm-core. - A module compiles against `<dpm/core.h>` and nothing else from DPM.
- Every dependency it consumes is a plain struct of function pointers, so a fake is a struct the test fills in — dependency injection is inherent, with no linker seams. - A peer is addressed by name and command string, so nothing about a peer needs to exist at compile time.
- A harness that links the real libdpm-core and points the module path at build output plus fixture stubs exercises the real five-step validation on a bare builder. - A harness that links the real libdpm-core.so and points the module path at build output plus fixture stubs exercises the real validation on a bare builder.
- Only final integration needs the whole system, and because alternate roots are first-class, it needs a directory rather than a virtual machine. - Only final integration needs the whole system, and because alternate roots are first-class, it needs a directory rather than a virtual machine.
This repository's own fixtures are deliberately broken stub modules — missing symbols, bad magic, a lying manifest, a too-new minimum version, a malformed version — plus one known-good stub. Developing libdpm-core never requires a real package module to exist. This repository's own fixtures are deliberately broken stub modules — missing symbols, a too-new minimum version, a malformed version — plus one known-good stub. Developing libdpm-core.so never requires a real package module to exist.
## What this repository produces ## What this repository produces
- **`libdpm-core.so`** — the library - **`libdpm-core.so`** — the library
- **the `dpm` binary** — the command-line tool - **the `dpm` binary** — the command-line tool
- **info** — a module bundled with the `dpm` binary and libdpm-core, used to test and demonstrate full DPM system functionality where appropriate - **info** — a module bundled with the `dpm` binary and libdpm-core.so, used to test and demonstrate full DPM system functionality where appropriate
Every other module is developed against libdpm-core and lives outside this repository. Every other module is developed against libdpm-core.so and lives outside this repository.
## Invariants ## Invariants
1. libdpm-core routes and hosts; modules implement. No package logic inside it, ever. 1. libdpm-core.so routes and hosts; modules implement. No package logic inside it, ever.
2. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through mediated APIs. 2. `<dpm/core.h>` names no module and carries no module-specific type. It offers discovery and interaction only.
3. A module is either fully valid or not loaded — no partial states, no consumer-side defense. 3. A module never links, includes, or hardcodes anything belonging to another module. All module-to-module interaction passes through libdpm-core.so.
4. Anything a linked program can redirect, a shell user can redirect too. 4. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through mediated calls.
5. A module is either fully valid or not loaded — no partial states, no consumer-side defense.
6. Anything a linked program can redirect, a shell user can redirect too.
## Further reading ## Further reading
- **DESIGN.md** — the full design specification - **DESIGN.md** — the full design specification
- **CONSUMERS.md** — linking libdpm-core and driving it from a program - **CONSUMERS.md** — linking libdpm-core.so and driving it from a program
- **MODULES.md** — writing, building, testing, and installing a module - **MODULES.md** — writing, building, testing, and installing a module
- **BUILD.md** — building, testing, and installing libdpm-core and the `dpm` binary - **BUILD.md** — building, testing, and installing libdpm-core.so and the `dpm` binary
- **DOCUMENTATION.md** — generating the code reference - **DOCUMENTATION.md** — generating the code reference

View File

@@ -95,37 +95,6 @@ typedef struct dpm_module_info {
const char* core_min; /* minimum libdpm-core version it supports */ const char* core_min; /* minimum libdpm-core version it supports */
} dpm_module_info; } dpm_module_info;
/* ------------------------------------------------------------------ */
/* Module contract structures (layout fixed by the DPM spec) */
/* ------------------------------------------------------------------ */
/** Magic constant opening every module API table. */
#define DPM_API_TABLE_MAGIC 0x314D5044u /* "DPM1" */
/**
* Every module API table begins with this header: the magic constant,
* then the full size in bytes of the table struct as the exporting
* module compiled it (permits tail-extension within a table version).
*/
typedef struct dpm_api_table_header {
uint32_t magic;
uint32_t size;
} dpm_api_table_header;
/** One entry of a module's declared functional surface. */
typedef struct dpm_manifest_entry {
const char* api_name; /* e.g. "raw" */
int table_version; /* e.g. 1 */
const char* symbol; /* exact exported symbol carrying the
table, e.g. "raw_api_v1" */
} dpm_manifest_entry;
/** A module's manifest: its entire declared functional surface. */
typedef struct dpm_manifest {
uint32_t count;
const dpm_manifest_entry* entries;
} dpm_manifest;
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Context lifecycle */ /* Context lifecycle */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -199,24 +168,6 @@ DPM_API dpm_module* dpm_require(dpm_ctx* ctx, const char* name);
DPM_API int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, DPM_API int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod,
dpm_module_info* out); dpm_module_info* out);
/**
* @brief Returns a module's API table for direct typed calls
*
* The table was already validated (manifest cross-check, magic,
* minimum size) at module load. The caller casts the pointer to the
* table struct type for that API and version. The table is valid for
* the life of the context.
*
* @param ctx The libdpm-core context
* @param mod A module handle from dpm_require()
* @param api_name The API name as declared in the module's manifest
* @param table_version The table version to retrieve
* @return The table pointer, or NULL if the module does not provide
* that api/version pair
*/
DPM_API const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod,
const char* api_name, int table_version);
/** /**
* @brief Dispatches a command to a module * @brief Dispatches a command to a module
* *
@@ -224,6 +175,11 @@ DPM_API const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod,
* command, and the argument vector. argv[0] is the command when * command, and the argument vector. argv[0] is the command when
* argc > 0; semantics beyond that are the module's to define. * argc > 0; semantics beyond that are the module's to define.
* *
* This is the only 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 — the same call
* a module makes to reach a peer.
*
* @param ctx The libdpm-core context * @param ctx The libdpm-core context
* @param mod A module handle from dpm_require() * @param mod A module handle from dpm_require()
* @param command The command name; NULL or empty behaves as the * @param command The command name; NULL or empty behaves as the
@@ -341,12 +297,12 @@ DPM_API const char* dpm_last_error(dpm_ctx* ctx);
* const char* dpm_module_version(void); * const char* dpm_module_version(void);
* const char* dpm_module_description(void); * const char* dpm_module_description(void);
* const char* dpm_module_core_min(void); * const char* dpm_module_core_min(void);
* const dpm_manifest* dpm_module_manifest(void);
* *
* plus one exported table symbol per manifest entry. libdpm-core * dpm_module_execute is the module's entire functional surface; its
* refuses to * capabilities are addressed by command string, so a module publishes
* load any module that does not validate completely (see the DPM * no headers, struct layouts, or symbols to anything that calls it.
* specification: load-time enforcement). * The library refuses to load any module that does not validate
* completely (see the DPM specification: load-time enforcement).
*/ */
#ifdef __cplusplus #ifdef __cplusplus

View File

@@ -35,7 +35,6 @@ struct dpm_module {
std::string version; std::string version;
std::string description; std::string description;
std::string core_min; std::string core_min;
const dpm_manifest* manifest = nullptr;
int (*execute)(dpm_ctx*, const char*, int, char**) = nullptr; int (*execute)(dpm_ctx*, const char*, int, char**) = nullptr;
}; };

View File

@@ -2,10 +2,10 @@
* @file info.cpp * @file info.cpp
* @brief The info module: contract symbols and command routing * @brief The info module: contract symbols and command routing
* *
* Bundles with the dpm binary and libdpm-core; used for testing and * Bundles with the dpm binary and libdpm-core.so; used for testing and
* reporting functionality of libdpm-core. Implements the full DPM * reporting functionality of libdpm-core.so. Implements the full DPM
* module contract: the five reserved * module contract: the four reserved symbols, with every capability
* symbols plus a manifest-declared API table. * reached by command string through the entry point.
* *
* @copyright Copyright (c) 2026 SILO GROUP LLC * @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org> * @author Chris Punches <chris.punches@silogroup.org>
@@ -30,42 +30,6 @@
#define INFO_MODULE_VERSION "0.1.0" #define INFO_MODULE_VERSION "0.1.0"
#define INFO_CORE_MIN "0.1.0" #define INFO_CORE_MIN "0.1.0"
/* ------------------------------------------------------------------ */
/* info_api_v1 — typed access surface */
/* ------------------------------------------------------------------ */
/**
* @brief Version 1 API table for the info module
*
* Lets consumers exercise the typed-access path (require → get_api
* → call) end to end, which is part of this module's purpose of
* testing libdpm-core functionality.
*/
struct info_api_v1_s {
dpm_api_table_header hdr;
const char* (*version)(void); /**< info module version */
const char* (*description)(void); /**< info module description */
};
static const char* api_version(void)
{
return INFO_MODULE_VERSION;
}
static const char* api_description(void)
{
return "Reports and tests libdpm-core functionality.";
}
extern "C" {
extern const struct info_api_v1_s info_api_v1;
const struct info_api_v1_s info_api_v1 = {
{ DPM_API_TABLE_MAGIC, sizeof(struct info_api_v1_s) },
api_version,
api_description,
};
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Reserved contract symbols */ /* Reserved contract symbols */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -95,19 +59,7 @@ extern "C" const char* dpm_module_core_min(void)
} }
/** /**
* @brief Declares the module's entire functional surface * @brief Command entry point
*/
extern "C" const dpm_manifest* dpm_module_manifest(void)
{
static const dpm_manifest_entry entries[] = {
{ "info", 1, "info_api_v1" },
};
static const dpm_manifest manifest = { 1, entries };
return &manifest;
}
/**
* @brief Generic command entry point
* *
* Routes the command to the appropriate handler. NULL or empty * Routes the command to the appropriate handler. NULL or empty
* command behaves as help. * command behaves as help.

View File

@@ -1,27 +1,18 @@
/* /*
* libdpm-core.map — the linker version script for libdpm-core.so * libdpm-core.map — the export list for libdpm-core.so
* *
* Passed to the linker with --version-script. It does two things: it * Passed to the linker with --version-script. Names under `global` are
* pins the library's exported symbol list, and it stamps every exported * exported; `local: *` hides everything else, so the public C API is the
* symbol with the version node named below. * only surface a consumer or a loaded module can bind to.
* *
* What goes in `global`: every function declared in the public header, * `global` is every function in include/dpm/core.h and nothing else. A
* include/dpm/core.h, and nothing else. The library compiles with * new public function is added here in the commit that adds it to the
* hidden default visibility, so `local: *` is what the implementation * header.
* already gets; listing it here makes the export set explicit and fails
* the link loudly if the two ever disagree.
* *
* Changing it: adding a public function means adding its name here in * The node name stamps each symbol (dpm_open@@DPM_CORE_1.0), so a break
* the same commit that adds it to the header. Removing or renaming one * can ship as a new node while already-linked binaries keep resolving
* breaks every consumer already linked against it, so a removal ships * the old one. Newest node first. DPM_CORE_0.1 was never released; it is
* as a new node above the existing one — binaries already linked * the shape a retired node takes.
* against the older node keep resolving it while new links bind to the
* newer one.
*
* The node name carries the ABI generation. Newest node first, retired
* generations below it. DPM_CORE_0.1 at the bottom was never released —
* it is there so the next break follows its shape instead of inventing
* one.
*/ */
DPM_CORE_1.0 { DPM_CORE_1.0 {
global: global:
@@ -29,7 +20,6 @@ DPM_CORE_1.0 {
dpm_close; dpm_close;
dpm_require; dpm_require;
dpm_module_info_of; dpm_module_info_of;
dpm_get_api;
dpm_execute; dpm_execute;
dpm_list_modules; dpm_list_modules;
dpm_cursor_next; dpm_cursor_next;

View File

@@ -2,9 +2,12 @@
* @file modules.cpp * @file modules.cpp
* @brief Module discovery, load-time validation, routing, enumeration * @brief Module discovery, load-time validation, routing, enumeration
* *
* Implements the load-time enforcement sequence: libdpm-core is the sole * Implements the load-time enforcement sequence: libdpm-core.so is the
* authority on module validity. A module is either fully valid or not * sole authority on module validity. A module is either fully valid or
* loaded — no partial states. * not loaded — no partial states.
*
* Dispatch through dpm_execute is the only path into module code, so a
* caller never holds compile-time knowledge of what it is calling.
* *
* @copyright Copyright (c) 2026 SILO GROUP LLC * @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org> * @author Chris Punches <chris.punches@silogroup.org>
@@ -51,9 +54,8 @@ void dpm_internal_unload(void* handle)
namespace { namespace {
using execute_fn = int (*)(dpm_ctx*, const char*, int, char**); using execute_fn = int (*)(dpm_ctx*, const char*, int, char**);
using string_fn = const char* (*)(void); using string_fn = const char* (*)(void);
using manifest_fn = const dpm_manifest* (*)(void);
/** dlsym with dlerror() discipline; returns nullptr on any error. */ /** dlsym with dlerror() discipline; returns nullptr on any error. */
void* resolve(void* handle, const char* symbol) void* resolve(void* handle, const char* symbol)
@@ -95,7 +97,6 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
"dpm_module_version", "dpm_module_version",
"dpm_module_description", "dpm_module_description",
"dpm_module_core_min", "dpm_module_core_min",
"dpm_module_manifest",
}; };
std::string missing; std::string missing;
@@ -117,7 +118,6 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version")); auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description")); auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
auto core_min_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_core_min")); auto core_min_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_core_min"));
auto manifest_f = reinterpret_cast<manifest_fn>(resolve(handle, "dpm_module_manifest"));
/* Step 2: minimum-version handshake. */ /* Step 2: minimum-version handshake. */
const char* core_min = core_min_f(); const char* core_min = core_min_f();
@@ -149,62 +149,12 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
return nullptr; return nullptr;
} }
/* Step 4: cross-check the manifest. */
const dpm_manifest* manifest = manifest_f();
if (!manifest) {
reason = "dpm_module_manifest() returned NULL";
dlclose(handle);
return nullptr;
}
if (manifest->count > 0 && !manifest->entries) {
reason = "manifest declares entries but the entry table is NULL";
dlclose(handle);
return nullptr;
}
for (uint32_t i = 0; i < manifest->count; i++) {
const dpm_manifest_entry& entry = manifest->entries[i];
if (!entry.api_name || !*entry.api_name ||
!entry.symbol || !*entry.symbol || entry.table_version < 1) {
reason = "manifest entry " + std::to_string(i) + " is malformed";
dlclose(handle);
return nullptr;
}
void* table = resolve(handle, entry.symbol);
if (!table) {
reason = std::string("manifest declares API '") + entry.api_name +
"' v" + std::to_string(entry.table_version) +
"' at symbol '" + entry.symbol +
"' but the symbol does not resolve";
dlclose(handle);
return nullptr;
}
/* Step 5: table sanity. */
auto* header = static_cast<const dpm_api_table_header*>(table);
if (header->magic != DPM_API_TABLE_MAGIC) {
reason = std::string("API table '") + entry.symbol +
"' has a bad magic constant";
dlclose(handle);
return nullptr;
}
if (header->size < sizeof(dpm_api_table_header)) {
reason = std::string("API table '") + entry.symbol +
"' reports an impossible size";
dlclose(handle);
return nullptr;
}
}
auto mod = std::make_unique<dpm_module>(); auto mod = std::make_unique<dpm_module>();
mod->name = name; mod->name = name;
mod->handle = handle; mod->handle = handle;
mod->version = version; mod->version = version;
mod->description = description; mod->description = description;
mod->core_min = core_min; mod->core_min = core_min;
mod->manifest = manifest;
mod->execute = exec_f; mod->execute = exec_f;
return mod; return mod;
} }
@@ -250,34 +200,6 @@ int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)
return 0; return 0;
} }
const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod,
const char* api_name, int table_version)
{
if (!ctx || !mod || !api_name) {
return nullptr;
}
for (uint32_t i = 0; i < mod->manifest->count; i++) {
const dpm_manifest_entry& entry = mod->manifest->entries[i];
if (entry.table_version == table_version &&
std::string(entry.api_name) == api_name) {
dlerror();
void* table = dlsym(mod->handle, entry.symbol);
if (dlerror() != nullptr || !table) {
dpmcore::set_error(ctx, std::string("API table symbol '") +
entry.symbol + "' vanished after load");
return nullptr;
}
return table;
}
}
dpmcore::set_error(ctx, std::string("module '") + mod->name +
"' does not provide API '" + api_name +
"' v" + std::to_string(table_version));
return nullptr;
}
int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command,
int argc, char** argv) int argc, char** argv)
{ {

View File

@@ -1,95 +0,0 @@
/**
* @file bad_magic.cpp
* @brief Broken fixture: API table with a wrong magic constant
*
* Contract-complete, but its declared table opens with garbage instead
* of the spec magic. libdpm-core must refuse it at validation step 5.
*
* Part of the Dark Horse Linux Package Manager (DPM)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cstdint>
namespace {
struct table_header {
uint32_t magic;
uint32_t size;
};
struct manifest_entry {
const char* api_name;
int table_version;
const char* symbol;
};
struct manifest {
uint32_t count;
const manifest_entry* entries;
};
} // namespace
struct badmagic_api_v1_s {
table_header hdr;
int (*ping)(void);
};
static int ping(void)
{
return 0;
}
extern "C" {
extern const struct badmagic_api_v1_s badmagic_api_v1;
const struct badmagic_api_v1_s badmagic_api_v1 = {
{ 0xDEADBEEFu, sizeof(struct badmagic_api_v1_s) },
ping,
};
}
extern "C" const char* dpm_module_version(void)
{
return "1.0.0";
}
extern "C" const char* dpm_module_description(void)
{
return "Fixture with a bad table magic.";
}
extern "C" const char* dpm_module_core_min(void)
{
return "0.1.0";
}
extern "C" const void* dpm_module_manifest(void)
{
static const manifest_entry entries[] = {
{ "badmagic", 1, "badmagic_api_v1" },
};
static const manifest m = { 1, entries };
return &m;
}
extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv)
{
(void)ctx;
(void)command;
(void)argc;
(void)argv;
return 0;
}

View File

@@ -20,23 +20,6 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include <cstdint>
namespace {
struct manifest_entry {
const char* api_name;
int table_version;
const char* symbol;
};
struct manifest {
uint32_t count;
const manifest_entry* entries;
};
} // namespace
extern "C" const char* dpm_module_version(void) extern "C" const char* dpm_module_version(void)
{ {
return "banana"; return "banana";
@@ -52,12 +35,6 @@ extern "C" const char* dpm_module_core_min(void)
return "0.1.0"; return "0.1.0";
} }
extern "C" const void* dpm_module_manifest(void)
{
static const manifest m = { 0, nullptr };
return &m;
}
extern "C" int dpm_module_execute(void* ctx, const char* command, extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv) int argc, char** argv)
{ {

View File

@@ -21,23 +21,6 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include <cstdint>
namespace {
struct manifest_entry {
const char* api_name;
int table_version;
const char* symbol;
};
struct manifest {
uint32_t count;
const manifest_entry* entries;
};
} // namespace
extern "C" const char* dpm_module_version(void) extern "C" const char* dpm_module_version(void)
{ {
return "1.0.0"; return "1.0.0";
@@ -53,12 +36,6 @@ extern "C" const char* dpm_module_core_min(void)
return "99.0.0"; return "99.0.0";
} }
extern "C" const void* dpm_module_manifest(void)
{
static const manifest m = { 0, nullptr };
return &m;
}
extern "C" int dpm_module_execute(void* ctx, const char* command, extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv) int argc, char** argv)
{ {

View File

@@ -3,8 +3,9 @@
* @brief Known-good stub module fixture * @brief Known-good stub module fixture
* *
* A complete, valid DPM module written against the documented module * A complete, valid DPM module written against the documented module
* contract with its own declarations — no libdpm-core headers — exactly * contract with its own declarations — no libdpm-core.so headers —
* as a standalone module author would. Used to validate the happy path. * exactly as a standalone module author would. Used to validate the
* happy path.
* *
* Part of the Dark Horse Linux Package Manager (DPM) * Part of the Dark Horse Linux Package Manager (DPM)
* *
@@ -21,45 +22,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include <cstdint> #include <cstring>
namespace {
struct table_header {
uint32_t magic;
uint32_t size;
};
struct manifest_entry {
const char* api_name;
int table_version;
const char* symbol;
};
struct manifest {
uint32_t count;
const manifest_entry* entries;
};
} // namespace
struct good_api_v1_s {
table_header hdr;
int (*ping)(void);
};
static int ping(void)
{
return 42;
}
extern "C" {
extern const struct good_api_v1_s good_api_v1;
const struct good_api_v1_s good_api_v1 = {
{ 0x314D5044u /* "DPM1" */, sizeof(struct good_api_v1_s) },
ping,
};
}
extern "C" const char* dpm_module_version(void) extern "C" const char* dpm_module_version(void)
{ {
@@ -76,21 +39,17 @@ extern "C" const char* dpm_module_core_min(void)
return "0.1.0"; return "0.1.0";
} }
extern "C" const void* dpm_module_manifest(void) /* Answers "ping" with 42 so a dispatch round trip is observable, and
{ 0 for anything else. */
static const manifest_entry entries[] = {
{ "good", 1, "good_api_v1" },
};
static const manifest m = { 1, entries };
return &m;
}
extern "C" int dpm_module_execute(void* ctx, const char* command, extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv) int argc, char** argv)
{ {
(void)ctx; (void)ctx;
(void)command;
(void)argc; (void)argc;
(void)argv; (void)argv;
if (command && std::strcmp(command, "ping") == 0) {
return 42;
}
return 0; return 0;
} }

View File

@@ -1,72 +0,0 @@
/**
* @file lying_manifest.cpp
* @brief Broken fixture: manifest declares an API it doesn't export
*
* Contract-complete, but its manifest names a table symbol that does
* not exist in the .so. libdpm-core must refuse it at validation step 4.
*
* Part of the Dark Horse Linux Package Manager (DPM)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <cstdint>
namespace {
struct manifest_entry {
const char* api_name;
int table_version;
const char* symbol;
};
struct manifest {
uint32_t count;
const manifest_entry* entries;
};
} // namespace
extern "C" const char* dpm_module_version(void)
{
return "1.0.0";
}
extern "C" const char* dpm_module_description(void)
{
return "Fixture whose manifest lies.";
}
extern "C" const char* dpm_module_core_min(void)
{
return "0.1.0";
}
extern "C" const void* dpm_module_manifest(void)
{
static const manifest_entry entries[] = {
{ "ghost", 1, "ghost_api_v1" },
};
static const manifest m = { 1, entries };
return &m;
}
extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv)
{
(void)ctx;
(void)command;
(void)argc;
(void)argv;
return 0;
}

View File

@@ -3,9 +3,8 @@
* @brief Test binary: drives libdpm-core through its public C API * @brief Test binary: drives libdpm-core through its public C API
* *
* Exercises context lifecycle, configuration, the full load-time * Exercises context lifecycle, configuration, the full load-time
* validation matrix against the fixture modules, versioned require, * validation matrix against the fixture modules, require, dispatch,
* typed API access, generic dispatch, and enumeration. Exits nonzero * and enumeration. Exits nonzero on any failure.
* on any failure.
* *
* @copyright Copyright (c) 2026 SILO GROUP LLC * @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org> * @author Chris Punches <chris.punches@silogroup.org>
@@ -57,12 +56,6 @@ static bool error_contains(dpm_ctx* ctx, const char* needle)
return err != nullptr && std::strstr(err, needle) != nullptr; return err != nullptr && std::strstr(err, needle) != nullptr;
} }
/* Mirror of the good fixture's table layout (consumer-side spec decl). */
struct good_api_v1_s {
dpm_api_table_header hdr;
int (*ping)(void);
};
int main(void) int main(void)
{ {
/* ---- dpm_open: invalid explicit override refuses ---- */ /* ---- dpm_open: invalid explicit override refuses ---- */
@@ -115,12 +108,6 @@ int main(void)
CHECK(dpm_require(ctx, "bad_version") == nullptr); CHECK(dpm_require(ctx, "bad_version") == nullptr);
CHECK(error_contains(ctx, "malformed version")); CHECK(error_contains(ctx, "malformed version"));
CHECK(dpm_require(ctx, "lying_manifest") == nullptr);
CHECK(error_contains(ctx, "does not resolve"));
CHECK(dpm_require(ctx, "bad_magic") == nullptr);
CHECK(error_contains(ctx, "magic"));
CHECK(dpm_require(ctx, "nonexistent") == nullptr); CHECK(dpm_require(ctx, "nonexistent") == nullptr);
CHECK(error_contains(ctx, "not found")); CHECK(error_contains(ctx, "not found"));
} }
@@ -144,21 +131,11 @@ int main(void)
CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0); CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0);
CHECK(dpm_module_info_of(ctx, good, nullptr) != 0); CHECK(dpm_module_info_of(ctx, good, nullptr) != 0);
/* typed access */ /* dispatch: the module's return value comes back verbatim, so a
const void* table = dpm_get_api(ctx, good, "good", 1); command round trip is observable without any compile-time
CHECK(table != nullptr); knowledge of the module */
if (table) { CHECK(dpm_execute(ctx, good, "ping", 0, nullptr) == 42);
const auto* api = static_cast<const good_api_v1_s*>(table); CHECK(dpm_execute(ctx, good, "anything_else", 0, nullptr) == 0);
CHECK(api->hdr.magic == DPM_API_TABLE_MAGIC);
CHECK(api->hdr.size == sizeof(good_api_v1_s));
CHECK(api->ping() == 42);
}
/* absent api/version pairs are known from the manifest */
CHECK(dpm_get_api(ctx, good, "good", 2) == nullptr);
CHECK(dpm_get_api(ctx, good, "ghost", 1) == nullptr);
/* generic dispatch */
CHECK(dpm_execute(ctx, good, nullptr, 0, nullptr) == 0); CHECK(dpm_execute(ctx, good, nullptr, 0, nullptr) == 0);
} }