Modules determine their own compatibility with the library

A module is built against the system-installed libdpm-core.so and is
responsible for being correct against it. Where it needs to act on the
version it is running under, dpm_core_version() reports that and the
module decides for itself.

dpm_module_core_min() is removed. It was a declaration handed to the
library to enforce on the module's behalf, and enforcement of that kind
belongs nowhere in a library that routes and hosts. The contract is now
three reserved symbols and load validation is two steps: the reserved
symbols resolve, and the version and description probes return
well-formed values.

compare_versions had no remaining caller and is removed; parse_version
stays for the well-formedness probe. The core_too_new fixture went with
the handshake it existed to exercise.
This commit is contained in:
2026-08-15 04:21:58 -04:00
parent 97b39cac6c
commit 17acad2b02
17 changed files with 101 additions and 185 deletions

View File

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

View File

@@ -58,7 +58,7 @@ libdpm-core.so validates the module completely at load; a handle is returned onl
```
dpm_module_info info;
dpm_module_info_of(ctx, mod, &info); /* info.name, .version, .description, .core_min */
dpm_module_info_of(ctx, mod, &info); /* info.name, .version, .description */
```
Deciding whether that version is suitable is yours. libdpm-core.so applies no version criterion of its own — a handle means the module is valid, not that it suits you.
@@ -77,7 +77,7 @@ This is the only path into module code, and it is the same path the `dpm` binary
dpm_cursor* cur = dpm_list_modules(ctx);
dpm_module_info info;
while (dpm_cursor_next(cur, &info) == 0) {
/* info.name, info.version, info.description, info.core_min */
/* info.name, info.version, info.description */
}
dpm_cursor_free(cur);
```
@@ -87,7 +87,7 @@ The cursor covers every valid module in the module path; invalid candidates are
## Services
- **`dpm_core_version`()** — the library version; callable without a context.
- **`dpm_module_info_of`(ctx, mod, out)** — the name, version, description, and minimum-library version read from a loaded module.
- **`dpm_module_info_of`(ctx, mod, out)** — the name, version, and description read from a loaded module.
- **`dpm_config_get`(ctx, module, section, key)** — a value from a module's configuration namespace, or NULL if unset.
- **`dpm_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
- **`dpm_module_path`(ctx)** — the resolved module directory.

View File

