From 267529bee32087933a5e74d85b93fe20a220edeb Mon Sep 17 00:00:00 2001 From: "Christopher M. Punches" Date: Fri, 14 Aug 2026 02:35:03 -0400 Subject: [PATCH] Drop the append-only versioning doctrine; document the version script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Module APIs and the library's exported ABI are versioned and retired deliberately: a break ships as a new table version or a new symbol version node, and consumers judge the versions they are handed. Document src/libdpm-core.map — what it pins, what belongs in it, and how a generation is retired — and declare DPM_CORE_0.1 as the shape the next retired node takes. --- docs/BUILD.md | 44 +++++++++++----- docs/CONSUMERS.md | 28 +++++----- docs/DESIGN.md | 116 +++++++++++++++++++++--------------------- docs/DOCUMENTATION.md | 10 ++-- docs/MODULES.md | 26 +++++----- docs/OVERVIEW.md | 30 +++++------ src/libdpm-core.map | 28 ++++++++++ 7 files changed, 164 insertions(+), 118 deletions(-) diff --git a/docs/BUILD.md b/docs/BUILD.md index 8de73ae..f08a0ef 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -6,11 +6,13 @@ - CMake 3.22 or later - Make - Doxygen (optional, for the code reference) -- pdflatex and makeindex (optional, for the PDF code reference) +- `pdflatex` and `makeindex` (optional, for the PDF code reference) The library itself depends only on libc and libstdc++, so it builds and runs on a minimal system. -## Building for development +## Development Builds + +Configure a build tree and compile it: ``` cmake -B -DCMAKE_BUILD_TYPE=Debug @@ -25,17 +27,17 @@ Artifacts land in: /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 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. +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 +### Automated Testing ``` ctest --test-dir --output-on-failure ``` -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. +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 +### Manual Execution The build tree plus the test fixtures form a complete self-contained environment; no installation is required. Point the CLI at local paths with its override flags: @@ -43,11 +45,11 @@ The build tree plus the test fixtures form a complete self-contained environment /bin/dpm --config-dir ./tests/fixtures/conf --module-path /modules info version ``` -The flags --config-dir, --module-path, --root, and --log-level each redirect the corresponding system default; --root sets the target root that package-operation modules act on. +The flags `--config-dir`, `--module-path`, `--root`, and `--log-level` each redirect the corresponding system default; `--root` sets the target root that package-operation modules act on. -## Building, testing, and installing a release +## Release Builds -One build tree carries the whole sequence — the test suite builds and runs in any configuration, so the artifacts that get tested are the artifacts that get installed: +One build tree carries the whole sequence, so the artifacts that get tested are the artifacts that get installed: ``` cmake -B -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr @@ -56,7 +58,25 @@ ctest --test-dir --output-on-failure cmake --install ``` -This is the packaging flow: configure once, build once, test what was built, install what was tested. Omit -DCMAKE_INSTALL_PREFIX=/usr for a /usr/local install. Under the install prefix this installs: +This is the packaging flow: configure once, build once, test what was built, install what was tested. + +### Automated Testing + +The test suite builds and runs in any configuration, so a release tree is tested by the same command a development tree is: + +``` +ctest --test-dir --output-on-failure +``` + +Running it between the build and the install step is what makes the sequence above a packaging flow rather than four unrelated commands. + +### Installation + +``` +cmake --install +``` + +Omit `-DCMAKE_INSTALL_PREFIX=/usr` at configure time for a `/usr/local` install. Under the install prefix this installs: ``` bin/dpm the CLI @@ -65,6 +85,4 @@ lib/dpm/modules/info.so the bundled info module include/dpm/ the public header ``` -The libdpm-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. diff --git a/docs/CONSUMERS.md b/docs/CONSUMERS.md index 4fdb63e..47089ce 100644 --- a/docs/CONSUMERS.md +++ b/docs/CONSUMERS.md @@ -1,6 +1,6 @@ # Consuming libdpm-core -Programs link libdpm-core to operate the package manager directly: build systems, installers, image builders, system tooling, and foreign-language bindings all use the same library the dpm CLI is built on. A program holding a default context is operating the installed package manager itself — system configuration, system module path, system tree, system locking — identically to invoking the installed dpm command. +Programs link libdpm-core to operate the package manager directly: build systems, installers, image builders, system tooling, and foreign-language bindings all use the same library the `dpm` CLI is built on. A program holding a default context is operating the installed package manager itself — system configuration, system module path, system tree, system locking — identically to invoking the installed `dpm` command. ## Compiling and linking @@ -26,7 +26,7 @@ dpm_ctx* ctx = dpm_open(NULL); dpm_close(ctx); ``` -dpm_open(NULL) reads the system configuration (/etc/dpm/conf.d/), resolves the system module path, and targets the root filesystem. dpm_close releases every handle the context issued; all pointers obtained through the context are invalid after it. +`dpm_open`(NULL) reads the system configuration (`/etc/dpm/conf.d/`), resolves the system module path, and targets the root filesystem. `dpm_close` releases every handle the context issued; all pointers obtained through the context are invalid after it. To point a context elsewhere, pass overrides — every field is optional: @@ -44,15 +44,15 @@ The root override is what makes chroot builds, image assembly, and sysroot manag ## Acquiring and using modules -**dpm_require** loads a module by name, on demand: +**`dpm_require`** loads a module by name, on demand: ``` dpm_module* mod = dpm_require(ctx, "mymodule"); ``` -libdpm-core validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent or invalid — dpm_last_error(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle. +libdpm-core validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent or invalid — `dpm_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle. -**dpm_module_info_of** reports what libdpm-core saw in the loaded module: +**`dpm_module_info_of`** reports what libdpm-core saw in the loaded module: ``` dpm_module_info info; @@ -61,13 +61,13 @@ dpm_module_info_of(ctx, mod, &info); /* info.name, .version, .description, .co 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: ``` int rc = dpm_execute(ctx, mod, "command", argc, argv); ``` -**dpm_get_api** returns a module's typed function table for direct calls: +**`dpm_get_api`** returns a module's typed function table for direct calls: ``` const mymodule_api_v1_s* api = (const mymodule_api_v1_s*)dpm_get_api(ctx, mod, "mymodule", 1); @@ -90,16 +90,16 @@ The cursor covers every valid module in the module path; invalid candidates are ## Services -- **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_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_last_error(ctx)** — a human-readable description of the most recent failure on the context, or NULL. +- **`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_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_last_error`(ctx)** — a human-readable description of the most recent failure on the context, or NULL. ## Ownership and errors -Strings returned by the library are owned by the context (or by the module that produced them) and remain valid until dpm_close; callers never free them. Functions returning int use 0 for success. Functions returning pointers use NULL for failure, with detail available from dpm_last_error. +Strings returned by the library are owned by the context (or by the module that produced them) and remain valid until `dpm_close`; callers never free them. Functions returning int use 0 for success. Functions returning pointers use NULL for failure, with detail available from `dpm_last_error`. ## Complete example diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f7e32f9..54ec917 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -25,15 +25,15 @@ (source of truth) (derived cache) ``` -libdpm-core.so is the single entry point for everything. Modules are shared objects that implement package functionality. All routing — library-to-module and module-to-module — passes through libdpm-core. No consumer touches dlopen, dlsym, or module discovery itself. +`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 Compatibility is directional, and the module is the one that declares it: -- **Each module reports the minimum libdpm-core version it supports** (a reserved contract symbol). At load, the running library compares its own version against that minimum: if it is older → refuse with an explicit "libdpm-core too old for this module" report; otherwise load. 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. +- **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. - **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**: 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. +- **An API is identified by name and table version.** A consumer asks for the exact pair it was written against, and a module that no longer carries that pair fails the request. Retiring a table is a version change in the module that retired it, judged by every consumer that requires it. ## libdpm-core.so @@ -47,7 +47,7 @@ libdpm-core provides: - generic dispatch — execute a command string with arguments against a named module (what the CLI uses); - typed access — a consumer requests a module's API at a version and receives a C function table (what modules and external programs use). - **Version reporting**: require resolves a module by name, loads it, and returns a handle, or reports precisely why it can't. The loaded module's version is readable from the handle, for the caller to judge. -- **Common services**: configuration access (per-module namespaces from /etc/dpm/conf.d/), logging, module-path queries. +- **Common services**: configuration access (per-module namespaces from `/etc/dpm/conf.d/`), logging, module-path queries. ## The libdpm-core C API @@ -55,74 +55,74 @@ All functions are extern "C". All returned strings are owned by libdpm-core (or ### Context lifecycle -**dpm_ctx\* dpm_open(const dpm_open_overrides\* overrides)** -Creates a libdpm-core context. Reads configuration from /etc/dpm/conf.d/ (or the config directory named in overrides), resolves the module path (overrides take precedence over config, config over the built-in default), and initializes logging per configuration. Performs no module loading. Returns NULL only on allocation failure or an unreadable/invalid explicit override; a missing config directory is not an error — defaults apply. `overrides` may be NULL, and may specify: config directory, module path, target root (for chroot/image/sysroot operation), and log level. Multiple simultaneous contexts with different roots are legal. +**`dpm_ctx* dpm_open(const dpm_open_overrides* overrides)`** +Creates a libdpm-core context. Reads configuration from `/etc/dpm/conf.d/` (or the config directory named in overrides), resolves the module path (overrides take precedence over config, config over the built-in default), and initializes logging per configuration. Performs no module loading. Returns NULL only on allocation failure or an unreadable/invalid explicit override; a missing config directory is not an error — defaults apply. `overrides` may be NULL, and may specify: config directory, module path, target root (for chroot/image/sysroot operation), and log level. Multiple simultaneous contexts with different roots are legal. -**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. ### Module acquisition -**dpm_module\* dpm_require(dpm_ctx\* ctx, const char\* name)** +**`dpm_module* dpm_require(dpm_ctx* ctx, const char* name)`** Resolves the module `name` in the module path and runs the full load-time validation sequence (see Load-time enforcement) if the module is not already loaded in this context. On success returns a module handle owned by the context (repeated calls return the same handle — modules are loaded at most once per context). On failure returns NULL and records the precise reason: not found, or validation step failed with the step and detail. No version criterion is applied here; compatibility is the caller's to determine from the reported version. -**int dpm_module_info_of(dpm_ctx\* ctx, dpm_module\* mod, dpm_module_info\* out)** +**`int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`** Fills `out` with the loaded module's name, version, description, and minimum-libdpm-core version exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. libdpm-core attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on. -**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. -**int dpm_execute(dpm_ctx\* ctx, dpm_module\* mod, const char\* command, int argc, char\*\* argv)** -Generic dispatch: invokes the module's dpm_module_execute with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). libdpm-core adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define. +**`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv)`** +Generic dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). libdpm-core adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define. ### Enumeration -**dpm_cursor\* dpm_list_modules(dpm_ctx\* ctx)** +**`dpm_cursor* dpm_list_modules(dpm_ctx* ctx)`** Scans the module path and returns a cursor over all *valid* modules (each candidate .so is validated on first scan; failures are logged and excluded). Returns NULL on an unreadable module path. -**int dpm_cursor_next(dpm_cursor\* cur, dpm_module_info\* out)** +**`int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out)`** Advances the cursor. Fills `out` with the next module's name, version, description, and minimum-libdpm-core version (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end. -**void dpm_cursor_free(dpm_cursor\* cur)** +**`void dpm_cursor_free(dpm_cursor* cur)`** Releases the cursor. NULL is a no-op. ### Services (available to modules and external consumers alike) -**const char\* dpm_core_version(void)** +**`const char* dpm_core_version(void)`** Returns the libdpm-core version as a static X.Y.Z string. Callable without a context. -**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/<module>.conf; the namespace "core", from core.conf, is the library's own). Returns NULL if unset. String valid until context close. +**`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/`<module>.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. -**const char\* dpm_module_path(dpm_ctx\* ctx)** +**`const char* dpm_module_path(dpm_ctx* ctx)`** Returns the resolved module directory path for this context. -**const char\* dpm_last_error(dpm_ctx\* ctx)** +**`const char* dpm_last_error(dpm_ctx* ctx)`** Returns a human-readable description of the most recent failure recorded on this context, or NULL if none. Overwritten by the next failing call on the same context. ## Module contract A module is one .so in the module directory. It exports, as extern "C", the following reserved symbols. Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; libdpm-core and consumers never free them. -**int dpm_module_execute(dpm_ctx\* ctx, const char\* command, int argc, char\*\* argv)** -The module's generic command entry point. `ctx` is the host context that dispatched the call — the module reaches every libdpm-core service (dpm_log, dpm_config_get, dpm_module_path, ...) through it. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. This is the only entry the CLI path ever uses; it must be callable immediately after load with no other setup. +**`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`** +The module's generic command entry point. `ctx` is the host context that dispatched the call — the module reaches every libdpm-core service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. This is the only entry the CLI path ever uses; it must be callable immediately after load with no other setup. -**const char\* dpm_module_version(void)** +**`const char* dpm_module_version(void)`** Returns the module's own version as an X.Y.Z string. Must be constant for the life of the module. This is the value libdpm-core reports to consumers, and the value they judge compatibility against. -**const char\* dpm_module_description(void)** +**`const char* dpm_module_description(void)`** Returns a one-line human-readable description, used in module listings. -**const char\* dpm_module_core_min(void)** +**`const char* dpm_module_core_min(void)`** Returns the minimum libdpm-core version (X.Y.Z) this module supports — the oldest one whose contract and services the module was written against. A libdpm-core older than that refuses to load the module, and says so. -**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" }). 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 libdpm-core. @@ -130,20 +130,20 @@ Returns a pointer to a static manifest table declaring the module's entire funct 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. -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. -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. +1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`. +2. **Minimum-version handshake.** `dpm_module_core_min`() must be ≤ the running libdpm-core version. If the library is too old, refuse and say so — the remedy is updating libdpm-core, and the message names it. Old modules on a newer libdpm-core always pass. +3. **Probe the cheap calls.** `dpm_module_version`() and `dpm_module_description`() are invoked immediately; NULL or malformed returns → refuse. +4. **Cross-check the manifest.** Every API the module declares must actually resolve via `dlsym`. A module advertising an API it doesn't export is refused. libdpm-core validates the module's entire declared surface at load, before offering any of it. 5. **Table sanity.** Check each declared table's magic constant (catches modules built against a stale or wrong layout) and minimum size for its version. -Failures happen at install/load time, loudly and itemized. Consumers never receive a partially valid module: if a handle was handed out, the contract already validated. The residual C-ABI limit — dlsym cannot verify signatures — is covered in practice by the magic, size, minimum-version handshake, and probes; defeating those requires deliberate lying, which is a package-signing concern upstream of the loader. +Failures happen at install/load time, loudly and itemized. Consumers never receive a partially valid module: if a handle was handed out, the contract already validated. 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 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. -- **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. +- **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. - **Owns the lock file and an append-only transaction journal** with a generation counter. Every mutation in the entire system ultimately passes through raw, so locking and journaling are implemented exactly once and inherited by every layer above. ## Module: pkg — the full package manager @@ -153,7 +153,7 @@ Ships as a package, installed by raw once sqlite3 is installed. Requires raw (vi - 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. - 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. ## The CLI @@ -161,7 +161,7 @@ Ships as a package, installed by raw once sqlite3 is installed. Requires raw (vi ## External consumers -Build systems and Dark Horse components link libdpm-core.so — the same library, the same path as everything else: +Build systems and Dark Horse components link `libdpm-core.so` — the same library, the same path as everything else: - open a context (optionally against an alternate root), - require the layer they need, @@ -169,7 +169,7 @@ Build systems and Dark Horse components link libdpm-core.so — the same library - fetch its API table, - call C functions directly. -libdpm-core installs its header to the standard include path and its library to the standard lib path: a consumer writes #include <dpm/core.h>, 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 <`dpm/core.h`>, 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 @@ -186,9 +186,9 @@ Every layer is a package installed and upgraded by the layer beneath it; the pac 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: -- **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 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. +- **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 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. 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. @@ -211,14 +211,14 @@ Every header lives under include/: include/dpm/ is the published API surface and ## Artifacts -Terminology: **the dpm binary** names the command-line tool; **libdpm-core** names the library. +Terminology: **the `dpm` binary** names the command-line tool; **libdpm-core** names the library. | Artifact | Location on system | |---|---| -| dpm | /usr/bin/dpm | -| libdpm-core.so | /usr/lib/libdpm-core.so | -| info.so | /usr/lib/dpm/modules/info.so | -| modules (raw.so, pkg.so, repo.so, source.so, ...) | /usr/lib/dpm/modules/<name>.so | +| `dpm` | `/usr/bin/dpm` | +| `libdpm-core.so` | `/usr/lib/libdpm-core.so` | +| `info.so` | `/usr/lib/dpm/modules/info.so` | +| modules (`raw.so`, `pkg.so`, `repo.so`, `source.so`, ...) | `/usr/lib/dpm/modules/.so` | ## Development and testing @@ -226,8 +226,8 @@ Development works because the design has no build-time coupling between peers: n ### What a build requires -- A module compiles against its own declarations (written to the spec — the externs it exports, the table structs it consumes) plus **libdpm-core.so, the one real link dependency** — and libdpm-core is by definition the stable, always-present, baseline-only piece. Cheap to have in every dev environment, trivially vendorable as a checkout. -- 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 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 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. @@ -235,11 +235,11 @@ A module repo therefore builds self-contained, always. **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 libdpm-core only.** A harness links the real libdpm-core, points the module path at the build output plus fixtures, and has it load the just-built .so exactly as production would — full five-step validation included, so contract violations fail here, in CI, not on a user's system. Where the module needs a peer, the fixture directory contains a **stub module**: a tiny .so exporting the reserved symbols and a fake table, which libdpm-core validates and serves like the real thing. The harness then drives dpm_module_execute end to end against fixture config and data. This layer runs on a bare builder with nothing installed. +**3. 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 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. +**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 @@ -254,22 +254,22 @@ The discipline that keeps this honest: fakes and stubs are written to the spec, 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 library (LD_LIBRARY_PATH pointed at that lib/ directory achieves the same). The system libdpm-core is never touched. -- **Pointing that library at local paths** is what the dpm_open overrides exist for: config directory, module path, and target root are all fields of the overrides struct, and the CLI exposes them as flags. A dev invocation: +- **Pointing 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 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 ``` -The config-dir override matters most: once the context reads config from the local conf dir, everything configurable — log file, module path defaults, per-module settings — resolves locally, so a checkout plus its fixtures is a complete self-contained environment. Adding --root at a scratch directory makes even real install operations land in a throwaway tree. +The config-dir override matters most: once the context reads config from the local conf dir, everything configurable — log file, module path defaults, per-module settings — resolves locally, so a checkout plus its fixtures is a complete self-contained environment. Adding `--root` at a scratch directory makes even real install operations land in a throwaway tree. -**Rule**: every field of the dpm_open overrides struct must be exposed as a CLI flag, so anything a linked consumer can redirect, a developer at the shell can redirect too. This holds for any future override field — nothing ships reachable from code but not from the command line. +**Rule**: every field of the `dpm_open` overrides struct must be exposed as a CLI flag, so anything a linked consumer can redirect, a developer at the shell can redirect too. This holds for any future override field — nothing ships reachable from code but not from the command line. ## 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 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. -- **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. +- **Breaking a module API means a new table version**: `raw_api_v2` ships as its own symbol, and `raw_api_v1` is retired on raw's schedule, published in raw's own version. A consumer that requires a retired table finds out when it asks for it, by name and version, before it does any work. +- **Breaking the library's exported ABI means a new symbol version node**: the version script pins the export set, and a break ships as a new node above the existing one, so binaries already linked keep resolving the node they were built against. +- **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. ## Invariants diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index a687873..2976da7 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -1,9 +1,9 @@ # Generating the Code Reference -When Doxygen is present, the build offers a docs target that generates the API and source reference from the documentation comments carried in the headers and sources. Two output formats are available as configure-time options: +When Doxygen is present, the build offers a `docs` target that generates the API and source reference from the documentation comments carried in the headers and sources. Two output formats are available as configure-time options: -- **-DDPM_DOCS_PDF** (default ON) — PDF reference, via Doxygen's native LaTeX output; requires pdflatex and makeindex -- **-DDPM_DOCS_HTML** (default OFF) — HTML reference +- **`-DDPM_DOCS_PDF`** (default ON) — PDF reference, via Doxygen's native LaTeX output; requires `pdflatex` and `makeindex` +- **`-DDPM_DOCS_HTML`** (default OFF) — HTML reference Generate and compile the PDF reference: @@ -11,9 +11,9 @@ Generate and compile the PDF reference: cmake --build --target docs-pdf ``` -The PDF lands at /docs/latex/refman.pdf. +The PDF lands at `/docs/latex/refman.pdf`. -With DPM_DOCS_HTML enabled at configure time, the docs target additionally produces the HTML reference in /docs/html: +With `DPM_DOCS_HTML` enabled at configure time, the `docs` target additionally produces the HTML reference in `/docs/html`: ``` cmake -B -DDPM_DOCS_HTML=ON diff --git a/docs/MODULES.md b/docs/MODULES.md index 311c6b3..2c34d19 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -6,22 +6,22 @@ A DPM module is one shared object in the module directory. libdpm-core loads it, 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)** -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. +**`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`** +The generic command entry point. `ctx` is the host context that dispatched the call; the module reaches every libdpm-core service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it. `command` is the subcommand name, equal to argv[0] when argc > 0. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup. -**const char\* dpm_module_version(void)** +**`const char* dpm_module_version(void)`** The module's own version as an X.Y.Z string. libdpm-core reports this value to consumers, and each consumer decides for itself whether the version suits it. -**const char\* dpm_module_description(void)** +**`const char* dpm_module_description(void)`** A one-line human-readable description, shown in module listings. -**const char\* dpm_module_core_min(void)** +**`const char* dpm_module_core_min(void)`** The minimum libdpm-core version (X.Y.Z) the module supports — the oldest one whose contract and services it was written against. A libdpm-core older than that refuses to load the module, and the refusal message says so. -**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. -The dpm_manifest and dpm_manifest_entry types, the table header, and the 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 @@ -29,7 +29,7 @@ The dpm_manifest and dpm_manifest_entry types, the table header, and the service ## API tables -A module's functions are published to consumers as API tables: one exported symbol per API version (mymodule_api_v1), pointing to a plain C struct of function pointers. Every table opens with a dpm_api_table_header — the DPM_API_TABLE_MAGIC constant, then the table struct's size in bytes as the module compiled it. Later revisions of the same version may append fields at the tail; the size field lets consumers detect what is present. +A module's functions are published to consumers as API tables: one exported symbol per API version (`mymodule_api_v1`), pointing to a plain C struct of function pointers. Every table opens with a `dpm_api_table_header` — the `DPM_API_TABLE_MAGIC` constant, then the table struct's size in bytes as the module compiled it. A consumer compares that size against the layout it was built against to see what it has been handed. All parameters and return values crossing a table are C types only. State passes through opaque handles; errors are int codes. @@ -69,22 +69,22 @@ cmake --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. +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 -Load the freshly built module through a locally run dpm without installing anything: +Load the freshly built module through a locally run `dpm` without installing anything: ``` dpm --module-path mymodule ``` -libdpm-core runs the full validation sequence on every load, so a contract mistake surfaces here, immediately and itemized, rather than after installation. The --config-dir flag points the module's configuration namespace at local files during development, and --root directs package operations at a scratch tree. +libdpm-core 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 -Modules install to lib/dpm/modules under the install prefix (/usr/lib/dpm/modules on a distribution install). libdpm-core discovers the module on its next scan; no registration step exists beyond the file being present and valid. +Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install). libdpm-core discovers the module on its next scan; no registration step exists beyond the file being present and valid. ## A working example -The info module bundled with the dpm binary and libdpm-core, at src/bundled-modules/info/, tests and demonstrates full DPM system functionality, and in doing so shows the contract, an API table, and this build structure in working form. +The info module bundled with the `dpm` binary and libdpm-core, 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. diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index 7763d1b..949f8b8 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -4,7 +4,7 @@ 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 binary** is the command-line tool, and it is one consumer of that library. Build systems, image builders, distribution tooling, and programs in any language with C FFI are equal consumers of the same library through the same interface. +**The `dpm` binary** is the command-line tool, and it is one consumer of that library. Build systems, image builders, distribution tooling, and programs in any language with C FFI are equal consumers of the same library through the same interface. libdpm-core implements no package operations. It routes and hosts. @@ -27,7 +27,7 @@ Everything passes through libdpm-core: binary-to-module, module-to-module, progr ## Running the dpm binary -The dpm binary's subcommand surface is exactly the set of loadable modules. `dpm [args...]` loads that module and hands the command to it; `dpm 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. +The `dpm` binary's subcommand surface is exactly the set of loadable modules. `dpm [args...]` loads that module and hands the command to it; `dpm help` asks the module to describe itself. There is no fixed list of operations baked into the tool, because the tool contains no capability logic — it parses arguments and prints. `dpm --list-modules` shows every module that passes validation, with its version and description. Candidates that fail are excluded from the listing and logged, so what is listed is what can actually be run. @@ -44,7 +44,7 @@ These four exist because of a rule the design imposes on itself: every field a l A program links libdpm-core and includes ``. Everything it does happens through a **context**, an opaque handle obtained from `dpm_open`. -Opening a context reads the configuration files, resolves which directory modules will be loaded from, and initializes logging. It loads no modules. The same four things the dpm binary exposes as flags are the fields of the overrides struct passed to `dpm_open`, and passing NULL accepts the system defaults — which is what makes a default context equivalent to invoking the installed dpm binary, since that binary is just another caller doing the same thing. +Opening a context reads the configuration files, resolves which directory modules will be loaded from, and initializes logging. It loads no modules. The same four things the `dpm` binary exposes as flags are the fields of the overrides struct passed to `dpm_open`, and passing NULL accepts the system defaults — which is what makes a default context equivalent to 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. @@ -56,10 +56,10 @@ The context owns everything it hands out. Every string a caller receives stays v **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_execute` passes a command name and an argument vector to the module's entry point. This is the path the `dpm` binary takes. - `dpm_get_api` returns a plain C struct of function pointers for a named API at a stated version. This is the path modules and external programs take to call functions directly. -**Enumerating.** `dpm_list_modules` yields a cursor over every valid module, which is what backs the listing the dpm binary prints. +**Enumerating.** `dpm_list_modules` yields a cursor over every valid module, which is what backs the listing the `dpm` binary prints. **Services.** A module reaches libdpm-core through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_module_path` to learn where modules live, `dpm_core_version` to learn the library's version. A module needs no file handling and no logging machinery of its own. @@ -77,7 +77,7 @@ The context owns everything it hands out. Every string a caller receives stays v | Artifact | Location | |---|---| -| the dpm binary | `/usr/bin/dpm` | +| 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/.so` | @@ -106,11 +106,11 @@ From the command line this shows up as a module missing from `--list-modules` wi ## Versioning -libdpm-core enforces exactly one version rule, the one where it is the host: each module states the oldest libdpm-core it supports, and a library older than that refuses to load the module, with a message naming the remedy. 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. +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. 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. -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. +A breaking change to a module's API means a new table version under its own symbol, and the module that owns it decides when the old version is retired. Because a consumer asks for an API by name and version, a retired table surfaces as a refused request at load, before the consumer has done any work. ## Why it works this way @@ -124,7 +124,7 @@ Each layer of the system installs the dependencies of the next using only what a ### Because one implementation must serve every caller -The requirement that every implementation exist exactly once, and be consumable by the dpm binary, by other layers, and by external programs, is what makes libdpm-core a C ABI library rather than an application with a library carved out of it. It is also why the command line and the C API stay in step: the flags are the override fields, one for one, so a program and a person redirect the same things by the same names. +The requirement that every implementation exist exactly once, and be consumable by the `dpm` binary, by other layers, and by external programs, is what makes libdpm-core a C ABI library rather than an application with a library carved out of it. It is also why the command line and the C API stay in step: the flags are the override fields, one for one, so a program and a person redirect the same things by the same names. ### Because nothing may be trusted that has not been verified @@ -132,9 +132,9 @@ libdpm-core is the sole authority on module validity, and the contract is enforc 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 coordination happens at load, not at build -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. +A consumer states the module and API version it needs and is told at load whether it is there. That is what allows modules to be released independently: agreement is reached when the pieces meet, through declared versions each consumer judges for itself, instead of through lockstep builds. ### Because modules are developed independently @@ -149,9 +149,9 @@ This repository's own fixtures are deliberately broken stub modules — missing ## What this repository produces -- **libdpm-core.so** — the library -- **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 +- **`libdpm-core.so`** — the library +- **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 Every other module is developed against libdpm-core and lives outside this repository. @@ -167,5 +167,5 @@ Every other module is developed against libdpm-core and lives outside this repos - **DESIGN.md** — the full design specification - **CONSUMERS.md** — linking libdpm-core and driving it from a program - **MODULES.md** — writing, building, testing, and installing a module -- **BUILD.md** — building, testing, and installing libdpm-core and the dpm binary +- **BUILD.md** — building, testing, and installing libdpm-core and the `dpm` binary - **DOCUMENTATION.md** — generating the code reference diff --git a/src/libdpm-core.map b/src/libdpm-core.map index 3a1ac37..b61c5bf 100644 --- a/src/libdpm-core.map +++ b/src/libdpm-core.map @@ -1,3 +1,28 @@ +/* + * libdpm-core.map — the linker version script for libdpm-core.so + * + * Passed to the linker with --version-script. It does two things: it + * pins the library's exported symbol list, and it stamps every exported + * symbol with the version node named below. + * + * What goes in `global`: every function declared in the public header, + * include/dpm/core.h, and nothing else. The library compiles with + * hidden default visibility, so `local: *` is what the implementation + * already gets; listing it here makes the export set explicit and fails + * the link loudly if the two ever disagree. + * + * Changing it: adding a public function means adding its name here in + * the same commit that adds it to the header. Removing or renaming one + * breaks every consumer already linked against it, so a removal ships + * as a new node above the existing one — binaries already linked + * against the older node keep resolving it while new links bind to the + * newer one. + * + * The node name carries the ABI generation. Newest node first, retired + * generations below it. DPM_CORE_0.1 at the bottom was never released — + * it is there so the next break follows its shape instead of inventing + * one. + */ DPM_CORE_1.0 { global: dpm_open; @@ -17,3 +42,6 @@ DPM_CORE_1.0 { local: *; }; + +DPM_CORE_0.1 { +};