Move version compatibility to the consumer; name libdpm-core explicitly

dpm_require no longer takes a minimum version and applies no version
criterion of its own. A handle now means the module is valid, not that
it suits the caller. dpm_module_info_of is added alongside it, reporting
the name, version, description, and minimum-libdpm-core version read at
load, so a consuming module can judge a dependency's version for itself.
The one rule still enforced is the minimum-version handshake, where
libdpm-core is the host and refuses a module that demands a newer library
than the one running.

dpm_module_info_of joins the version script, so the exported surface is
now fourteen symbols under DPM_CORE_1.0.

Separately, the bare word "core" is gone from prose everywhere. It named
both the command-line tool and the library, so every use forced the
reader to guess which. Text now says "the dpm binary" or "libdpm-core".
Identifiers keep their spelling: libdpm-core, core.h, core.conf, the
"core" configuration namespace, dpm_core_version, core_min, DPM_CORE_1.0,
the dpmcore namespace, test_core, core_api.

Three user-visible strings changed with it: the load-refusal message now
reads "requires libdpm-core >= X, running libdpm-core is Y — update
libdpm-core", and the info module's description and help text name the
library. The test asserting on the refusal text was updated to match.

DESIGN.md's terminology line no longer defines "DPM Core" as the CLI,
which was the source of the ambiguity. OVERVIEW.md is restructured around
the three layers a reader meets DPM at — user, developer, filesystem —
so a code-level symbol never appears without saying whose layer it is.
MODULES.md describes the bundled info module as testing and demonstrating
full DPM system functionality rather than as a reference implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 00:38:21 -04:00
parent da35fe4758
commit c41adc2498
22 changed files with 327 additions and 243 deletions

View File