@@ -28,10 +28,10 @@ Modules are loaded with `RTLD_LOCAL`, so a module's symbols never enter the glob
## Versioning model
Compatibility is directional, and the module is the one that declares it:
Every version question is settled by the party that has to live with the answer. libdpm-core.so reports; it rules on nothing.
- **Each module reports the minimum library version it supports** (a reserved contract symbol). At load, the running library compares its own version against that minimum: if it is older → refuse with an explicit "library too old for this module" report; otherwise load.
- **Consuming modules judge module versions; libdpm-core.so does not.** The library reports the version it saw at load and draws no conclusion from it. A module that depends on another requires it by name, reads the reported version, and decides for itself whether that version is suitable for the commands it intends to issue. A load is a statement that the module is valid, never that it is compatible with a particular caller.
- **A module determines its own compatibility with the library.** It is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. `dpm_core_version()` reports the running version to any module that needs to act on it, and that module proceeds or fails on its own judgement.
- **Consuming modules judge module versions.** A module that depends on another requires it by name, reads the reported version, and decides for itself whether that version is suitable for the commands it intends to issue. A load is a statement that the module is valid, never that it is compatible with a particular caller.
## libdpm-core.so
@@ -63,7 +63,7 @@ Releases the context: unloads every module handle it issued, closes log targets,
Resolves the module `name` in the module path and runs the full load-time validation sequence (see Load-time enforcement) if the module is not already loaded in this context. On success returns a module handle owned by the context (repeated calls return the same handle — modules are loaded at most once per context). On failure returns NULL and records the precise reason: not found, or validation step failed with the step and detail. No version criterion is applied here; compatibility is the caller's to determine from the reported version.
**`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-library version exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. The library attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on.
Fills `out` with the loaded module's name, version, and description exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. The library attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on.
**`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv)`**
Dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). The library adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define.
@@ -76,7 +76,7 @@ This is the entire path into module code. A caller addresses a module by name an
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)`**
Advances the cursor. Fills `out` with the next module's name, version, description, and minimum-library version (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end.
Advances the cursor. Fills `out` with the next module's name, version, and description (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end.
**`void dpm_cursor_free(dpm_cursor* cur)`**
Releases the cursor. NULL is a no-op.
@@ -111,8 +111,7 @@ Returns the module's own version as an X.Y.Z string. Must be constant for the li
**`const char* dpm_module_description(void)`**
Returns a one-line human-readable description, used in module listings.
**`const char* dpm_module_core_min(void)`**
Returns the minimum library version (X.Y.Z) this module supports — the oldest one whose contract and services the module was written against. A library older than that refuses to load the module, and says so.
**Version compatibility with the library is the module's own to determine.** A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where it needs to act on what it is running under, `dpm_core_version()` reports the running version and the module decides what to do with that.
**Symbol naming**: functional exports are prefixed with the module's name (raw_\*, pkg_\*); the dpm_ prefix is reserved for the contract and for libdpm-core.so.
@@ -123,8 +122,7 @@ Returns the minimum library version (X.Y.Z) this module supports — the oldest
libdpm-core.so is the sole authority on module validity; the contract above is enforced by its validator, not by any SDK. Validation is all-or-nothing; a module is registered only after passing every step:
1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`.
2. **Minimum-version handshake.** `dpm_module_core_min`() must be ≤ the running library version. If the library is too old, refuse and say so — the remedy is updating the library, and the message names it.
3. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse.
2. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse.
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.
@@ -232,7 +230,7 @@ Peers are reached at runtime by name and command string. Building pkg does not r
- Working on **pkg**: edit, run unit tests (instant, zero environment), harness run with a raw stub before merge. A real raw is never needed, or even possessed, until integration.
- Working on **raw**: same, except its tests need only fixture package files and a scratch tree.
- Working on **libdpm-core.so**: its test fixtures are deliberately broken modules — missing symbols, a too-new minimum-version declaration, a malformed version — plus one known-good stub. Development never needs any real package module.
- Working on **libdpm-core.so**: its test fixtures are deliberately broken modules — missing symbols, a malformed version — plus one known-good stub. Development never needs any real package module.
- **Debugging** is the layer-2 harness under a debugger — it is the "run the module without the system" mechanism, so no separate standalone build exists or is maintained.
The discipline that keeps this honest: stubs are written to the command vocabulary the real peer documents, and layer-2 validation plus the layer-3 bootstrap run in CI, so a stub that drifts from reality is caught by the first integration pass rather than shipped.
@@ -256,7 +254,7 @@ The config-dir override matters most: once the context reads config from the loc
- **A module's interface is its command vocabulary.** Retiring or changing the meaning of a command is a version change in the module that owns it, judged by every consumer that dispatches to it.
- **Breaking the library's exported ABI means a new symbol version generation**: the export set is what consumers link against, so a break is a deliberate, versioned event rather than an incidental one.
- **Compatibility is decided by the consumer**: modules declare the minimum library version they support, and libdpm-core.so enforces that one handshake because it is the host. Everything else is reported, not enforced — the library hands a consumer the version it saw and the consumer decides whether to proceed.
- **Compatibility is decided by the party that has to live with it**: a module determines whether it works with the library it is running against, and a consuming module determines whether a peer's version suits it. libdpm-core.so reports the versions it saw and enforces nothing beyond validity.
## Invariants

View File

@@ -15,15 +15,22 @@ The module's own version as an X.Y.Z string. libdpm-core.so reports this value t
**`const char* dpm_module_description(void)`**
A one-line human-readable description, shown in module listings.
**`const char* dpm_module_core_min(void)`**
The minimum library version (X.Y.Z) the module supports — the oldest one whose contract and services it was written against. A libdpm-core.so older than that refuses to load the module, and the refusal message says so.
The `dpm_ctx` type and the service declarations all come from the installed public header:
```
#include <dpm/core.h>
```
## You determine your own compatibility with the library
Your module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where you need to act on what you are running under, `dpm_core_version()` reports the running version and you decide what to do:
```
const char* running = dpm_core_version();
```
Check it, proceed or fail on your own terms, and report through `dpm_log` and your return code.
## Your interface is your command vocabulary
A module publishes no headers, no struct layouts, and no symbols to anything that calls it. Everything it offers is reached through `dpm_module_execute`, addressed by command string, with arguments passed as an argument vector and a status returned as an int.
@@ -57,7 +64,7 @@ A module that depends on a peer is the party that judges the peer's version. Req
## Validation at load
libdpm-core.so is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, it verifies, in order: every reserved contract symbol resolves; the minimum-version handshake passes; and the version and description probes return well-formed values. A module failing any step is refused with an itemized reason, visible in the load-failure output. A module that loads is fully valid — consumers never defend against partial states.
libdpm-core.so is the sole authority on module validity, and validation is all-or-nothing. Before a module is offered to anyone, it verifies that every reserved contract symbol resolves and that the version and description probes return well-formed values. A module failing either 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
@@ -99,7 +106,7 @@ dpm --module-path <build-dir> mymodule <command>
libdpm-core.so runs the full validation sequence on every load, so a contract mistake surfaces here, immediately and itemized, rather than after installation. The `--config-dir` flag points the module's configuration namespace at local files during development, and `--root` directs package operations at a scratch tree.
Where your module calls a peer, put a **stub module** in the fixture module path: a small .so exporting the four reserved symbols and answering the commands your module issues. libdpm-core.so validates and dispatches to it exactly as it would the real peer. Because a peer is addressed only by name and command string, the stub is a complete substitute — there is nothing else about the real peer your module could have depended on.
Where your module calls a peer, put a **stub module** in the fixture module path: a small .so exporting the three reserved symbols and answering the commands your module issues. libdpm-core.so validates and dispatches to it exactly as it would the real peer. Because a peer is addressed only by name and command string, the stub is a complete substitute — there is nothing else about the real peer your module could have depended on.
## Installing

View File

@@ -53,7 +53,7 @@ The context owns everything it hands out. Every string a caller receives stays v
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_last_error` carrying the precise reason. Modules load at most once per context, and repeated calls return the same handle.
**Reading what the library saw.** `dpm_module_info_of` fills in a module's name, version, description, and minimum-library version. Those values are reported, and no conclusion is drawn from them.
**Reading what the library saw.** `dpm_module_info_of` fills in a module's name, version, and description. Those values are reported, and no conclusion is drawn from them.
**Calling into a module.** `dpm_execute` passes a command name and an argument vector to the module's entry point and returns its result. This is the only path into module code, and it is the same one the `dpm` binary takes.
@@ -104,17 +104,18 @@ The `ctx` a module needs is the one handed to it in its own entry point, so it r
*What a module author implements.*
A module is one `.so` in the module directory exporting four reserved symbols: a command entry point, its own version, a one-line description, and the minimum library version it supports. It includes `<dpm/core.h>` for those declarations and links `-ldpm-core`, and that is its entire build dependency.
A module is one `.so` in the module directory exporting three reserved symbols: a command entry point, its own version, and a one-line description. It includes `<dpm/core.h>` for those declarations and links `-ldpm-core`, and that is its entire build dependency.
The entry point receives the context that dispatched the call, the command name, and an argument vector, and returns an int. Everything a module offers the rest of the system is reachable through that one function, addressed by command name — which is what keeps a caller free of any compile-time knowledge of the module it is calling.
A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where it needs to know what it is running on, `dpm_core_version()` reports the running version and the module acts on that itself.
## Load-time enforcement
libdpm-core.so validates a module completely before offering it to anyone:
1. Every reserved contract symbol resolves.
2. The module's minimum library version is not newer than the running library's own.
3. The version and description probes return well-formed values.
2. The version and description probes return well-formed values.
A module failing any step is refused with an itemized reason and its library handle closed. Validation is all-or-nothing: if a handle comes back, the contract already passed. Consumers never defend against partially valid modules, because they cannot receive one.
@@ -122,9 +123,11 @@ From the command line this shows up as a module missing from `--list-modules` wi
## Versioning
libdpm-core.so enforces exactly one version rule, the one where it is the host: each module states the oldest library version it supports, and a library older than that refuses to load the module, with a message naming the remedy.
Every version question belongs to the party that has to live with the answer.
Every other version question belongs to the consumer. A module that depends on another requires it, reads the version reported back, and decides for itself whether that version is suitable for the commands it intends to issue. libdpm-core.so reports what it saw and draws no conclusion from it, so a handle means the module is valid rather than that it suits any particular caller.
A module determines its own compatibility with the library it is running against. `dpm_core_version()` reports the running version, and the module proceeds or fails on its own judgement. A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it.
A module that depends on another module requires it, reads the version reported back, and decides for itself whether that version is suitable for the commands it intends to issue. libdpm-core.so reports what it saw and draws no conclusion from it, so a handle means the module is valid rather than that it suits any particular caller.
## Why it works this way
@@ -157,7 +160,7 @@ One repository per module, plus the libdpm-core.so repository. A module links li
- A harness that links the real libdpm-core.so and points the module path at build output plus fixture stubs exercises the real validation on a bare builder.
- Only final integration needs the whole system, and because alternate roots are first-class, it needs a directory rather than a virtual machine.
This repository's own fixtures are deliberately broken stub modules — missing symbols, a too-new minimum version, a malformed version — plus one known-good stub. Developing libdpm-core.so never requires a real package module to exist.
This repository's own fixtures are deliberately broken stub modules — missing symbols, a malformed version — plus one known-good stub. Developing libdpm-core.so never requires a real package module to exist.
## What this repository produces

