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) 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() enable_testing()
@@ -95,7 +95,7 @@ file(WRITE ${CMAKE_BINARY_DIR}/DartConfiguration.tcl
set(FIXTURE_MODULE_DIR ${CMAKE_BINARY_DIR}/tests/fixtures) set(FIXTURE_MODULE_DIR ${CMAKE_BINARY_DIR}/tests/fixtures)
foreach(fixture good missing_symbols core_too_new bad_version) foreach(fixture good missing_symbols bad_version)
add_library(fixture_${fixture} MODULE tests/fixtures/src/${fixture}.cpp) add_library(fixture_${fixture} MODULE tests/fixtures/src/${fixture}.cpp)
set_target_properties(fixture_${fixture} PROPERTIES set_target_properties(fixture_${fixture} PROPERTIES
PREFIX "" PREFIX ""
@@ -123,7 +123,7 @@ set_target_properties(test_core PROPERTIES
BUILD_RPATH "$ORIGIN/../lib" BUILD_RPATH "$ORIGIN/../lib"
) )
foreach(fixture good missing_symbols core_too_new bad_version) foreach(fixture good missing_symbols bad_version)
add_dependencies(test_core fixture_${fixture}) add_dependencies(test_core fixture_${fixture})
endforeach() 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 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. 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_cursor* cur = dpm_list_modules(ctx);
dpm_module_info info; dpm_module_info info;
while (dpm_cursor_next(cur, &info) == 0) { 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); dpm_cursor_free(cur);
``` ```
@@ -87,7 +87,7 @@ The cursor covers every valid module in the module path; invalid candidates are
## Services ## Services
- **`dpm_core_version`()** — the library version; callable without a context. - **`dpm_core_version`()** — the library version; callable without a context.
- **`dpm_module_info_of`(ctx, mod, out)** — the name, version, description, and minimum-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_config_get`(ctx, module, section, key)** — a value from a module's configuration namespace, or NULL if unset.
- **`dpm_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`. - **`dpm_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
- **`dpm_module_path`(ctx)** — the resolved module directory. - **`dpm_module_path`(ctx)** — the resolved module directory.

View File

@@ -28,10 +28,10 @@ Modules are loaded with `RTLD_LOCAL`, so a module's symbols never enter the glob
## Versioning model ## 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. - **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; 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. - **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 ## 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. Resolves the module `name` in the module path and runs the full load-time validation sequence (see Load-time enforcement) if the module is not already loaded in this context. On success returns a module handle owned by the context (repeated calls return the same handle — modules are loaded at most once per context). On failure returns NULL and records the precise reason: not found, or validation step failed with the step and detail. No version criterion is applied here; compatibility is the caller's to determine from the reported version.
**`int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`** **`int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`**
Fills `out` with the loaded module's name, version, description, and minimum-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)`** **`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. 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. 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-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)`** **`void dpm_cursor_free(dpm_cursor* cur)`**
Releases the cursor. NULL is a no-op. 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)`** **`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)`** **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.
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.
**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. **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: libdpm-core.so is the sole authority on module validity; the contract above is enforced by its validator, not by any SDK. Validation is all-or-nothing; a module is registered only after passing every step:
1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`. 1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`.
2. **Minimum-version handshake.** `dpm_module_core_min`() must be ≤ the running library version. If the library is too old, refuse and say so — the remedy is updating the library, and the message names it. 2. **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.
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. 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 **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 **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. - **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. 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. - **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. - **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 ## 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)`** **`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)`**
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: The `dpm_ctx` type and the service declarations all come from the installed public header:
``` ```
#include <dpm/core.h> #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 ## 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. 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 ## 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 ## 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. 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 ## 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. **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. **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.* *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. 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 ## Load-time enforcement
libdpm-core.so validates a module completely before offering it to anyone: libdpm-core.so validates a module completely before offering it to anyone:
1. Every reserved contract symbol resolves. 1. Every reserved contract symbol resolves.
2. The module's minimum library version is not newer than the running library's own. 2. The version and description probes return well-formed values.
3. 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. 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 ## 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 ## 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. - A harness that links the real libdpm-core.so and points the module path at build output plus fixture stubs exercises the real validation on a bare builder.
- Only final integration needs the whole system, and because alternate roots are first-class, it needs a directory rather than a virtual machine. - Only final integration needs the whole system, and because alternate roots are first-class, it needs a directory rather than a virtual machine.
This repository's own fixtures are deliberately broken stub modules — missing symbols, 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 ## 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* 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 libdpm-core version it supports */
} dpm_module_info; } 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 * context. Modules are loaded at most once per context; repeated
* calls return the same handle. * calls return the same handle.
* *
* Version compatibility is the caller's judgement, not this library's: * Version compatibility is the caller's judgement: read the loaded
* read the loaded module's version with dpm_module_info_of() and * module's version with dpm_module_info_of() and decide whether it is
* decide whether it is acceptable. * acceptable.
* *
* @param ctx The libdpm-core context * @param ctx The libdpm-core context
* @param name The module name (its filename minus .so) * @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); 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 * Fills `out` with the module's name, version, and description,
* minimum-libdpm-core version, exactly as they were read at load. No * exactly as they were read at load. The caller decides whether the
* compatibility conclusion is drawn from these values; the caller * version it is looking at suits its purposes.
* decides whether the version it is looking at is too new or too
* old for its purposes.
* *
* @param ctx The libdpm-core context * @param ctx The libdpm-core context
* @param mod A module handle from dpm_require() * @param mod A module handle from dpm_require()
@@ -210,9 +207,8 @@ 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, and description;
* minimum-libdpm-core version; the string pointers remain valid until * 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()
* @param out Receives the next module's information * @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); * int argc, char** argv);
* const char* dpm_module_version(void); * const char* dpm_module_version(void);
* const char* dpm_module_description(void); * const char* dpm_module_description(void);
* const char* dpm_module_core_min(void);
* *
* dpm_module_execute is the module's entire functional surface; its * dpm_module_execute is the module's entire functional surface; its
* capabilities are addressed by command string, so a module publishes * capabilities are addressed by command string, so a module publishes
* no headers, struct layouts, or symbols to anything that calls it. * 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 * The library refuses to load any module that does not validate
* completely (see the DPM specification: load-time enforcement). * completely (see the DPM specification: load-time enforcement).
*/ */