@@ -5,7 +5,7 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# libdpm-core.so — the core library (C ABI) # libdpm-core.so — the library (C ABI)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
add_library(dpm-core SHARED add_library(dpm-core SHARED
src/context.cpp src/context.cpp
@@ -58,7 +58,7 @@ set_target_properties(dpm PROPERTIES
) )
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# info — the bundled module (tests and reports core functionality) # info — the bundled module (tests and reports libdpm-core functionality)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
add_library(info MODULE add_library(info MODULE
src/bundled-modules/info/info.cpp src/bundled-modules/info/info.cpp
@@ -76,7 +76,7 @@ set_target_properties(info PROPERTIES
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/modules
) )
# The module bundles with core, so building the CLI builds it too: after a # The module bundles with libdpm-core, so building the CLI builds it too: after a
# clean, running dpm from the build tree finds a module directory that is # clean, running dpm from the build tree finds a module directory that is
# populated. This orders the build only — the CLI does not link the module. # populated. This orders the build only — the CLI does not link the module.
add_dependencies(dpm info) add_dependencies(dpm info)
@@ -106,7 +106,7 @@ foreach(fixture good missing_symbols bad_magic lying_manifest core_too_new bad_v
endforeach() endforeach()
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Core API test binary — drives libdpm-core through its public C API # API test binary — drives libdpm-core through its public C API
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
add_executable(test_core tests/test_core.cpp) add_executable(test_core tests/test_core.cpp)
@@ -130,7 +130,7 @@ endforeach()
add_test(NAME core_api COMMAND test_core) add_test(NAME core_api COMMAND test_core)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# CLI end-to-end tests (the CLI is core's cheapest full-stack client) # CLI end-to-end tests (the dpm binary is the cheapest full-stack client)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
add_test(NAME cli_help COMMAND dpm --help) add_test(NAME cli_help COMMAND dpm --help)
set_tests_properties(cli_help PROPERTIES PASS_REGULAR_EXPRESSION "Usage: dpm") set_tests_properties(cli_help PROPERTIES PASS_REGULAR_EXPRESSION "Usage: dpm")

View File

@@ -1,4 +1,4 @@
# Building DPM Core # Building libdpm-core and the dpm binary
## Prerequisites ## Prerequisites
@@ -21,11 +21,11 @@ Artifacts land in:
``` ```
<build-dir>/bin/dpm the CLI <build-dir>/bin/dpm the CLI
<build-dir>/lib/libdpm-core.so the core 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
``` ```
The dpm binary built here finds the locally built libdpm-core.so on its own — an embedded library search path points it at the lib/ directory next to it in the build tree, so running it directly uses the core you just built with nothing to set up first. This is only the default: LD_LIBRARY_PATH takes precedence over the embedded path, so the binary can be pointed at any other core, including the system-installed one. The dpm binary built here finds the locally built libdpm-core.so on its own — an embedded library search path points it at the lib/ directory next to it in the build tree, so running it directly uses the library you just built with nothing to set up first. This is only the default: LD_LIBRARY_PATH takes precedence over the embedded path, so the binary can be pointed at any other libdpm-core, including the system-installed one.
### Running the tests ### Running the tests
@@ -33,7 +33,7 @@ The dpm binary built here finds the locally built libdpm-core.so on its own —
ctest --test-dir <build-dir> --output-on-failure ctest --test-dir <build-dir> --output-on-failure
``` ```
This runs the core API test binary (which exercises the full load-time validation matrix against the fixture modules in tests/fixtures/) and the CLI end-to-end tests. This runs the API test binary (which exercises the full load-time validation matrix against the fixture modules in tests/fixtures/) and the CLI end-to-end tests.
### Running the CLI from the build tree ### Running the CLI from the build tree
@@ -60,11 +60,11 @@ This is the packaging flow: configure once, build once, test what was built, ins
``` ```
bin/dpm the CLI bin/dpm the CLI
lib/libdpm-core.so the core 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
``` ```
Core configuration installs to /etc/dpm/conf.d/core.conf regardless of prefix. The libdpm-core configuration installs to /etc/dpm/conf.d/core.conf regardless of prefix.

View File

@@ -4,7 +4,7 @@ Programs link libdpm-core to operate the package manager directly: build systems
## Compiling and linking ## Compiling and linking
With core installed, include the public header and link the library: With libdpm-core installed, include the public header and link the library:
``` ```
#include <dpm/core.h> #include <dpm/core.h>
@@ -44,13 +44,22 @@ The root override is what makes chroot builds, image assembly, and sysroot manag
## Acquiring and using modules ## Acquiring and using modules
**dpm_require** loads a module by name, on demand, with an optional minimum version: **dpm_require** loads a module by name, on demand:
``` ```
dpm_module* mod = dpm_require(ctx, "mymodule", "1.0.0"); dpm_module* mod = dpm_require(ctx, "mymodule");
``` ```
Core validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent, invalid, or below the minimum — dpm_last_error(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle. 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.
**dpm_module_info_of** reports what libdpm-core saw in the loaded module:
```
dpm_module_info info;
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.
**dpm_execute** drives a module the way the CLI does — a command name and arguments: **dpm_execute** drives a module the way the CLI does — a command name and arguments:
@@ -81,7 +90,8 @@ The cursor covers every valid module in the module path; invalid candidates are
## Services ## Services
- **dpm_core_version()** — core's version; callable without a context. - **dpm_core_version()** — the libdpm-core 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_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.
@@ -104,7 +114,7 @@ int main(void) {
return 1; return 1;
} }
dpm_module* mod = dpm_require(ctx, "info", NULL); dpm_module* mod = dpm_require(ctx, "info");
if (!mod) { if (!mod) {
fprintf(stderr, "%s\n", dpm_last_error(ctx)); fprintf(stderr, "%s\n", dpm_last_error(ctx));
dpm_close(ctx); dpm_close(ctx);

View File

@@ -14,7 +14,7 @@
v v v v
libdpm-core.so libdpm-core.so
(discovery, validation, routing, (discovery, validation, routing,
version negotiation, config, logging) version reporting, config, logging)
| | | |
v v v v
raw module pkg module ... future modules (repo, source, ...) raw module pkg module ... future modules (repo, source, ...)
@@ -25,52 +25,55 @@
(source of truth) (derived cache) (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 — core-to-module and module-to-module — passes through core. No consumer touches dlopen, dlsym, or module discovery itself. 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.
## 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 core version it supports** (a reserved contract symbol). At load, core compares its own version against that minimum: running core is older → refuse with an explicit "core too old for this module" report; otherwise load. Core never rejects a module for being old, because core's contract evolves append-only — a newer core supports everything an older core did. - **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. A module is never rejected for being old, because the libdpm-core contract evolves append-only — a newer libdpm-core supports everything an older one did.
- **Consumers state only minimums**, never maximums: require("raw", min 1.2) means "raw at 1.2 or anything newer." Module APIs evolve append-only (new table versions beside old ones), so newer is always acceptable and an update can never render a consumer's requirement unsatisfiable. - **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.
- **Updates only add satisfiable states**: because bounds are minimums and evolution is append-only, updating core or any module preserves every previously working combination. The single possible load refusal — module requires a newer core — names its own remedy. - **Updates only add satisfiable states**: module APIs evolve append-only (new table versions beside old ones), so a newer module still carries everything an older consumer called, and updating libdpm-core or any module preserves every previously working combination. The single possible load refusal — module requires a newer libdpm-core — names its own remedy.
## libdpm-core.so ## libdpm-core.so
Dependencies: libc, libstdc++, libdl. Never more — core must remain loadable in the barren case forever, so package logic never leaks into it. Core routes and hosts; modules implement. 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.
Core provides: libdpm-core 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. Core is the sole authority on what a valid module is; the contract definition lives inside core as data. There is no SDK package — the interface is specified by this document and enforced by core's validator. - **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.
- **Routing**: - **Routing**:
- generic dispatch — execute a command string with arguments against a named module (what the CLI uses); - 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). - 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 negotiation**: require resolves a module by name, checks the requested minimum, loads, and returns a handle, or reports precisely why it can't. - **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.
## Core C API ## The libdpm-core C API
All functions are extern "C". All returned strings are owned by 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 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.
### Context lifecycle ### Context lifecycle
**dpm_ctx\* dpm_open(const dpm_open_overrides\* overrides)** **dpm_ctx\* dpm_open(const dpm_open_overrides\* overrides)**
Creates a 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 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.
**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.
### Module acquisition ### Module acquisition
**dpm_module\* dpm_require(dpm_ctx\* ctx, const char\* name, const char\* min_version)** **dpm_module\* dpm_require(dpm_ctx\* ctx, const char\* name)**
Resolves the module `name` in the module path, runs the full load-time validation sequence (see Load-time enforcement) if the module is not already loaded in this context, and checks that the module's version is ≥ `min_version` (X.Y.Z comparison; NULL means "any version"). 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, validation step failed (with the step and detail), or version below minimum (with both versions). 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)**
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.
**const void\* dpm_get_api(dpm_ctx\* ctx, dpm_module\* mod, const char\* api_name, int table_version)** **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. 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). Core adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define. 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.
### Enumeration ### Enumeration
@@ -78,7 +81,7 @@ Generic dispatch: invokes the module's dpm_module_execute with the context, `com
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-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-libdpm-core 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.
@@ -86,10 +89,10 @@ 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 core's own version as a static X.Y.Z string. Callable without a context. Returns the libdpm-core 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; "core" names core's own file). 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.
**void dpm_log(dpm_ctx\* ctx, int level, const char\* message)** **void dpm_log(dpm_ctx\* ctx, int level, const char\* message)**
Writes `message` at `level` (FATAL=0, ERROR=1, WARN=2, INFO=3, DEBUG=4) to the context's configured log targets (console and/or file). Messages above the configured level are dropped. NULL message is a no-op. Writes `message` at `level` (FATAL=0, ERROR=1, WARN=2, INFO=3, DEBUG=4) to the context's configured log targets (console and/or file). Messages above the configured level are dropped. NULL message is a no-op.
@@ -102,42 +105,42 @@ 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; core and consumers never free them. 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.
**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 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 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.
**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 and must match the version by which consumers state minimums. 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.
**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 core version (X.Y.Z) this module supports — the oldest core whose contract and services the module was written against. Core refuses to load the module if its own version is lower, and says so. 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.
**const dpm_manifest\* dpm_module_manifest(void)** **const dpm_manifest\* dpm_module_manifest(void)**
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" }). 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. 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. **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.
**Symbol naming**: functional exports are prefixed with the module's name (raw_\*, pkg_\*); the dpm_ prefix is reserved for the contract and core. **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
Core is the sole authority on module validity; the contract above is enforced by core's validator, not by any SDK. Validation is all-or-nothing; a module is registered only after passing every step: 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:
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. **Core-minimum handshake.** dpm_module_core_min() must be ≤ core's version. If core is too old, refuse and say so — the remedy is updating core, and the message names it. Old modules on newer core always pass. 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.
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. Core validates the module's entire declared surface at load, before offering any of it. 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. 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 core handed out a handle, the contract already validated. The residual C-ABI limit — dlsym cannot verify signatures — is covered in practice by the magic, size, core-minimum 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. 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.
## Module: raw — the file-based installer ## Module: raw — the file-based installer
Ships with the base system alongside 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: core + raw function with nothing else present. 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.
- **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** (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.
@@ -145,9 +148,9 @@ Ships with the base system alongside core. Depends on the baseline only; archive
## 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 core, minimum version) and libsqlite3. Ships as a package, installed by raw once sqlite3 is installed. Requires raw (via libdpm-core, 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 core, in-process — shared locking, real error propagation, no output parsing. - 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.
- **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. - 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.
@@ -161,11 +164,12 @@ Ships as a package, installed by raw once sqlite3 is installed. Requires raw (vi
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 — the same library, the same path as everything else:
- open a context (optionally against an alternate root), - open a context (optionally against an alternate root),
- require the layer they need at a minimum version, - require the layer they need,
- read its reported version and decide whether it suits them,
- fetch its API table, - fetch its API table,
- call C functions directly. - call C functions directly.
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. 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.
## Bootstrap chain ## Bootstrap chain
@@ -173,33 +177,33 @@ Core installs its header to the standard include path and its library to the sta
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: core discovers pkg, validates it, full management is live now: libdpm-core 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 core — one repository per module, plus the 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 libdpm-core — one repository per module, plus the libdpm-core repository. Each repo owns its source, build, and tests, and produces exactly one artifact:
- **Core repository**: libdpm-core.so and the dpm CLI. 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 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 core, used for testing and reporting functionality of core. - **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.
- **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 (contract fakes and 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 — minimums only — rather than through lockstep builds. 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.
### Core repository layout ### Layout of the libdpm-core repository
``` ```
include/dpm/ public headers — installed to the system include path; the include/dpm/ public headers — installed to the system include path; the
dpm/ directory is the consumer namespace, so an installed dpm/ directory is the consumer namespace, so an installed
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 core library src/ implementations of the library
src/cli/ the dpm CLI entry point src/cli/ the dpm CLI 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 core API test binary, CLI tests tests/ fixture modules, the API test binary, CLI tests
docs/ project documentation docs/ project documentation
``` ```
@@ -207,7 +211,7 @@ Every header lives under include/: include/dpm/ is the published API surface and
## Artifacts ## Artifacts
Terminology: **DPM Core** names the dpm CLI binary; **libdpm-core** names the library. Terminology: **the dpm binary** names the command-line tool; **libdpm-core** names the library.
| Artifact | Location on system | | Artifact | Location on system |
|---|---| |---|---|
@@ -222,36 +226,36 @@ Development works because the design has no build-time coupling between peers: n
### 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 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 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.
- Peer modules are reached at runtime through core's require/get_api. 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. - 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. 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 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 libdpm-core, 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. 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.
**3. Module-hosting tests — need core only.** A harness links the real libdpm-core, points the module path at the build output plus fixtures, and has core 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 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. 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.
**4. Integration — the only layer that needs everything, and it builds itself.** Real core + 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, 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. **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 + contract tests (instant, zero environment), harness run 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 fakes point the other way — its tests need only fixture package files and a scratch tree.
- Working on **core**: its test fixtures are deliberately broken modules — missing symbols, wrong magic, lying manifests, too-new core-min — plus one known-good stub. Core development never needs any real package module. - 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.
- **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-3 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: 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.
## Development capabilities ## Development capabilities
During development the CLI must be pointable at a local libdpm-core, and that core must be configurable to local paths (module path, config dir, etc.). Two mechanisms provide this: 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:
- **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 core (LD_LIBRARY_PATH pointed at that lib/ directory achieves the same). The system core is never touched. - **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 that core 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 CLI 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
@@ -264,13 +268,13 @@ The config-dir override matters most: once the context reads config from the loc
## Evolution rules ## Evolution rules
- **Append-only ABI**: breaking a module API means exporting a new table (raw_api_v2) beside the old one, never mutating v1. Old tables remain until consumers are gone. - **Append-only ABI**: breaking a module API means exporting a new table (raw_api_v2) beside the old one, never mutating v1. Old tables remain until consumers are gone.
- **Append-only core contract**: newer core loads everything older core did; a module's only version assertion against core is its minimum. - **Append-only libdpm-core contract**: a newer libdpm-core loads everything an older one did; a module's only version assertion against the library is its minimum.
- **Minimums only, everywhere**: modules declare the minimum core they support; consumers declare the minimum module version they need. No maximums, no exact-match constraints an update can never make a previously working combination refuse to load. - **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. No maximums and no exact-match constraints live in the library, so an update can never make a previously working combination refuse to load.
## Invariants ## Invariants
1. Core routes and hosts; modules implement. No package logic in core, ever. 1. libdpm-core 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 core-mediated APIs. 2. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through libdpm-core-mediated APIs.
3. Truth lives in the tree; everything above is regenerable cache or convenience. 3. Truth lives in the tree; everything above is regenerable cache or convenience.
4. Dropping down a layer by hand is always legal; layers above detect it and reconcile. 4. Dropping down a layer by hand is always legal; layers above detect it and reconcile.
5. A module is either fully valid or not loaded — no partial states, no consumer-side defense. 5. A module is either fully valid or not loaded — no partial states, no consumer-side defense.

View File

@@ -1,27 +1,27 @@
# Developing DPM Modules # Developing DPM Modules
A DPM module is one shared object in the module directory. Core loads it, validates it completely, routes CLI 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 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.
## 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. 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.
**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 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 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.
**const char\* dpm_module_version(void)** **const char\* dpm_module_version(void)**
The module's own version as an X.Y.Z string. Consumers state minimum-version requirements against this value. 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.
**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 core version (X.Y.Z) the module supports — the oldest core whose contract and services it was written against. Core refuses to load the module if its own version is lower, and the refusal message says so. 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.
**const dpm_manifest\* dpm_module_manifest(void)** **const dpm_manifest\* dpm_module_manifest(void)**
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. 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 core service declarations all come from the installed public header: 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>
@@ -33,11 +33,11 @@ A module's functions are published to consumers as API tables: one exported symb
All parameters and return values crossing a table are C types only. State passes through opaque handles; errors are int codes. All parameters and return values crossing a table are C types only. State passes through opaque handles; errors are int codes.
**Symbol naming**: every functional export is prefixed with the module's name (mymodule_\*). The dpm_ prefix is reserved for the contract symbols and core. **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.
## Validation at load ## Validation at load
Core is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, core verifies, in order: every reserved contract symbol resolves; the core-minimum 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 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.
## Building ## Building
@@ -67,7 +67,9 @@ 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 core's require/get_api — a peer module is never linked and never needs to be present to build. 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.
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
@@ -77,12 +79,12 @@ Load the freshly built module through a locally run dpm without installing anyth
dpm --module-path <build-dir> mymodule <command> dpm --module-path <build-dir> mymodule <command>
``` ```
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 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.
## Installing ## Installing
Modules install to lib/dpm/modules under the install prefix (/usr/lib/dpm/modules on a distribution install). 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 discovers the module on its next scan; no registration step exists beyond the file being present and valid.
## Reference implementation ## A working example
The info module bundled with the core repository at src/bundled-modules/info/ is a complete working example of the contract, an API table, and this build structure. 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.

View File

@@ -2,87 +2,131 @@
## What DPM is ## What DPM is
DPM is the package manager for Dark Horse Linux. Its core is **libdpm-core**, a shared library that discovers modules, validates them, routes calls to them, negotiates 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**, 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.
The `dpm` command-line tool 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.
Core implements no package operations. It routes and hosts. libdpm-core implements no package operations. It routes and hosts.
## The shape of the system ## The shape of the system
``` ```
dpm CLI build systems / DHL tools / other languages the dpm binary build systems / DHL tools / other languages
\ / \ /
v v v v
libdpm-core.so libdpm-core.so
(discovery, validation, routing, (discovery, validation, routing,
version negotiation, config, logging) version reporting, config, logging)
| |
v v
modules modules
(one .so each) (one .so each)
``` ```
Everything passes through core: CLI-to-module, module-to-module, program-to-module. No consumer calls `dlopen`, `dlsym`, or performs module discovery itself. Everything passes through libdpm-core: binary-to-module, module-to-module, program-to-module. No consumer performs module discovery or dynamic loading itself.
## How it works ## Three layers to read this at
### The context DPM is met at three different layers, and which one you are at determines what any statement below means for you.
All work happens through a context handle obtained from `dpm_open`. Opening a context reads configuration from `/etc/dpm/conf.d/`, resolves the module directory, and initializes logging. Four things can be overridden at open time — the configuration directory, the module path, the target root, and the log level — and every one is exposed as a flag on the `dpm` command, so anything a linked program can redirect, a developer at a shell can redirect too. - **The user layer** — a person at a shell running the dpm binary. Everything here is a command and its flags.
- **The developer layer** — a program written against the C API in `<dpm/core.h>`, linking `-ldpm-core`. Everything here is a function call. Module authors live here too, on the other side of the same boundary.
- **The filesystem layer** — the directories and files DPM reads and writes: configuration, the module directory, the target root, and the installed artifacts.
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 can be open at once. The sections that follow are labelled by layer. Where a mechanism exists at more than one layer, it is described once at each, because the same idea looks different depending on where you stand.
The context owns everything it hands out. Every string a consumer receives stays valid until `dpm_close`, and callers never free anything. ## How it works at the user layer
### Acquiring a module The dpm binary's subcommand surface is exactly the set of loadable modules. `dpm <module> <command> [args...]` loads that module and hands the command to it; `dpm <module> help` asks the module to describe itself. There is no fixed list of operations baked into the tool, because the tool contains no capability logic — it parses arguments and prints.
`dpm_require` resolves a module by name in the module path, validates it completely, and returns a handle — or returns NULL and records exactly why. A minimum version may be stated; NULL accepts any version. Modules load at most once per context, and repeated calls return the same handle. `dpm --list-modules` shows every module that passes validation, with its version and description. Candidates that fail are excluded from the listing and logged, so what is listed is what can actually be run.
Once a module is loaded, there are two ways to reach it: Four flags redirect the system defaults:
- **Generic dispatch** — `dpm_execute` passes a command name and an argument vector to the module's entry point. This is the path the CLI uses, and it is why the CLI's command surface is exactly the set of loadable modules. - `-c, --config-dir PATH` — where configuration is read from
- **Typed access** — `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 use to call functions directly. - `-m, --module-path PATH` — which directory modules are loaded from
- `-r, --root PATH` — the target root that package operations act on
- `-L, --log-level LEVEL` — FATAL, ERROR, WARN, INFO, or DEBUG
### Enumeration 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.
`dpm_list_modules` scans the module path and yields every valid module with its name, version, description, and minimum core version. Candidates that fail validation are logged and excluded, so a listing shows what can actually be used. ## How it works at the developer layer
### The module contract 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 module is one `.so` in the module directory that exports five reserved symbols: a command entry point, its own version, a one-line description, the minimum 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. 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.
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 context owns everything it hands out. Every string a caller receives stays valid until `dpm_close`, and callers never free anything.
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_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.
**Calling into a module.** Two paths:
- `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.
**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.
## How it works at the filesystem layer
**Configuration** lives in `/etc/dpm/conf.d/`. Each `.conf` file in that directory is one namespace named after the file: `core.conf` holds the library's own settings, and a module named `mymodule` reads `mymodule.conf`. Files are sectioned, and a value is addressed by namespace, section, and key. The library's own file carries the log level, whether to write a log file and where, and the default module directory.
**Modules** live in `/usr/lib/dpm/modules/`. A module's name is its filename without the `.so` extension, so `info.so` is the module named `info`. Discovery is the presence of a valid file in that directory — there is no registry, and no registration step.
**Logging** goes to the console always, and to a log file when configuration enables one; the default path is `/var/log/dpm/dpm.log`.
**The target root** defaults to `/`. Pointed elsewhere, it is the tree that package operations act on, which is what turns a scratch directory into a complete target for image assembly or a chroot build.
**Installed artifacts:**
| Artifact | Location |
|---|---|
| the dpm binary | `/usr/bin/dpm` |
| `libdpm-core.so` | `/usr/lib/libdpm-core.so` |
| public header | `/usr/include/dpm/core.h` |
| modules | `/usr/lib/dpm/modules/<name>.so` |
## The module contract
*Developer layer, module-author side.*
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.
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. 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.
### Load-time enforcement ## Load-time enforcement
Core validates a module completely before offering it to anyone, in five steps: libdpm-core validates a module completely before offering it to anyone, in five steps:
1. Every reserved contract symbol resolves. 1. Every reserved contract symbol resolves.
2. The module's minimum core version is not newer than core's own. 2. The module's minimum libdpm-core 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. 4. Every symbol the manifest declares actually resolves.
5. Every declared table carries the correct magic constant and a sane size. 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 core hands out a handle, 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.
### Versioning At the user layer this is visible as a module missing from `--list-modules` with a logged reason, or as an itemized failure when the module is named directly.
Compatibility is directional, and the module is the party that declares it. ## Versioning
Each module states the oldest core it supports. Core compares its own version against that minimum and refuses only when core is older, with a message naming the remedy. Core never rejects a module for being old, because core's contract grows append-only — a newer core supports everything an older core did. 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. A module is never rejected for being old, because the library's contract grows append-only — a newer libdpm-core supports everything an older one did.
Consumers state minimums and never maximums. Module APIs also evolve append-only: a breaking change means exporting a new table beside the old one rather than mutating the existing one. The consequence is that updating core or any module preserves every combination that previously worked, and the single possible load refusal names its own fix. 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.
### Configuration and logging Module APIs evolve append-only: a breaking change means exporting a new table beside the old one rather than mutating the existing one. A newer module therefore still carries everything an older consumer called, so updating libdpm-core or any module preserves every combination that previously worked.
Configuration lives in per-module namespaces: each module's `.conf` file under the context's configuration directory, with `core` naming core's own file. A module reads its settings through the context that dispatched the call, so a module needs no file handling of its own. Logging works the same way — a module writes through the context, and the context decides the destinations and the threshold.
## 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 core from taking on any dependency beyond the baseline, which forbids package logic from living in core, which produces the division the whole architecture rests on: **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 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.
### Because capability has to grow in layers ### Because capability has to grow in layers
@@ -90,47 +134,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 CLI, 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. A program that opens a default context is operating the installed package manager itself — system configuration, system module path, system tree, system locking — identically to invoking `dpm`, because `dpm` is just another caller of the same library. The CLI is argument parsing and printing, and no capability logic lives in it. 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. This is also why the user layer and the developer layer stay in step: the flags are the override fields, one for one, so a developer and a user 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
Core is the sole authority on module validity, and the contract is enforced by core's 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 core can check a module's entire declared surface before offering any of it — an API absent from the manifest does not exist, even if its symbol does. 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.
The residual limit is that `dlsym` cannot verify a function signature. The magic constant, the size field, the core-minimum handshake, and the probes cover this in practice; defeating them takes deliberate lying, which is a package-signing concern upstream of the loader. 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 updates must never strand a working system ### Because updates must never strand a working system
Minimums everywhere, no maximums, and append-only evolution in both directions mean an update can only add satisfiable states. This is what allows modules to be released independently: coordination happens through the versioning model instead of through lockstep builds. libdpm-core holds no maximums and no exact-match constraints, and evolution is append-only in both directions, so an update can only add satisfiable states. This is what allows modules to be released independently: coordination happens through the versioning model instead of through lockstep builds.
### Because modules are developed independently ### Because modules are developed independently
One repository per module, plus the 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 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:
- A module compiles against its own declarations plus core. - A module compiles against its own declarations plus libdpm-core.
- 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. - 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 harness that links real 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 and points the module path at build output plus fixture stubs exercises the real five-step 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.
The core repository's own fixtures are deliberately broken stub modules — missing symbols, bad magic, a lying manifest, a too-new core minimum, a malformed version — plus one known-good stub. Core development never requires a real package module to exist. 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.
## What the core repository produces ## What this repository produces
- **libdpm-core.so** — the library - **libdpm-core.so** — the library
- **dpm** — 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, 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 and lives outside this repository.
## Invariants ## Invariants
1. Core routes and hosts; modules implement. No package logic in core, ever. 1. libdpm-core 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 core-mediated APIs. 2. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through mediated APIs.
3. A module is either fully valid or not loaded — no partial states, no consumer-side defense. 3. A module is either fully valid or not loaded — no partial states, no consumer-side defense.
4. 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 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 core itself - **BUILD.md** — building, testing, and installing libdpm-core and the dpm binary
- **DOCUMENTATION.md** — generating the code reference - **DOCUMENTATION.md** — generating the code reference

View File

@@ -92,7 +92,7 @@ typedef struct dpm_module_info {
const char* name; /* module name (filename minus .so) */ const char* name; /* module name (filename minus .so) */
const char* version; /* module's own X.Y.Z */ const char* version; /* module's own X.Y.Z */
const char* description; /* one-line description */ const char* description; /* one-line description */
const char* core_min; /* minimum core version it supports */ const char* core_min; /* minimum libdpm-core version it supports */
} dpm_module_info; } dpm_module_info;
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -131,7 +131,7 @@ typedef struct dpm_manifest {
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**
* @brief Creates a core context * @brief Creates a libdpm-core context
* *
* Reads configuration from /etc/dpm/conf.d/ (or the overridden config * Reads configuration from /etc/dpm/conf.d/ (or the overridden config
* directory), resolves the module path (override > config > built-in * directory), resolves the module path (override > config > built-in
@@ -148,7 +148,7 @@ typedef struct dpm_manifest {
DPM_API dpm_ctx* dpm_open(const dpm_open_overrides* overrides); DPM_API dpm_ctx* dpm_open(const dpm_open_overrides* overrides);
/** /**
* @brief Releases a core context * @brief Releases a libdpm-core context
* *
* Unloads every module handle the context issued, closes log targets, * Unloads every module handle the context issued, closes log targets,
* and frees all memory owned by the context. All handles and strings * and frees all memory owned by the context. All handles and strings
@@ -170,15 +170,34 @@ DPM_API void dpm_close(dpm_ctx* ctx);
* context. Modules are loaded at most once per context; repeated * context. Modules are loaded at most once per context; repeated
* calls return the same handle. * calls return the same handle.
* *
* @param ctx The core context * Version compatibility is the caller's judgement, not this library's:
* read the loaded module's version with dpm_module_info_of() and
* decide whether it is acceptable.
*
* @param ctx The libdpm-core context
* @param name The module name (its filename minus .so) * @param name The module name (its filename minus .so)
* @param min_version Minimum acceptable module version as X.Y.Z;
* NULL accepts any version
* @return A module handle owned by the context, or NULL on failure * @return A module handle owned by the context, or NULL on failure
* with the precise reason retrievable via dpm_last_error() * with the precise reason retrievable via dpm_last_error()
*/ */
DPM_API dpm_module* dpm_require(dpm_ctx* ctx, const char* name, DPM_API dpm_module* dpm_require(dpm_ctx* ctx, const char* name);
const char* min_version);
/**
* @brief Reports what libdpm-core sees in a loaded module
*
* Fills `out` with the module's name, version, description, and
* minimum-libdpm-core version, exactly as they were read at load. No
* compatibility conclusion is drawn from these values; the caller
* decides whether the version it is looking at is too new or too
* old for its purposes.
*
* @param ctx The libdpm-core context
* @param mod A module handle from dpm_require()
* @param out Receives the module's information; the string pointers
* remain valid until context close
* @return 0 on success, nonzero if the module cannot be reported on
*/
DPM_API int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod,
dpm_module_info* out);
/** /**
* @brief Returns a module's API table for direct typed calls * @brief Returns a module's API table for direct typed calls
@@ -188,7 +207,7 @@ DPM_API dpm_module* dpm_require(dpm_ctx* ctx, const char* name,
* table struct type for that API and version. The table is valid for * table struct type for that API and version. The table is valid for
* the life of the context. * the life of the context.
* *
* @param ctx The 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 api_name The API name as declared in the module's manifest * @param api_name The API name as declared in the module's manifest
* @param table_version The table version to retrieve * @param table_version The table version to retrieve
@@ -205,7 +224,7 @@ 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.
* *
* @param ctx The 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
* module's help command * module's help command
@@ -226,7 +245,7 @@ DPM_API int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command,
* Scans the module path and validates each candidate .so; failures * Scans the module path and validates each candidate .so; failures
* are logged and excluded from the results. * are logged and excluded from the results.
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @return A cursor over all valid modules, or NULL on an unreadable * @return A cursor over all valid modules, or NULL on an unreadable
* module path * module path
*/ */
@@ -236,7 +255,7 @@ DPM_API dpm_cursor* dpm_list_modules(dpm_ctx* ctx);
* @brief Advances an enumeration cursor * @brief Advances an enumeration cursor
* *
* Fills `out` with the next module's name, version, description, and * Fills `out` with the next module's name, version, description, and
* minimum-core version; the string pointers remain valid until * minimum-libdpm-core version; the string pointers remain valid until
* context close. * context close.
* *
* @param cur The cursor from dpm_list_modules() * @param cur The cursor from dpm_list_modules()
@@ -257,10 +276,10 @@ DPM_API void dpm_cursor_free(dpm_cursor* cur);
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**
* @brief Returns core's own version * @brief Returns libdpm-core's own version
* *
* @return Core's version as a static X.Y.Z string; callable without * @return The libdpm-core version as a static X.Y.Z string; callable
* a context * without a context
*/ */
DPM_API const char* dpm_core_version(void); DPM_API const char* dpm_core_version(void);
@@ -268,9 +287,10 @@ DPM_API const char* dpm_core_version(void);
* @brief Returns a configuration value from a module's namespace * @brief Returns a configuration value from a module's namespace
* *
* A module's configuration namespace is its own .conf file under the * A module's configuration namespace is its own .conf file under the
* context's configuration directory; "core" names core's own file. * context's configuration directory; the namespace named "core", from
* core.conf, is the library's own.
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @param module The configuration namespace to read * @param module The configuration namespace to read
* @param section The section name within the file * @param section The section name within the file
* @param key The key within the section * @param key The key within the section
@@ -286,7 +306,7 @@ DPM_API const char* dpm_config_get(dpm_ctx* ctx, const char* module,
* Targets are the console and, when configured, the log file. * Targets are the console and, when configured, the log file.
* Messages above the configured level are dropped. * Messages above the configured level are dropped.
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @param level The severity (DPM_LOG_FATAL through DPM_LOG_DEBUG) * @param level The severity (DPM_LOG_FATAL through DPM_LOG_DEBUG)
* @param message The message to log; NULL is a no-op * @param message The message to log; NULL is a no-op
*/ */
@@ -295,7 +315,7 @@ DPM_API void dpm_log(dpm_ctx* ctx, int level, const char* message);
/** /**
* @brief Returns the resolved module directory path * @brief Returns the resolved module directory path
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @return The module directory path this context resolved * @return The module directory path this context resolved
*/ */
DPM_API const char* dpm_module_path(dpm_ctx* ctx); DPM_API const char* dpm_module_path(dpm_ctx* ctx);
@@ -303,14 +323,14 @@ DPM_API const char* dpm_module_path(dpm_ctx* ctx);
/** /**
* @brief Returns the most recent failure recorded on the context * @brief Returns the most recent failure recorded on the context
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @return A human-readable description of the most recent failure, or * @return A human-readable description of the most recent failure, or
* NULL if none; overwritten by the next failing call * NULL if none; overwritten by the next failing call
*/ */
DPM_API const char* dpm_last_error(dpm_ctx* ctx); DPM_API const char* dpm_last_error(dpm_ctx* ctx);
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Module contract (implemented by modules, called by core) */ /* Module contract (implemented by modules, called by libdpm-core) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* /*
@@ -323,7 +343,8 @@ DPM_API const char* dpm_last_error(dpm_ctx* ctx);
* const char* dpm_module_core_min(void); * const char* dpm_module_core_min(void);
* const dpm_manifest* dpm_module_manifest(void); * const dpm_manifest* dpm_module_manifest(void);
* *
* plus one exported table symbol per manifest entry. Core refuses to * plus one exported table symbol per manifest entry. libdpm-core
* refuses to
* load any module that does not validate completely (see the DPM * load any module that does not validate completely (see the DPM
* specification: load-time enforcement). * specification: load-time enforcement).
*/ */

View File

@@ -1,6 +1,6 @@
/** /**
* @file context.hpp * @file context.hpp
* @brief The core context: configuration, logging, module registry * @brief The libdpm-core context: configuration, logging, module registry
* *
* @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,7 +30,7 @@
#include "internal/modules.hpp" #include "internal/modules.hpp"
/** @brief A core context: configuration, logging, and the module registry */ /** @brief A libdpm-core context: configuration, logging, module registry */
struct dpm_ctx { struct dpm_ctx {
std::string config_dir; std::string config_dir;
std::string module_path; std::string module_path;
@@ -56,7 +56,7 @@ namespace dpmcore {
/** /**
* @brief Records a failure reason on the context * @brief Records a failure reason on the context
* *
* @param ctx The core context; NULL is a no-op * @param ctx The libdpm-core context; NULL is a no-op
* @param msg The failure description * @param msg The failure description
*/ */
void set_error(dpm_ctx* ctx, const std::string& msg); void set_error(dpm_ctx* ctx, const std::string& msg);
@@ -67,7 +67,7 @@ void set_error(dpm_ctx* ctx, const std::string& msg);
* Parses every .conf file in the context's configuration directory * Parses every .conf file in the context's configuration directory
* into the context's configuration store. * into the context's configuration store.
* *
* @param ctx The core context * @param ctx The libdpm-core context
*/ */
void load_config_dir(dpm_ctx* ctx); void load_config_dir(dpm_ctx* ctx);

View File

@@ -62,7 +62,7 @@ namespace dpmcore {
* Loads the named module's .so from the context's module path and * Loads the named module's .so from the context's module path and
* verifies the complete contract. * verifies the complete contract.
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @param name The module name * @param name The module name
* @param reason Receives the refusal reason on failure * @param reason Receives the refusal reason on failure
* @return The validated module (caller owns), or nullptr on failure * @return The validated module (caller owns), or nullptr on failure

View File

@@ -31,7 +31,7 @@
enum Command { enum Command {
CMD_UNKNOWN, /**< Unknown or unsupported command */ CMD_UNKNOWN, /**< Unknown or unsupported command */
CMD_HELP, /**< Display help information */ CMD_HELP, /**< Display help information */
CMD_VERSION, /**< Display core and module versions */ CMD_VERSION, /**< Display libdpm-core and module versions */
CMD_SYSTEM, /**< Display system information */ CMD_SYSTEM, /**< Display system information */
CMD_CONFIG, /**< Display configuration information */ CMD_CONFIG, /**< Display configuration information */
}; };
@@ -47,15 +47,15 @@ Command parse_command(const char* cmd_str);
/** /**
* @brief Displays the info module's help text * @brief Displays the info module's help text
* *
* @param ctx Host core context * @param ctx Host libdpm-core context
* @return 0 on success * @return 0 on success
*/ */
int cmd_help(dpm_ctx* ctx); int cmd_help(dpm_ctx* ctx);
/** /**
* @brief Reports the running core version and the info module version * @brief Reports the running libdpm-core version and the module version
* *
* @param ctx Host core context * @param ctx Host libdpm-core context
* @return 0 on success * @return 0 on success
*/ */
int cmd_version(dpm_ctx* ctx); int cmd_version(dpm_ctx* ctx);
@@ -63,15 +63,15 @@ int cmd_version(dpm_ctx* ctx);
/** /**
* @brief Reports operating system and architecture information * @brief Reports operating system and architecture information
* *
* @param ctx Host core context * @param ctx Host libdpm-core context
* @return 0 on success * @return 0 on success
*/ */
int cmd_system(dpm_ctx* ctx); int cmd_system(dpm_ctx* ctx);
/** /**
* @brief Reports core configuration as resolved by the running context * @brief Reports configuration as resolved by the running context
* *
* @param ctx Host core context * @param ctx Host libdpm-core context
* @return 0 on success * @return 0 on success
*/ */
int cmd_config(dpm_ctx* ctx); int cmd_config(dpm_ctx* ctx);
@@ -79,7 +79,7 @@ int cmd_config(dpm_ctx* ctx);
/** /**
* @brief Reports an unrecognized command * @brief Reports an unrecognized command
* *
* @param ctx Host core context * @param ctx Host libdpm-core context
* @param command The unrecognized command string * @param command The unrecognized command string
* @return 1 to indicate failure * @return 1 to indicate failure
*/ */

View File

@@ -2,8 +2,9 @@
* @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 core; used for testing and reporting functionality of * Bundles with the dpm binary and libdpm-core; used for testing and
* core. Implements the full DPM module contract: the five reserved * reporting functionality of libdpm-core. Implements the full DPM
* module contract: the five reserved
* symbols plus a manifest-declared API table. * symbols plus a manifest-declared API table.
* *
* @copyright Copyright (c) 2026 SILO GROUP LLC * @copyright Copyright (c) 2026 SILO GROUP LLC
@@ -36,9 +37,9 @@
/** /**
* @brief Version 1 API table for the info module * @brief Version 1 API table for the info module
* *
* Lets consumers exercise core's typed-access path (require → get_api * Lets consumers exercise the typed-access path (require → get_api
* → call) end to end, which is part of this module's purpose of * → call) end to end, which is part of this module's purpose of
* testing core functionality. * testing libdpm-core functionality.
*/ */
struct info_api_v1_s { struct info_api_v1_s {
dpm_api_table_header hdr; dpm_api_table_header hdr;
@@ -53,7 +54,7 @@ static const char* api_version(void)
static const char* api_description(void) static const char* api_description(void)
{ {
return "Reports and tests DPM core functionality."; return "Reports and tests libdpm-core functionality.";
} }
extern "C" { extern "C" {
@@ -82,11 +83,11 @@ extern "C" const char* dpm_module_version(void)
*/ */
extern "C" const char* dpm_module_description(void) extern "C" const char* dpm_module_description(void)
{ {
return "Reports and tests DPM core functionality."; return "Reports and tests libdpm-core functionality.";
} }
/** /**
* @brief Returns the minimum core version this module supports * @brief Returns the minimum libdpm-core version this module supports
*/ */
extern "C" const char* dpm_module_core_min(void) extern "C" const char* dpm_module_core_min(void)
{ {
@@ -111,7 +112,7 @@ extern "C" const dpm_manifest* dpm_module_manifest(void)
* 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.
* *
* @param ctx Host core context (reaches core services) * @param ctx Host libdpm-core context (reaches its services)
* @param command The command string to execute * @param command The command string to execute
* @param argc Number of arguments * @param argc Number of arguments
* @param argv Array of argument strings (argv[0] is the command) * @param argv Array of argument strings (argv[0] is the command)

View File

@@ -2,7 +2,7 @@
* @file commands.cpp * @file commands.cpp
* @brief Implementation of the info module command handlers * @brief Implementation of the info module command handlers
* *
* Reports on, and thereby exercises, core functionality: version, * Reports on, and thereby exercises, libdpm-core functionality: version,
* system details, and configuration as resolved by the host context. * system details, and configuration as resolved by the host context.
* *
* @copyright Copyright (c) 2026 SILO GROUP LLC * @copyright Copyright (c) 2026 SILO GROUP LLC
@@ -118,13 +118,13 @@ Command parse_command(const char* cmd_str)
int cmd_help(dpm_ctx* ctx) int cmd_help(dpm_ctx* ctx)
{ {
dpm_log(ctx, DPM_LOG_INFO, "DPM Info Module - Reports and tests DPM core functionality."); dpm_log(ctx, DPM_LOG_INFO, "DPM Info Module - Reports and tests libdpm-core functionality.");
dpm_log(ctx, DPM_LOG_INFO, ""); dpm_log(ctx, DPM_LOG_INFO, "");
dpm_log(ctx, DPM_LOG_INFO, "Available commands:"); dpm_log(ctx, DPM_LOG_INFO, "Available commands:");
dpm_log(ctx, DPM_LOG_INFO, ""); dpm_log(ctx, DPM_LOG_INFO, "");
dpm_log(ctx, DPM_LOG_INFO, " version - Display core and module version information"); dpm_log(ctx, DPM_LOG_INFO, " version - Display libdpm-core and module version information");
dpm_log(ctx, DPM_LOG_INFO, " system - Display system information"); dpm_log(ctx, DPM_LOG_INFO, " system - Display system information");
dpm_log(ctx, DPM_LOG_INFO, " config - Display configuration as resolved by core"); dpm_log(ctx, DPM_LOG_INFO, " config - Display configuration as resolved by libdpm-core");
dpm_log(ctx, DPM_LOG_INFO, " help - Display this help message"); dpm_log(ctx, DPM_LOG_INFO, " help - Display this help message");
dpm_log(ctx, DPM_LOG_INFO, ""); dpm_log(ctx, DPM_LOG_INFO, "");
return 0; return 0;

View File

@@ -103,7 +103,7 @@ bool option_matches(const char* arg, const char* short_form,
/** /**
* @brief Prints the table of available modules * @brief Prints the table of available modules
* *
* @param ctx The core context * @param ctx The libdpm-core context
* @return 0 on success, 1 on failure * @return 0 on success, 1 on failure
*/ */
int list_modules(dpm_ctx* ctx) int list_modules(dpm_ctx* ctx)
@@ -248,7 +248,7 @@ int main(int argc, char** argv)
char** module_argv = &argv[i + 1]; char** module_argv = &argv[i + 1];
int module_argc = argc - i - 1; int module_argc = argc - i - 1;
dpm_module* mod = dpm_require(ctx, module_name, nullptr); dpm_module* mod = dpm_require(ctx, module_name);
if (!mod) { if (!mod) {
const char* err = dpm_last_error(ctx); const char* err = dpm_last_error(ctx);
std::fprintf(stderr, "dpm: %s\n", std::fprintf(stderr, "dpm: %s\n",

View File

@@ -3,6 +3,7 @@ DPM_CORE_1.0 {
dpm_open; dpm_open;
dpm_close; dpm_close;
dpm_require; dpm_require;
dpm_module_info_of;
dpm_get_api; dpm_get_api;
dpm_execute; dpm_execute;
dpm_list_modules; dpm_list_modules;

View File

@@ -2,7 +2,7 @@
* @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: core is the sole * Implements the load-time enforcement sequence: libdpm-core is the sole
* authority on module validity. A module is either fully valid or not * authority on module validity. A module is either fully valid or not
* loaded — no partial states. * loaded — no partial states.
* *
@@ -119,7 +119,7 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
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")); auto manifest_f = reinterpret_cast<manifest_fn>(resolve(handle, "dpm_module_manifest"));
/* Step 2: core-minimum handshake. */ /* Step 2: minimum-version handshake. */
const char* core_min = core_min_f(); const char* core_min = core_min_f();
long parsed[3]; long parsed[3];
if (!core_min || !parse_version(core_min, parsed)) { if (!core_min || !parse_version(core_min, parsed)) {
@@ -128,8 +128,9 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
return nullptr; return nullptr;
} }
if (compare_versions(core_min, DPM_CORE_VERSION_STR) > 0) { if (compare_versions(core_min, DPM_CORE_VERSION_STR) > 0) {
reason = std::string("requires core >= ") + core_min + reason = std::string("requires libdpm-core >= ") + core_min +
", running core is " DPM_CORE_VERSION_STR " — update core"; ", running libdpm-core is " DPM_CORE_VERSION_STR
" — update libdpm-core";
dlclose(handle); dlclose(handle);
return nullptr; return nullptr;
} }
@@ -212,25 +213,17 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
extern "C" { extern "C" {
dpm_module* dpm_require(dpm_ctx* ctx, const char* name, dpm_module* dpm_require(dpm_ctx* ctx, const char* name)
const char* min_version)
{ {
if (!ctx || !name || !*name) { if (!ctx || !name || !*name) {
return nullptr; return nullptr;
} }
long parsed[3];
if (min_version && !dpmcore::parse_version(min_version, parsed)) {
dpmcore::set_error(ctx, std::string("malformed minimum version: ") +
min_version);
return nullptr;
}
dpm_module* mod = nullptr;
auto it = ctx->modules.find(name); auto it = ctx->modules.find(name);
if (it != ctx->modules.end()) { if (it != ctx->modules.end()) {
mod = it->second.get(); return it->second.get();
} else { }
std::string reason; std::string reason;
auto loaded = dpmcore::validate_and_load(ctx, name, reason); auto loaded = dpmcore::validate_and_load(ctx, name, reason);
if (!loaded) { if (!loaded) {
@@ -238,21 +231,25 @@ dpm_module* dpm_require(dpm_ctx* ctx, const char* name,
"': " + reason); "': " + reason);
return nullptr; return nullptr;
} }
mod = loaded.get();
dpm_module* mod = loaded.get();
ctx->modules[name] = std::move(loaded); ctx->modules[name] = std::move(loaded);
}
if (min_version &&
dpmcore::compare_versions(mod->version.c_str(), min_version) < 0) {
dpmcore::set_error(ctx, std::string("module '") + name +
"' is version " + mod->version +
", below required minimum " + min_version);
return nullptr;
}
return mod; return mod;
} }
int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)
{
if (!ctx || !mod || !out) {
return 1;
}
out->name = mod->name.c_str();
out->version = mod->version.c_str();
out->description = mod->description.c_str();
out->core_min = mod->core_min.c_str();
return 0;
}
const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod, const void* dpm_get_api(dpm_ctx* ctx, dpm_module* mod,
const char* api_name, int table_version) const char* api_name, int table_version)
{ {

View File

@@ -3,7 +3,7 @@
* @brief Broken fixture: API table with a wrong magic constant * @brief Broken fixture: API table with a wrong magic constant
* *
* Contract-complete, but its declared table opens with garbage instead * Contract-complete, but its declared table opens with garbage instead
* of the spec magic. Core must refuse it at validation step 5. * of the spec magic. libdpm-core must refuse it at validation step 5.
* *
* Part of the Dark Horse Linux Package Manager (DPM) * Part of the Dark Horse Linux Package Manager (DPM)
* *

View File

@@ -3,7 +3,7 @@
* @brief Broken fixture: malformed module version string * @brief Broken fixture: malformed module version string
* *
* Contract-complete, but dpm_module_version() returns something that * Contract-complete, but dpm_module_version() returns something that
* is not X.Y.Z. Core must refuse it at validation step 3. * is not X.Y.Z. libdpm-core must refuse it at validation step 3.
* *
* Part of the Dark Horse Linux Package Manager (DPM) * Part of the Dark Horse Linux Package Manager (DPM)
* *

View File

@@ -1,10 +1,10 @@
/** /**
* @file core_too_new.cpp * @file core_too_new.cpp
* @brief Broken fixture: demands a core newer than any that exists * @brief Broken fixture: demands a libdpm-core newer than any that exists
* *
* Contract-complete, but dpm_module_core_min() reports 99.0.0. Core * Contract-complete, but dpm_module_core_min() reports 99.0.0.
* must refuse it at validation step 2 with a message naming the * libdpm-core must refuse it at validation step 2 with a message
* remedy (update core). * naming the remedy (update libdpm-core).
* *
* Part of the Dark Horse Linux Package Manager (DPM) * Part of the Dark Horse Linux Package Manager (DPM)
* *
@@ -45,7 +45,7 @@ extern "C" const char* dpm_module_version(void)
extern "C" const char* dpm_module_description(void) extern "C" const char* dpm_module_description(void)
{ {
return "Fixture demanding a future core."; return "Fixture demanding a future libdpm-core.";
} }
extern "C" const char* dpm_module_core_min(void) extern "C" const char* dpm_module_core_min(void)

View File

@@ -3,8 +3,8 @@
* @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 core headers — exactly as a * contract with its own declarations — no libdpm-core headers — exactly
* standalone module author would. Used to validate core's happy path. * 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)
* *

View File

@@ -3,7 +3,7 @@
* @brief Broken fixture: manifest declares an API it doesn't export * @brief Broken fixture: manifest declares an API it doesn't export
* *
* Contract-complete, but its manifest names a table symbol that does * Contract-complete, but its manifest names a table symbol that does
* not exist in the .so. Core must refuse it at validation step 4. * not exist in the .so. libdpm-core must refuse it at validation step 4.
* *
* Part of the Dark Horse Linux Package Manager (DPM) * Part of the Dark Horse Linux Package Manager (DPM)
* *

View File

@@ -3,7 +3,7 @@
* @brief Broken fixture: exports only part of the module contract * @brief Broken fixture: exports only part of the module contract
* *
* Missing dpm_module_execute, dpm_module_core_min, and * Missing dpm_module_execute, dpm_module_core_min, and
* dpm_module_manifest. Core must refuse it at validation step 1 and * dpm_module_manifest. libdpm-core must refuse it at step 1 and
* name the missing symbols. * name the missing symbols.
* *
* Part of the Dark Horse Linux Package Manager (DPM) * Part of the Dark Horse Linux Package Manager (DPM)

View File

@@ -1,6 +1,6 @@
/** /**
* @file test_core.cpp * @file test_core.cpp
* @brief Core 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, versioned require,
@@ -105,41 +105,44 @@ int main(void)
/* ---- validation matrix: every broken fixture refused, precisely ---- */ /* ---- validation matrix: every broken fixture refused, precisely ---- */
{ {
CHECK(dpm_require(ctx, "missing_symbols", nullptr) == nullptr); CHECK(dpm_require(ctx, "missing_symbols") == nullptr);
CHECK(error_contains(ctx, "missing required contract symbols")); CHECK(error_contains(ctx, "missing required contract symbols"));
CHECK(error_contains(ctx, "dpm_module_execute")); CHECK(error_contains(ctx, "dpm_module_execute"));
CHECK(dpm_require(ctx, "core_too_new", nullptr) == nullptr); CHECK(dpm_require(ctx, "core_too_new") == nullptr);
CHECK(error_contains(ctx, "update core")); CHECK(error_contains(ctx, "update libdpm-core"));
CHECK(dpm_require(ctx, "bad_version", nullptr) == 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) == nullptr); CHECK(dpm_require(ctx, "lying_manifest") == nullptr);
CHECK(error_contains(ctx, "does not resolve")); CHECK(error_contains(ctx, "does not resolve"));
CHECK(dpm_require(ctx, "bad_magic", nullptr) == nullptr); CHECK(dpm_require(ctx, "bad_magic") == nullptr);
CHECK(error_contains(ctx, "magic")); CHECK(error_contains(ctx, "magic"));
CHECK(dpm_require(ctx, "nonexistent", nullptr) == nullptr); CHECK(dpm_require(ctx, "nonexistent") == nullptr);
CHECK(error_contains(ctx, "not found")); CHECK(error_contains(ctx, "not found"));
} }
/* ---- known-good module: require, versions, api, execute ---- */ /* ---- known-good module: require, versions, api, execute ---- */
{ {
dpm_module* good = dpm_require(ctx, "good", nullptr); dpm_module* good = dpm_require(ctx, "good");
CHECK(good != nullptr); CHECK(good != nullptr);
/* loaded at most once per context */ /* loaded at most once per context */
CHECK(dpm_require(ctx, "good", nullptr) == good); CHECK(dpm_require(ctx, "good") == good);
/* minimum-version negotiation */ /* libdpm-core reports what it saw; the caller judges compatibility */
CHECK(dpm_require(ctx, "good", "1.0.0") == good); dpm_module_info seen;
CHECK(dpm_require(ctx, "good", "1.2.3") == good); CHECK(dpm_module_info_of(ctx, good, &seen) == 0);
CHECK(dpm_require(ctx, "good", "2.0.0") == nullptr); CHECK(std::strcmp(seen.name, "good") == 0);
CHECK(error_contains(ctx, "below required minimum")); CHECK(std::strcmp(seen.version, "1.2.3") == 0);
CHECK(dpm_require(ctx, "good", "not.a.version") == nullptr); CHECK(std::strcmp(seen.core_min, "0.1.0") == 0);
CHECK(error_contains(ctx, "malformed minimum version")); CHECK(seen.description != nullptr && *seen.description);
CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0);
CHECK(dpm_module_info_of(ctx, good, nullptr) != 0);
/* typed access */ /* typed access */
const void* table = dpm_get_api(ctx, good, "good", 1); const void* table = dpm_get_api(ctx, good, "good", 1);