View File

@@ -92,7 +92,6 @@ typedef struct dpm_module_info {
const char* name; /* module name (filename minus .so) */
const char* version; /* module's own X.Y.Z */
const char* description; /* one-line description */
const char* core_min; /* minimum libdpm-core version it supports */
} dpm_module_info;
/* ------------------------------------------------------------------ */
@@ -139,9 +138,9 @@ DPM_API void dpm_close(dpm_ctx* ctx);
* context. Modules are loaded at most once per context; repeated
* calls return the same handle.
*
* 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.
* Version compatibility is the caller's judgement: 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)
@@ -151,13 +150,11 @@ DPM_API void dpm_close(dpm_ctx* ctx);
DPM_API dpm_module* dpm_require(dpm_ctx* ctx, const char* name);
/**
* @brief Reports what libdpm-core sees in a loaded module
* @brief Reports what the library 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.
* Fills `out` with the module's name, version, and description,
* exactly as they were read at load. The caller decides whether the
* version it is looking at suits its purposes.
*
* @param ctx The libdpm-core context
* @param mod A module handle from dpm_require()
@@ -210,9 +207,8 @@ DPM_API dpm_cursor* dpm_list_modules(dpm_ctx* ctx);
/**
* @brief Advances an enumeration cursor
*
* Fills `out` with the next module's name, version, description, and
* minimum-libdpm-core version; the string pointers remain valid until
* context close.
* Fills `out` with the next module's name, version, and description;
* the string pointers remain valid until context close.
*
* @param cur The cursor from dpm_list_modules()
* @param out Receives the next module's information
@@ -296,11 +292,17 @@ DPM_API const char* dpm_last_error(dpm_ctx* ctx);
* int argc, char** argv);
* const char* dpm_module_version(void);
* const char* dpm_module_description(void);
* const char* dpm_module_core_min(void);
*
* dpm_module_execute is the module's entire functional surface; its
* capabilities are addressed by command string, so a module publishes
* no headers, struct layouts, or symbols to anything that calls it.
*
* A module determines for itself whether it can work with the library
* it is running against: dpm_core_version() reports the running
* version, and the module proceeds or fails on its own judgement. A
* module is built against the system-installed libdpm-core.so and is
* responsible for being correct against it.
*
* The library refuses to load any module that does not validate
* completely (see the DPM specification: load-time enforcement).
*/