View File

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

View File

@@ -1,6 +1,6 @@
/** /**
* @file version.hpp * @file version.hpp
* @brief X.Y.Z version parsing and comparison * @brief X.Y.Z version parsing
* *
* @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>
@@ -33,13 +33,4 @@ namespace dpmcore {
*/ */
bool parse_version(const char* s, long out[3]); 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 } // namespace dpmcore

View File

@@ -4,7 +4,7 @@
* *
* Bundles with the dpm binary and libdpm-core.so; used for testing and * Bundles with the dpm binary and libdpm-core.so; used for testing and
* reporting functionality of libdpm-core.so. Implements the full DPM * 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. * reached by command string through the entry point.
* *
* @copyright Copyright (c) 2026 SILO GROUP LLC * @copyright Copyright (c) 2026 SILO GROUP LLC
@@ -28,7 +28,6 @@
#include "commands.hpp" #include "commands.hpp"
#define INFO_MODULE_VERSION "0.1.0" #define INFO_MODULE_VERSION "0.1.0"
#define INFO_CORE_MIN "0.1.0"
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Reserved contract symbols */ /* Reserved contract symbols */
@@ -50,14 +49,6 @@ extern "C" const char* dpm_module_description(void)
return "Reports and tests libdpm-core functionality."; 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 * @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 * Passed to the linker with --version-script; see CMakeLists.txt, which
* exported; `local: *` hides everything else, so the public C API is the * also lists it as a link dependency so edits force a relink. It does
* only surface a consumer or a loaded module can bind to. * 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 * 1. It limits the export set.
* 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 * `global` names every function declared in <dpm/core.h>. `local: *`
* the shape a retired node takes. * 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 { DPM_CORE_1.0 {
global: global:

View File

@@ -96,7 +96,6 @@ std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
"dpm_module_execute", "dpm_module_execute",
"dpm_module_version", "dpm_module_version",
"dpm_module_description", "dpm_module_description",
"dpm_module_core_min",
}; };
std::string missing; 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 exec_f = reinterpret_cast<execute_fn>(resolve(handle, "dpm_module_execute"));
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version")); auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description")); auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
auto core_min_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_core_min"));
/* Step 2: minimum-version handshake. */ /* Step 2: probe the cheap calls. */
const char* core_min = core_min_f();
long parsed[3]; 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(); const char* version = version_f();
if (!version || !parse_version(version, parsed)) { if (!version || !parse_version(version, parsed)) {
reason = "dpm_module_version() returned a malformed version"; 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->handle = handle;
mod->version = version; mod->version = version;
mod->description = description; mod->description = description;
mod->core_min = core_min;
mod->execute = exec_f; mod->execute = exec_f;
return mod; 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->name = mod->name.c_str();
out->version = mod->version.c_str(); out->version = mod->version.c_str();
out->description = mod->description.c_str(); out->description = mod->description.c_str();
out->core_min = mod->core_min.c_str();
return 0; return 0;
} }
@@ -264,7 +245,6 @@ dpm_cursor* dpm_list_modules(dpm_ctx* ctx)
info.name = mod->name.c_str(); info.name = mod->name.c_str();
info.version = mod->version.c_str(); info.version = mod->version.c_str();
info.description = mod->description.c_str(); info.description = mod->description.c_str();
info.core_min = mod->core_min.c_str();
cur->infos.push_back(info); cur->infos.push_back(info);
} }