View File

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

View File

@@ -1,6 +1,6 @@
/**
* @file version.hpp
* @brief X.Y.Z version parsing and comparison
* @brief X.Y.Z version parsing
*
* @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org>
@@ -33,13 +33,4 @@ namespace dpmcore {
*/
bool parse_version(const char* s, long out[3]);
/**
* @brief Compares two valid X.Y.Z version strings
*
* @param a First version
* @param b Second version
* @return -1 if a < b, 0 if equal, 1 if a > b
*/
int compare_versions(const char* a, const char* b);
} // namespace dpmcore

View File

@@ -4,7 +4,7 @@
*
* Bundles with the dpm binary and libdpm-core.so; used for testing and
* reporting functionality of libdpm-core.so. Implements the full DPM
* module contract: the four reserved symbols, with every capability
* module contract: the three reserved symbols, with every capability
* reached by command string through the entry point.
*
* @copyright Copyright (c) 2026 SILO GROUP LLC
@@ -28,7 +28,6 @@
#include "commands.hpp"
#define INFO_MODULE_VERSION "0.1.0"
#define INFO_CORE_MIN "0.1.0"
/* ------------------------------------------------------------------ */
/* Reserved contract symbols */
@@ -50,14 +49,6 @@ extern "C" const char* dpm_module_description(void)
return "Reports and tests libdpm-core functionality.";
}
/**
* @brief Returns the minimum libdpm-core version this module supports
*/
extern "C" const char* dpm_module_core_min(void)
{
return INFO_CORE_MIN;
}
/**
* @brief Command entry point
*

View File

@@ -1,18 +1,46 @@
/*
* libdpm-core.map — the export list for libdpm-core.so
* libdpm-core.map — the linker version script for libdpm-core.so
*
* Passed to the linker with --version-script. Names under `global` are
* exported; `local: *` hides everything else, so the public C API is the
* only surface a consumer or a loaded module can bind to.
* Passed to the linker with --version-script; see CMakeLists.txt, which
* also lists it as a link dependency so edits force a relink. It does
* two separate jobs.
*
* `global` is every function in include/dpm/core.h and nothing else. A
* new public function is added here in the commit that adds it to the
* header.
*
* The node name stamps each symbol (dpm_open@@DPM_CORE_1.0), so a break
* can ship as a new node while already-linked binaries keep resolving
* the old one. Newest node first. DPM_CORE_0.1 was never released; it is
* the shape a retired node takes.
* 1. It limits the export set.
*
* `global` names every function declared in <dpm/core.h>. `local: *`
* hides everything else, including the template instantiations that
* libstdc++ headers emit with default visibility.
*
*
* 2. It versions the exports.
*
* Every symbol here is stamped with the node name: dpm_open becomes
* dpm_open@@DPM_CORE_1.0. The linker reads that node off the library a
* consumer links against and records it in the consumer's own binary,
* and at startup the dynamic linker verifies the library still provides
* it.
*
* That keeps an already-installed module working after the library
* changes underneath it, which is what makes updating libdpm-core.so
* safe mid-bootstrap.
*
* A breaking change ships as a new node while the old node keeps its
* original definitions, so binaries built against the old node keep
* resolving them. Both live in one .so under one soname, preserving the
* invariant the routing model depends on: exactly one instance of the
* library mapped per process.
*
*
* Changing this file:
*
* - A new public function goes into `global` in the same commit that
* adds it to <dpm/core.h>.
* - A breaking change to an existing function adds a new node above
* DPM_CORE_1.0, leaving this node and its definitions intact.
*
* Newest node first. DPM_CORE_0.1 was never released; it is here as the
* shape a retired node takes.
*/
DPM_CORE_1.0 {
global:

View File

@@ -96,7 +96,6 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
"dpm_module_execute",
"dpm_module_version",
"dpm_module_description",
"dpm_module_core_min",
};
std::string missing;
@@ -117,25 +116,9 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
auto exec_f = reinterpret_cast<execute_fn>(resolve(handle, "dpm_module_execute"));
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
auto core_min_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_core_min"));
/* Step 2: minimum-version handshake. */
const char* core_min = core_min_f();
/* Step 2: probe the cheap calls. */
long parsed[3];
if (!core_min || !parse_version(core_min, parsed)) {
reason = "dpm_module_core_min() returned a malformed version";
dlclose(handle);
return nullptr;
}
if (compare_versions(core_min, DPM_CORE_VERSION_STR) > 0) {
reason = std::string("requires libdpm-core >= ") + core_min +
", running libdpm-core is " DPM_CORE_VERSION_STR
" — update libdpm-core";
dlclose(handle);
return nullptr;
}
/* Step 3: probe the cheap calls. */
const char* version = version_f();
if (!version || !parse_version(version, parsed)) {
reason = "dpm_module_version() returned a malformed version";
@@ -154,7 +137,6 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
mod->handle = handle;
mod->version = version;
mod->description = description;
mod->core_min = core_min;
mod->execute = exec_f;
return mod;
}
@@ -196,7 +178,6 @@ int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)
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;
}
@@ -264,7 +245,6 @@ dpm_cursor* dpm_list_modules(dpm_ctx* ctx)
info.name = mod->name.c_str();
info.version = mod->version.c_str();
info.description = mod->description.c_str();
info.core_min = mod->core_min.c_str();
cur->infos.push_back(info);
}

View File

@@ -1,6 +1,6 @@
/**
* @file version.cpp
* @brief X.Y.Z version parsing and comparison
* @brief X.Y.Z version parsing
*
* @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org>
@@ -60,24 +60,4 @@ bool parse_version(const char* s, long out[3])
return true;
}
int compare_versions(const char* a, const char* b)
{
long va[3] = {0, 0, 0};
long vb[3] = {0, 0, 0};
parse_version(a, va);
parse_version(b, vb);
for (int i = 0; i < 3; i++) {
if (va[i] < vb[i]) {
return -1;
}
if (va[i] > vb[i]) {
return 1;
}
}
return 0;
}
} // namespace dpmcore

View File

@@ -30,11 +30,6 @@ extern "C" const char* dpm_module_description(void)
return "Fixture with a malformed version.";
}
extern "C" const char* dpm_module_core_min(void)
{
return "0.1.0";
}
extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv)
{

View File

@@ -1,47 +0,0 @@
/**
* @file core_too_new.cpp
* @brief Broken fixture: demands a libdpm-core newer than any that exists
*
* Contract-complete, but dpm_module_core_min() reports 99.0.0.
* libdpm-core must refuse it at validation step 2 with a message
* naming the remedy (update libdpm-core).
*
* Part of the Dark Horse Linux Package Manager (DPM)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
extern "C" const char* dpm_module_version(void)
{
return "1.0.0";
}
extern "C" const char* dpm_module_description(void)
{
return "Fixture demanding a future libdpm-core.";
}
extern "C" const char* dpm_module_core_min(void)
{
return "99.0.0";
}
extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv)
{
(void)ctx;
(void)command;
(void)argc;
(void)argv;
return 0;
}

View File

@@ -34,11 +34,6 @@ extern "C" const char* dpm_module_description(void)
return "Known-good stub module.";
}
extern "C" const char* dpm_module_core_min(void)
{
return "0.1.0";
}
/* Answers "ping" with 42 so a dispatch round trip is observable, and
0 for anything else. */
extern "C" int dpm_module_execute(void* ctx, const char* command,

View File

@@ -2,9 +2,8 @@
* @file missing_symbols.cpp
* @brief Broken fixture: exports only part of the module contract
*
* Missing dpm_module_execute, dpm_module_core_min, and
* dpm_module_manifest. libdpm-core must refuse it at step 1 and
* name the missing symbols.
* Exports the version and description probes and nothing else.
* libdpm-core.so refuses it at step 1 and names dpm_module_execute.
*
* Part of the Dark Horse Linux Package Manager (DPM)
*

View File

@@ -102,9 +102,6 @@ int main(void)
CHECK(error_contains(ctx, "missing required contract symbols"));
CHECK(error_contains(ctx, "dpm_module_execute"));
CHECK(dpm_require(ctx, "core_too_new") == nullptr);
CHECK(error_contains(ctx, "update libdpm-core"));
CHECK(dpm_require(ctx, "bad_version") == nullptr);
CHECK(error_contains(ctx, "malformed version"));
@@ -125,7 +122,6 @@ int main(void)
CHECK(dpm_module_info_of(ctx, good, &seen) == 0);
CHECK(std::strcmp(seen.name, "good") == 0);
CHECK(std::strcmp(seen.version, "1.2.3") == 0);
CHECK(std::strcmp(seen.core_min, "0.1.0") == 0);
CHECK(seen.description != nullptr && *seen.description);
CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0);
@@ -150,7 +146,6 @@ int main(void)
count++;
CHECK(std::strcmp(info.name, "good") == 0);
CHECK(std::strcmp(info.version, "1.2.3") == 0);
CHECK(std::strcmp(info.core_min, "0.1.0") == 0);
CHECK(info.description != nullptr && *info.description);
}
CHECK(count == 1);