View File

@@ -1,6 +1,6 @@
/** /**
* @file version.cpp * @file version.cpp
* @brief X.Y.Z version parsing and comparison * @brief X.Y.Z version parsing
* *
* @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>
@@ -60,24 +60,4 @@ bool parse_version(const char* s, long out[3])
return true; 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 } // namespace dpmcore

View File

@@ -30,11 +30,6 @@ extern "C" const char* dpm_module_description(void)
return "Fixture with a malformed version."; 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, extern "C" int dpm_module_execute(void* ctx, const char* command,
int argc, char** argv) 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."; 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 /* Answers "ping" with 42 so a dispatch round trip is observable, and
0 for anything else. */ 0 for anything else. */
extern "C" int dpm_module_execute(void* ctx, const char* command, extern "C" int dpm_module_execute(void* ctx, const char* command,

View File

@@ -2,9 +2,8 @@
* @file missing_symbols.cpp * @file missing_symbols.cpp
* @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 * Exports the version and description probes and nothing else.
* dpm_module_manifest. libdpm-core must refuse it at step 1 and * libdpm-core.so refuses it at step 1 and names dpm_module_execute.
* name the missing symbols.
* *
* Part of the Dark Horse Linux Package Manager (DPM) * 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, "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);
CHECK(error_contains(ctx, "update libdpm-core"));
CHECK(dpm_require(ctx, "bad_version") == nullptr); CHECK(dpm_require(ctx, "bad_version") == nullptr);
CHECK(error_contains(ctx, "malformed version")); CHECK(error_contains(ctx, "malformed version"));
@@ -125,7 +122,6 @@ int main(void)
CHECK(dpm_module_info_of(ctx, good, &seen) == 0); CHECK(dpm_module_info_of(ctx, good, &seen) == 0);
CHECK(std::strcmp(seen.name, "good") == 0); CHECK(std::strcmp(seen.name, "good") == 0);
CHECK(std::strcmp(seen.version, "1.2.3") == 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(seen.description != nullptr && *seen.description);
CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0); CHECK(dpm_module_info_of(ctx, nullptr, &seen) != 0);
@@ -150,7 +146,6 @@ int main(void)
count++; count++;
CHECK(std::strcmp(info.name, "good") == 0); CHECK(std::strcmp(info.name, "good") == 0);
CHECK(std::strcmp(info.version, "1.2.3") == 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(info.description != nullptr && *info.description);
} }
CHECK(count == 1); CHECK(count == 1);