Define the documentation build in docs/CMakeLists.txt and collect the prose
The Doxygen configuration, the page list, and the docs target live in docs/, and the written documents live in docs/PROSE/, leaving the top-level file with the output paths and the add_subdirectory call. The subdirectory is given its own binary directory so that CMake's scaffolding stays out of the documentation output, which holds pdf/ and html/ and nothing else. Paths both files need are set once above the call: the child's output and cleanup and the parent's clean rule refer to the same variables.
This commit is contained in:
88
docs/PROSE/BUILD.md
Normal file
88
docs/PROSE/BUILD.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Building DPM Core and CLI {#build}
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GCC/G++ supporting C++20
|
||||
- CMake 3.22 or later
|
||||
- Make
|
||||
- Doxygen (optional, for the 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.
|
||||
|
||||
## Development Builds
|
||||
|
||||
Configure a build tree and compile it:
|
||||
|
||||
```
|
||||
cmake -B <build-dir> -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build <build-dir>
|
||||
```
|
||||
|
||||
Artifacts land in:
|
||||
|
||||
```
|
||||
<build-dir>/bin/dpm the dpm binary
|
||||
<build-dir>/lib/libdpm-core.so the library
|
||||
<build-dir>/modules/info.so the bundled info module
|
||||
```
|
||||
|
||||
The `dpm` binary built here finds the locally built `libdpm-core.so` on its own — an embedded library search path points it at the `lib/` directory next to it in the build tree, so running it directly uses the 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.
|
||||
|
||||
### Automated Testing
|
||||
|
||||
```
|
||||
ctest --test-dir <build-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.
|
||||
|
||||
### Manual Execution
|
||||
|
||||
The build tree plus the test fixtures form a complete self-contained environment; no installation is required. Point the `dpm` binary at local paths with its override flags:
|
||||
|
||||
```
|
||||
<build-dir>/bin/dpm --config-dir ./tests/fixtures/conf --module-path <build-dir>/modules info version
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Release Builds
|
||||
|
||||
One build tree carries the whole sequence, so the artifacts that get tested are the artifacts that get installed:
|
||||
|
||||
```
|
||||
cmake -B <build-dir> -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake --build <build-dir>
|
||||
ctest --test-dir <build-dir> --output-on-failure
|
||||
cmake --install <build-dir>
|
||||
```
|
||||
|
||||
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 <build-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 <build-dir>
|
||||
```
|
||||
|
||||
Omit `-DCMAKE_INSTALL_PREFIX=/usr` at configure time for a `/usr/local` install. Under the install prefix this installs:
|
||||
|
||||
```
|
||||
bin/dpm the dpm binary
|
||||
lib/libdpm-core.so the library
|
||||
lib/dpm/modules/info.so the bundled info module
|
||||
include/dpm/ the public header
|
||||
```
|
||||
|
||||
The library's own configuration installs to `/etc/dpm/conf.d/core.conf` regardless of prefix.
|
||||
129
docs/PROSE/CONSUMERS.md
Normal file
129
docs/PROSE/CONSUMERS.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Interaction with libdpm-core.so {#consumers}
|
||||
|
||||
Programs link libdpm-core.so as an ordinary shared library dependency — the same way they link any other library — to reach the package manager in-process: build systems, installers, image builders, system tooling, and foreign-language bindings all use the library the `dpm` binary is built on. A program holding a default context is working against system configuration, the system module path, the system tree, and system locking, the same environment the installed `dpm` binary sees.
|
||||
|
||||
## Compiling and Linking
|
||||
|
||||
With the library installed, include the public header and link it:
|
||||
|
||||
```
|
||||
#include <dpm/core.h>
|
||||
```
|
||||
|
||||
```
|
||||
g++ myprog.cpp -ldpm-core
|
||||
```
|
||||
|
||||
The header installs to the standard include path and the library to the standard lib path, so no additional flags are required. The interface is a C ABI: every function is extern "C", every type crossing the boundary is a C type, and state passes through opaque handles — callable from C, C++, or any language with C FFI.
|
||||
|
||||
`<dpm/core.h>` names no module and carries no module-specific type. It offers two things: discovery of modules, and interaction with them.
|
||||
|
||||
## The Context
|
||||
|
||||
All work happens through a context handle:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
To point a context elsewhere, pass overrides — every field is optional:
|
||||
|
||||
```
|
||||
dpm_open_overrides ov = {
|
||||
"/path/to/conf.d",
|
||||
"/path/to/modules",
|
||||
"/path/to/root",
|
||||
-1
|
||||
};
|
||||
dpm_ctx* ctx = dpm_open(&ov);
|
||||
```
|
||||
|
||||
Leaving `config_dir` NULL selects `/etc/dpm/conf.d/`; leaving `module_path` NULL selects the configured value and then the built-in default; leaving `root` NULL selects `/`; a `log_level` of -1 takes the configured value.
|
||||
|
||||
The 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 simultaneous contexts with different roots are legal.
|
||||
|
||||
## Acquiring and Using Modules
|
||||
|
||||
`dpm_require` loads a module by name, on demand:
|
||||
|
||||
```
|
||||
dpm_module* mod = dpm_require(ctx, "mymodule");
|
||||
```
|
||||
|
||||
libdpm-core.so validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent or invalid — `dpm_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle.
|
||||
|
||||
`dpm_module_info_of` reports what the library saw in the loaded module:
|
||||
|
||||
```
|
||||
dpm_module_info info;
|
||||
dpm_module_info_of(ctx, mod, &info);
|
||||
```
|
||||
|
||||
`info` carries `name`, `version`, and `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.
|
||||
|
||||
`dpm_execute` invokes the module — a command name and arguments:
|
||||
|
||||
```
|
||||
int rc = dpm_execute(ctx, mod, "command", argc, argv);
|
||||
```
|
||||
|
||||
This is the only path into module code, and it is the same path the `dpm` binary uses and the same path a module uses to reach a peer. You address a module by name and a capability by command string, so your program compiles against no module header, no struct layout, and no module symbol. What a module accepts as commands and arguments, and what its return codes mean, is documented by that module.
|
||||
|
||||
## Enumerating Modules
|
||||
|
||||
```
|
||||
dpm_cursor* cur = dpm_list_modules(ctx);
|
||||
dpm_module_info info;
|
||||
while (dpm_cursor_next(cur, &info) == 0) {
|
||||
/* each iteration fills info */
|
||||
}
|
||||
dpm_cursor_free(cur);
|
||||
```
|
||||
|
||||
The cursor covers every valid module in the module path; invalid candidates are excluded and logged.
|
||||
|
||||
## Services
|
||||
|
||||
- `dpm_core_version`()** — the library version; callable without a context.
|
||||
- `dpm_module_info_of`(ctx, mod, out)** — the name, version, and description read from a loaded module.
|
||||
- `dpm_config_get`(ctx, module, section, key)** — a value from a module's configuration namespace, or NULL if unset.
|
||||
- `dpm_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
|
||||
- `dpm_module_path`(ctx)** — the resolved module directory.
|
||||
- `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`.
|
||||
|
||||
## Complete Example
|
||||
|
||||
```
|
||||
#include <dpm/core.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void) {
|
||||
dpm_ctx* ctx = dpm_open(NULL);
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "failed to initialize\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
dpm_module* mod = dpm_require(ctx, "info");
|
||||
if (!mod) {
|
||||
fprintf(stderr, "%s\n", dpm_last_error(ctx));
|
||||
dpm_close(ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rc = dpm_execute(ctx, mod, "version", 0, NULL);
|
||||
|
||||
dpm_close(ctx);
|
||||
return rc;
|
||||
}
|
||||
```
|
||||
268
docs/PROSE/DESIGN.md
Normal file
268
docs/PROSE/DESIGN.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# DPM Design {#design}
|
||||
|
||||
## Constraints
|
||||
|
||||
- Must be able to operate in a barren environment providing libc and libstdc++ — the standard build runs there as-is, no special variants. On a fully populated system the same binaries simply have more modules loadable.
|
||||
- Capability grows in layers: each layer installs the dependencies of the next using only what already works.
|
||||
- Every implementation exists exactly once, and is consumable by the `dpm` binary, by other layers, and by external programs (build systems, Dark Horse tooling) through C interfaces.
|
||||
- No module ever links, includes, or hardcodes anything belonging to another module. Every module-to-module interaction passes through libdpm-core.so.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
the dpm binary -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
|
||||
a build system -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
|
||||
```
|
||||
|
||||
`<dpm/core.h>` is the declared interface; `libdpm-core.so` implements it. A caller includes the header, links `-ldpm-core`, and asks the library to discover and invoke modules by name. A module reaching a peer performs the identical two steps its own caller performed, which is why the chain above repeats rather than branching into a second mechanism.
|
||||
|
||||
The header names no module and carries no module-specific type. It offers discovery and interaction, and that is the entire vocabulary — which is what allows the `dpm` binary, a build system, and a module to all use it without any of them being privileged.
|
||||
|
||||
### One Process, One Library Instance
|
||||
|
||||
The whole chain executes in the caller's address space. The `dpm` binary (or the build system) is the process; libdpm-core.so is mapped into it; every module the library loads is mapped into it as well. Calls are direct function calls — no subprocess, no serialization, no output parsing.
|
||||
|
||||
A module links libdpm-core.so like any other consumer, and when the module is loaded the dynamic linker binds it to the copy already mapped in the process. There is one instance, so a module drives the same context, module registry, configuration, and log targets the original caller opened.
|
||||
|
||||
Modules are loaded with `RTLD_LOCAL`, so a module's symbols never enter the global namespace. The only symbols a module can resolve are libdpm-core.so's. No path from one module to another exists that does not pass through the library.
|
||||
|
||||
## Versioning Model
|
||||
|
||||
Every version question is settled by the party that has to live with the answer. libdpm-core.so reports; it rules on nothing.
|
||||
|
||||
- **A module determines its own compatibility with the library.** It is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. `dpm_core_version()` reports the running version to any module that needs to act on it, and that module proceeds or fails on its own judgement.
|
||||
- **Consuming modules judge module versions.** A module that depends on another requires it by name, reads the reported version, and decides for itself whether that version is suitable for the commands it intends to issue. A load is a statement that the module is valid, never that it is compatible with a particular caller.
|
||||
|
||||
## libdpm-core.so
|
||||
|
||||
Dependencies: libc, libstdc++, libdl. Never more — the library must remain loadable in the barren case forever, so package logic never leaks into it. libdpm-core.so routes and hosts; modules implement.
|
||||
|
||||
It provides:
|
||||
|
||||
- **Discovery**: module path resolution, enumeration of installed module .so's.
|
||||
- **Validation**: the full load-time contract enforcement described below. libdpm-core.so is the sole authority on what a valid module is; the contract definition lives inside the library as data. There is no SDK package — the interface is specified by this document and enforced by the library's validator.
|
||||
- **Routing**: dispatch a command string with arguments to a named module. This is the only path into module code, and it is the same path for the `dpm` binary, for a build system, and for a module calling a peer.
|
||||
- **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.
|
||||
|
||||
## The C API
|
||||
|
||||
All functions are extern "C". All returned strings are owned by the library (or by the module that produced them), are valid until the context is closed, and are never freed by the caller. All functions returning int use 0 for success and nonzero error codes; details of the most recent failure are retrievable per-context.
|
||||
|
||||
### Context Lifecycle
|
||||
|
||||
`dpm_ctx* dpm_open(const dpm_open_overrides* overrides)`
|
||||
Creates a context. Reads configuration from `/etc/dpm/conf.d/` (or the config directory named in overrides), resolves the module path (overrides take precedence over config, config over the built-in default), and initializes logging per configuration. Performs no module loading. Returns NULL only on allocation failure or an unreadable/invalid explicit override; a missing config directory is not an error — defaults apply. `overrides` may be NULL, and may specify: config directory, module path, target root (for chroot/image/sysroot operation), and log level. Multiple simultaneous contexts with different roots are legal.
|
||||
|
||||
`void dpm_close(dpm_ctx* ctx)`
|
||||
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)`
|
||||
Resolves the module `name` in the module path and runs the full load-time validation sequence (see Load-Time Enforcement) if the module is not already loaded in this context. On success returns a module handle owned by the context (repeated calls return the same handle — modules are loaded at most once per context). On failure returns NULL and records the precise reason: not found, or validation step failed with the step and detail. No version criterion is applied here; compatibility is the caller's to determine from the reported version.
|
||||
|
||||
`int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)`
|
||||
Fills `out` with the loaded module's name, version, and description exactly as they were read at load (string pointers valid until context close). This is how a consumer obtains the version it will judge. The library attaches no meaning to the values. Returns 0 on success, nonzero if the module cannot be reported on.
|
||||
|
||||
`int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command, int argc, char** argv)`
|
||||
Dispatch: invokes the module's `dpm_module_execute` with the context, `command`, and the argument vector. Returns the module's return value verbatim (0 = success). The library adds nothing to the call besides delivery; argument semantics beyond "argv[0] is the command" are the module's to define.
|
||||
|
||||
This is the entire path into module code. A caller addresses a module by name and a capability by command string, so it holds no compile-time knowledge of the module it is calling — no headers, no struct layouts, no symbols. That is what allows a module to be developed, built, and tested with no peer present.
|
||||
|
||||
### Enumeration
|
||||
|
||||
`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)`
|
||||
Advances the cursor. Fills `out` with the next module's name, version, and description (string pointers valid until context close). Returns 0 and fills `out` while entries remain; returns nonzero at end.
|
||||
|
||||
`void dpm_cursor_free(dpm_cursor* cur)`
|
||||
Releases the cursor. NULL is a no-op.
|
||||
|
||||
### Services (Available to Modules and External Consumers Alike)
|
||||
|
||||
`const char* dpm_core_version(void)`
|
||||
Returns the library version as a static X.Y.Z string. Callable without a context.
|
||||
|
||||
`const char* dpm_config_get(dpm_ctx* ctx, const char* module, const char* section, const char* key)`
|
||||
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)`
|
||||
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)`
|
||||
Returns the resolved module directory path for this context.
|
||||
|
||||
`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 includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following reserved symbols as extern "C". Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; the library and consumers never free them.
|
||||
|
||||
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
|
||||
The module's command entry point, and the only entry through which it performs work. `ctx` is the host context that dispatched the call — the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
|
||||
|
||||
`const char* dpm_module_version(void)`
|
||||
Returns the module's own version as an X.Y.Z string. Must be constant for the life of the module. This is the value the library reports to consumers, and the value they judge compatibility against.
|
||||
|
||||
`const char* dpm_module_description(void)`
|
||||
Returns a one-line human-readable description, used in module listings.
|
||||
|
||||
**Version compatibility with the library is the module's own to determine.** A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where it needs to act on what it is running under, `dpm_core_version()` reports the running version and the module decides what to do with that.
|
||||
|
||||
**Symbol naming**: functional exports are prefixed with the module's name (raw_\*, pkg_\*); the dpm_ prefix is reserved for the contract and for libdpm-core.so.
|
||||
|
||||
**A module publishes no headers to other modules.** Its capabilities are addressed by command string through `dpm_execute`, so nothing about its internals — types, struct layouts, symbol names — is ever compiled into a caller. A module's documented command vocabulary is its interface.
|
||||
|
||||
## Load-Time Enforcement
|
||||
|
||||
libdpm-core.so is the sole authority on module validity; the contract above is enforced by its validator, not by any SDK. Validation is all-or-nothing; a module is registered only after passing every step:
|
||||
|
||||
1. **Resolve all reserved contract symbols.** Any missing → refuse, log the exact list, `dlclose`.
|
||||
2. **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.
|
||||
|
||||
## Module: raw — The File-Based Installer
|
||||
|
||||
Ships with the base system alongside libdpm-core.so. Depends on the baseline only; archive decompression is vendored in, and the package format is chosen to keep that small. This is what makes barren-environment operation possible: libdpm-core.so plus raw function with nothing else present.
|
||||
|
||||
- **Owns the backing tree** (`/var/lib/dpm/`): one directory per installed package holding manifest, metadata, and hooks. The tree is the database at this layer.
|
||||
- **Operations**, addressed as commands: install a package file, remove, verify, and queries answered by walking the tree — slow but always correct, zero dependencies.
|
||||
- **Owns the lock file and a transaction journal** with a generation counter. Every mutation in the entire system ultimately passes through raw, so locking and journaling are implemented exactly once and inherited by every layer above.
|
||||
|
||||
## Module: pkg — The Full Package Manager
|
||||
|
||||
Ships as a package, installed by raw once sqlite3 is installed. Requires raw — by name, through libdpm-core.so, judging raw's reported version itself — and libsqlite3.
|
||||
|
||||
- Never touches the tree directly: every filesystem mutation is a dispatch to raw through the library, in-process — shared locking, real error propagation, no output parsing. pkg is built with no knowledge of raw beyond its name and its command vocabulary.
|
||||
- **The sqlite database is a derived cache** under one invariant: it contains nothing that cannot be rebuilt by scanning the tree. It records the last journal generation it applied; on open, if the tree is ahead (someone used raw directly — explicitly allowed, that's the escape hatch for broken systems), it replays or rebuilds. Self-healing by construction.
|
||||
- Adds what the cache enables: fast queries, dependency resolution against the installed set, multi-package transactions with rollback.
|
||||
- Consumers that want dependency-aware operations dispatch to pkg by name; they transitively get raw's guarantees because there is no second code path to the tree.
|
||||
|
||||
## The dpm Binary
|
||||
|
||||
`dpm` is argument parsing and printing. It includes `<dpm/core.h>`, links `-ldpm-core`, enumerates modules, and forwards subcommands through dispatch. Its command surface is exactly the set of loadable modules — in the barren case that's raw's commands; on a full system, everything installed. No capability logic lives in it.
|
||||
|
||||
## External Consumers
|
||||
|
||||
Build systems and Dark Horse components link `libdpm-core.so` as an ordinary shared library dependency — `#include <dpm/core.h>`, `-ldpm-core`, the same as any other library they link:
|
||||
|
||||
- open a context (optionally against an alternate root),
|
||||
- require the module they need,
|
||||
- read its reported version and decide whether it suits them,
|
||||
- dispatch commands to it.
|
||||
|
||||
The header installs to the standard include path and the library to the standard lib path. A consumer that opens a default context (no overrides) is working against system configuration, the system module path, the system tree, and system locking — the same environment the installed `dpm` binary sees, because that binary is just another caller of the same library. Overrides redirect individual paths only when a caller explicitly sets them. Whether a consumer addresses raw only (image builders that just deploy trees) or pkg (dependency-aware tooling) is their choice of require(); behavior is identical to the `dpm` binary's because it is the same implementation.
|
||||
|
||||
## Bootstrap Chain
|
||||
|
||||
```
|
||||
minimal start: dpm + libdpm-core.so + raw module (baseline deps only)
|
||||
raw installs: sqlite3 package
|
||||
raw installs: dpm-pkg package (drops the pkg module .so)
|
||||
now: libdpm-core.so discovers pkg, validates it, full management is live
|
||||
```
|
||||
|
||||
Every layer is a package installed and upgraded by the layer beneath it; the package manager maintains itself with the same mechanism it offers the OS. Future modules follow the identical pattern — a repo/network module declares its requirements (pkg, a TLS library), lands as a package, and the capability appears on next discovery.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
Modules are developed independently from each other and independently from the library — one repository per module, plus the libdpm-core.so repository. Each repo owns its source, build, and tests, and produces exactly one artifact:
|
||||
|
||||
- **The libdpm-core.so repository**: `libdpm-core.so` and the `dpm` binary. Contains no module code. Its test fixtures include deliberately broken stub modules for validating the loader, and one known-good stub — never a real package module.
|
||||
- **The exception**: an info module that bundles with the `dpm` binary and libdpm-core.so, used to test and demonstrate full DPM system functionality where appropriate.
|
||||
- **One repository per module** (raw, pkg, and every future module): produces that module's .so. Links `libdpm-core.so` — the only cross-repo build dependency in the system — and nothing else from DPM. Peer modules never appear in a module's repository, build, or test environment; peers are runtime concerns, faked at test time with stub modules and real only at distribution-level integration.
|
||||
|
||||
No repository can block another's development: a module builds and its full pre-integration test surface runs with nothing present but its own checkout and an installed or vendored libdpm-core.so. Release coordination happens through the versioning model — each consumer judging the versions it is handed — rather than through lockstep builds.
|
||||
|
||||
### Layout of the libdpm-core.so Repository
|
||||
|
||||
```
|
||||
include/dpm/ public headers — installed to the system include path; the
|
||||
dpm/ directory is the consumer namespace, so an installed
|
||||
consumer writes #include <dpm/core.h>
|
||||
include/internal/ library-private headers — used only by src/, never installed
|
||||
src/core/ implementations of the library
|
||||
src/cli/ the dpm binary's entry point
|
||||
src/bundled-modules/info/ the bundled info module
|
||||
data/ files installed as-is (core.conf)
|
||||
tests/ fixture modules, the API test binary, CLI tests
|
||||
docs/PROSE/ the project's written documentation
|
||||
docs/ the Doxygen configuration and the docs target
|
||||
```
|
||||
|
||||
Every header lives under include/: include/dpm/ is the published API surface and defines what consumers see; include/internal/ is the implementation's own headers, invisible outside the repo because the install rule ships only include/dpm/.
|
||||
|
||||
## Artifacts
|
||||
|
||||
Terminology: **the `dpm` binary** names the command-line tool; **libdpm-core.so** names the library; **`<dpm/core.h>`** names its header.
|
||||
|
||||
| Artifact | Location on system |
|
||||
|---|---|
|
||||
| `dpm` | `/usr/bin/dpm` |
|
||||
| `libdpm-core.so` | `/usr/lib/libdpm-core.so` |
|
||||
| `core.h` | `/usr/include/dpm/core.h` |
|
||||
| `info.so` | `/usr/lib/dpm/modules/info.so` |
|
||||
| modules (`raw.so`, `pkg.so`, `repo.so`, `source.so`, ...) | `/usr/lib/dpm/modules/<name>.so` |
|
||||
|
||||
## Development and Testing
|
||||
|
||||
Development works because the design has no build-time coupling between peers: nothing links against a peer module, ever, and nothing includes a peer's headers, ever. "Not all the pieces are there" is the normal, permanent condition at build time. What remains resolves into three test layers, each needing strictly less than the full system.
|
||||
|
||||
### What a Build Requires
|
||||
|
||||
A module compiles against `<dpm/core.h>` and links `libdpm-core.so` — the one real link dependency, and by definition the stable, always-present, baseline-only piece. Cheap to have in every dev environment, trivially vendorable as a checkout.
|
||||
|
||||
Peers are reached at runtime by name and command string. Building pkg does not require raw to exist anywhere, and there is no compile-time knowledge of raw to acquire — not a header, not a struct, not a symbol. A module repo therefore builds self-contained, always.
|
||||
|
||||
### Test Layers
|
||||
|
||||
**1. Unit tests — need nothing.** The module's implementation compiles once as an object library, linked into both the .so and a test binary. Pure logic, error paths, parsing — no library, no peers.
|
||||
|
||||
**2. Module-hosting tests — need libdpm-core.so only.** A harness links the real library, points the module path at the build output plus fixtures, and has it load the just-built .so exactly as production would — full validation included, so contract violations fail here, in CI, not on a user's system. Where the module needs a peer, the fixture directory contains a **stub module**: a tiny .so exporting the reserved symbols and answering the commands the module under test issues, which the library validates and dispatches to like the real thing. The harness then drives `dpm_module_execute` end to end against fixture config and data. Because a peer is addressed by name and command, the stub is a complete substitute — there is nothing else about the real peer that the module under test could have depended on. This layer runs on a bare builder with nothing installed.
|
||||
|
||||
**3. Integration — the only layer that needs everything, and it builds itself.** Real libdpm-core.so plus real raw, then the actual bootstrap chain into a scratch root: `dpm_open` against an alternate root, raw installs sqlite3 and the pkg package into it, the library discovers pkg, real operations run against the throwaway tree. Because alternate roots are first-class in the API, this needs a directory, not a VM. Full-distribution CI does the same with real packages.
|
||||
|
||||
### Day-to-Day Workflow
|
||||
|
||||
- Working on **pkg**: edit, run unit tests (instant, zero environment), harness run with a raw stub before merge. A real raw is never needed, or even possessed, until integration.
|
||||
- Working on **raw**: same, except its tests need only fixture package files and a scratch tree.
|
||||
- Working on **libdpm-core.so**: its test fixtures are deliberately broken modules — missing symbols, a malformed version — plus one known-good stub. Development never needs any real package module.
|
||||
- **Debugging** is the layer-2 harness under a debugger — it is the "run the module without the system" mechanism, so no separate standalone build exists or is maintained.
|
||||
|
||||
The discipline that keeps this honest: stubs are written to the command vocabulary the real peer documents, and layer-2 validation plus the layer-3 bootstrap run in CI, so a stub that drifts from reality is caught by the first integration pass rather than shipped.
|
||||
|
||||
## Development Capabilities
|
||||
|
||||
During development the `dpm` binary must be pointable at a locally built library, and that library must be configurable to local paths (module path, config dir, etc.). Two mechanisms provide this:
|
||||
|
||||
- **Pointing the binary at a local library** is dynamic-linker territory, needing no DPM mechanism: development builds carry an rpath to their own build tree's lib/ directory, so the locally built binary resolves the locally built library (`LD_LIBRARY_PATH` pointed at that lib/ directory achieves the same). The system-installed library is never touched.
|
||||
- **Pointing that library at local paths** is what the `dpm_open` overrides exist for: config directory, module path, and target root are all fields of the overrides struct, and the `dpm` binary exposes them as flags. A dev invocation:
|
||||
|
||||
```
|
||||
./build/bin/dpm --config-dir ./tests/fixtures/conf --module-path ./build/modules raw install ./fixture.dpm
|
||||
```
|
||||
|
||||
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 flag on the `dpm` binary, so anything a linked consumer can redirect, a developer at the shell can redirect too. This holds for any future override field — nothing ships reachable from code but not from the command line.
|
||||
|
||||
## Evolution Rules
|
||||
|
||||
- **A module's interface is its command vocabulary.** Retiring or changing the meaning of a command is a version change in the module that owns it, judged by every consumer that dispatches to it.
|
||||
- **Breaking the library's exported ABI means a new symbol version generation**: the export set is what consumers link against, so a break is a deliberate, versioned event rather than an incidental one.
|
||||
- **Compatibility is decided by the 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
|
||||
|
||||
1. libdpm-core.so routes and hosts; modules implement. No package logic in the library, ever.
|
||||
2. `<dpm/core.h>` names no module and carries no module-specific type. It offers discovery and interaction only.
|
||||
3. A module never links, includes, or hardcodes anything belonging to another module. All module-to-module interaction passes through libdpm-core.so.
|
||||
4. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through library-mediated dispatch.
|
||||
5. Truth lives in the tree; everything above is regenerable cache or convenience.
|
||||
6. Dropping down a layer by hand is always legal; layers above detect it and reconcile.
|
||||
7. A module is either fully valid or not loaded — no partial states, no consumer-side defense.
|
||||
29
docs/PROSE/DOCUMENTATION.md
Normal file
29
docs/PROSE/DOCUMENTATION.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Generating the Code Reference {#documentation}
|
||||
|
||||
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, together with the documents in `docs/PROSE/` as its pages. `src/documentation.cpp` declares which documents those are and in what order; `docs/CMakeLists.txt` configures the generator and defines the target.
|
||||
|
||||
One command generates it:
|
||||
|
||||
```
|
||||
cmake --build <build-dir> --target docs
|
||||
```
|
||||
|
||||
The finished documents land in `<build-dir>/docs/`:
|
||||
|
||||
```
|
||||
<build-dir>/docs/pdf/dpm-core-<version>.pdf the PDF reference
|
||||
<build-dir>/docs/html/index.html the HTML reference
|
||||
<build-dir>/docs/tmp/ scratch space for the generators
|
||||
```
|
||||
|
||||
Doxygen and LaTeX both work under `docs/tmp/`, and the finished document is copied up into `docs/`. `cmake --build <build-dir> --target clean` removes the whole tree.
|
||||
|
||||
Both formats are produced by default. Either can be turned off at configure time:
|
||||
|
||||
- **`-DDPM_DOCS_PDF`** (default ON) — PDF reference, via Doxygen's native LaTeX output; requires `pdflatex` and `makeindex`
|
||||
- **`-DDPM_DOCS_HTML`** (default ON) — HTML reference
|
||||
|
||||
```
|
||||
cmake -B <build-dir> -DDPM_DOCS_PDF=OFF
|
||||
cmake --build <build-dir> --target docs
|
||||
```
|
||||
117
docs/PROSE/MODULES.md
Normal file
117
docs/PROSE/MODULES.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Developing DPM Modules {#modules}
|
||||
|
||||
A DPM module is one shared object in the module directory. libdpm-core.so loads it, validates it completely, and dispatches commands to it on behalf of whatever asked — the `dpm` binary, a build system, or another module. This document covers writing, building, testing, and installing a module.
|
||||
|
||||
## The Module Contract
|
||||
|
||||
A module includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following symbols as extern "C". All returned strings must be non-NULL, static or module-owned, and valid for the lifetime of the loaded module; callers never free them.
|
||||
|
||||
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
|
||||
The command entry point, and the only entry through which the module performs work. `ctx` is the host context that dispatched the call; the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name, equal to argv[0] when argc > 0. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
|
||||
|
||||
`const char* dpm_module_version(void)`
|
||||
The module's own version as an X.Y.Z string. libdpm-core.so reports this value to consumers, and each consumer decides for itself whether the version suits it.
|
||||
|
||||
`const char* dpm_module_description(void)`
|
||||
A one-line human-readable description, shown in module listings.
|
||||
|
||||
The `dpm_ctx` type and the service declarations all come from the installed public header:
|
||||
|
||||
```
|
||||
#include <dpm/core.h>
|
||||
```
|
||||
|
||||
## You Determine Your Own Compatibility With the Library
|
||||
|
||||
Your module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where you need to act on what you are running under, `dpm_core_version()` reports the running version and you decide what to do:
|
||||
|
||||
```
|
||||
const char* running = dpm_core_version();
|
||||
```
|
||||
|
||||
Check it, proceed or fail on your own terms, and report through `dpm_log` and your return code.
|
||||
|
||||
## Your Interface Is Your Command Vocabulary
|
||||
|
||||
A module publishes no headers, no struct layouts, and no symbols to anything that calls it. Everything it offers is reached through `dpm_module_execute`, addressed by command string, with arguments passed as an argument vector and a status returned as an int.
|
||||
|
||||
That is what a caller compiles against: a module name and a command name, both strings. Document your commands, their arguments, and their return codes — that documentation is your interface, and it is the only thing a consumer can depend on.
|
||||
|
||||
**Symbol naming**: every functional export is prefixed with the module's name (mymodule_\*). The dpm_ prefix is reserved for the contract symbols and for libdpm-core.so.
|
||||
|
||||
## Calling Another Module
|
||||
|
||||
A module reaches a peer by performing the same two steps its own caller performed — ask libdpm-core.so for the module by name, then ask libdpm-core.so to invoke it:
|
||||
|
||||
```
|
||||
int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)
|
||||
{
|
||||
dpm_module* peer = dpm_require(ctx, "othermodule");
|
||||
if (!peer) {
|
||||
dpm_log(ctx, DPM_LOG_ERROR, dpm_last_error(ctx));
|
||||
return 1;
|
||||
}
|
||||
|
||||
return dpm_execute(ctx, peer, "somecommand", argc, argv);
|
||||
}
|
||||
```
|
||||
|
||||
The `ctx` is the one handed to your entry point. Nothing else is needed to reach the library.
|
||||
|
||||
**Never link, include, or hardcode anything belonging to a peer.** No peer headers, no shared types, no peer symbols. Modules are loaded with `RTLD_LOCAL`, so a peer's symbols are not reachable from your module even if you tried — libdpm-core.so is the only path, and the only knowledge you hold about a peer is its name and the commands it documents.
|
||||
|
||||
A module that depends on a peer is the party that judges the peer's version. Require it, read its reported version with `dpm_module_info_of`, and decide whether it is suitable for the commands you intend to issue. libdpm-core.so reports; it does not rule.
|
||||
|
||||
## Validation at Load
|
||||
|
||||
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
|
||||
|
||||
A module repository builds with CMake:
|
||||
|
||||
```
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(mymodule)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
add_library(mymodule MODULE mymodule.cpp)
|
||||
|
||||
set_target_properties(mymodule PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".so"
|
||||
)
|
||||
|
||||
target_link_libraries(mymodule PRIVATE dpm-core)
|
||||
|
||||
install(TARGETS mymodule LIBRARY DESTINATION lib/dpm/modules)
|
||||
```
|
||||
|
||||
```
|
||||
cmake -B <build-dir>
|
||||
cmake --build <build-dir>
|
||||
```
|
||||
|
||||
libdpm-core.so is the only DPM link dependency a module ever has. Dependencies on other modules are runtime concerns, resolved by name through require and dispatch — a peer module is never linked, never included, and never needs to be present to build or to test.
|
||||
|
||||
## Running and Testing Locally
|
||||
|
||||
Load the freshly built module through a locally run `dpm` binary without installing anything:
|
||||
|
||||
```
|
||||
dpm --module-path <build-dir> mymodule <command>
|
||||
```
|
||||
|
||||
libdpm-core.so runs the full validation sequence on every load, so a contract mistake surfaces here, immediately and itemized, rather than after installation. The `--config-dir` flag points the module's configuration namespace at local files during development, and `--root` directs package operations at a scratch tree.
|
||||
|
||||
Where your module calls a peer, put a **stub module** in the fixture module path: a small .so exporting the 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
|
||||
|
||||
Modules install to `lib/dpm/modules` under the install prefix (`/usr/lib/dpm/modules` on a distribution install). libdpm-core.so discovers the module on its next scan; no registration step exists beyond the file being present and valid.
|
||||
|
||||
## A Working Example
|
||||
|
||||
The info module bundled with the `dpm` binary and libdpm-core.so, at `src/bundled-modules/info/`, tests and demonstrates full DPM system functionality, and in doing so shows the contract, command routing, and this build structure in working form.
|
||||
180
docs/PROSE/OVERVIEW.md
Normal file
180
docs/PROSE/OVERVIEW.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Overview {#overview}
|
||||
|
||||
## What DPM Is
|
||||
|
||||
DPM is the package manager for Dark Horse Linux. At its center is **libdpm-core.so**, a shared library that discovers modules, validates them, and routes calls to them, and that provides configuration and logging to whatever it has loaded. Modules — shared objects in the module directory — implement package functionality.
|
||||
|
||||
**The `dpm` binary** is the command-line tool, and it is one consumer of that library. Build systems, image builders, distribution tooling, and programs in any language with C FFI are equal consumers of the same library through the same interface.
|
||||
|
||||
libdpm-core.so implements no package operations. It routes and hosts.
|
||||
|
||||
## The Shape of the System
|
||||
|
||||
Everything reaches a module the same way, and a module reaching another module is the same step repeated:
|
||||
|
||||
```
|
||||
the dpm binary -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
|
||||
a build system -> <dpm/core.h> -> module A -> <dpm/core.h> -> module B
|
||||
```
|
||||
|
||||
`<dpm/core.h>` is the declared interface; libdpm-core.so is what implements it. A caller includes the header, links `-ldpm-core`, and asks the library to discover and invoke modules by name.
|
||||
|
||||
It all happens in one address space. The `dpm` binary (or the build system) is the process; libdpm-core.so is mapped into it; every module the library loads is mapped into it as well. Calls are direct function calls with no subprocess, no serialization, and no output parsing anywhere in the chain.
|
||||
|
||||
Two properties make the routing rule real rather than a convention:
|
||||
|
||||
- **There is one instance of libdpm-core.so in the process.** A module links against the library like any other consumer, and when the module is loaded the dynamic linker binds it to the copy already mapped. A module therefore drives the same context, the same module registry, and the same configuration the original caller opened.
|
||||
- **Modules cannot see each other.** They are loaded with `RTLD_LOCAL`, so a module's symbols never enter the global namespace. The only symbols a module can resolve are libdpm-core.so's, so there is physically no path from one module to another that does not pass through the library.
|
||||
|
||||
## Running the dpm Binary
|
||||
|
||||
The `dpm` binary's subcommand surface is exactly the set of loadable modules. `dpm <module> <command> [args...]` loads that module and hands the command to it; `dpm <module> help` asks the module to describe itself. There is no fixed list of operations baked into the tool, because the tool contains no capability logic — it parses arguments and prints.
|
||||
|
||||
`dpm --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.
|
||||
|
||||
Four flags redirect the system defaults:
|
||||
|
||||
- `-c, --config-dir PATH` — where configuration is read from
|
||||
- `-m, --module-path PATH` — which directory modules are loaded from
|
||||
- `-r, --root PATH` — the target root that package operations act on
|
||||
- `-L, --log-level LEVEL` — FATAL, ERROR, WARN, INFO, or DEBUG
|
||||
|
||||
These four exist because of a rule the design imposes on itself: every field a linked program can override must also be a flag, so anything reachable from code is reachable from a shell. That rule holds for any override added in the future.
|
||||
|
||||
## Writing a Program Against the Library
|
||||
|
||||
A program includes `<dpm/core.h>` and links `-ldpm-core`, the same as it would link any other shared library. Everything it does happens through a **context**, an opaque handle obtained from `dpm_open`.
|
||||
|
||||
Opening a context reads the configuration files, resolves which directory modules will be loaded from, and initializes logging. It loads no modules. The same four things the `dpm` binary exposes as flags are the fields of the overrides struct passed to `dpm_open`, and passing NULL accepts the system defaults — which is what makes a default context equivalent to what the installed `dpm` binary sees, since that binary is just another caller doing the same thing.
|
||||
|
||||
The target root override is what makes chroot builds, image assembly, and sysroot management work: package operations act on the given tree instead of the running system. Multiple contexts with different roots may be open at once.
|
||||
|
||||
The context owns everything it hands out. Every string a caller receives stays valid until `dpm_close`, and callers never free anything.
|
||||
|
||||
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_last_error` carrying the precise reason. Modules load at most once per context, and repeated calls return the same handle.
|
||||
|
||||
**Reading what 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.
|
||||
|
||||
**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 the library through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_module_path` to learn where modules live, `dpm_core_version` to learn the library's version. A module needs no file handling and no logging machinery of its own.
|
||||
|
||||
## How a Module Reaches Another Module
|
||||
|
||||
A module is a consumer of `<dpm/core.h>` exactly like the `dpm` binary is. To reach a peer it performs the identical two steps its own caller performed: ask the library for the module by name, then ask the library to invoke it.
|
||||
|
||||
```
|
||||
int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)
|
||||
{
|
||||
dpm_module* peer = dpm_require(ctx, "othermodule");
|
||||
if (!peer) {
|
||||
dpm_log(ctx, DPM_LOG_ERROR, dpm_last_error(ctx));
|
||||
return 1;
|
||||
}
|
||||
return dpm_execute(ctx, peer, "somecommand", argc, argv);
|
||||
}
|
||||
```
|
||||
|
||||
The `ctx` a module needs is the one handed to it in its own entry point, so it requires nothing else to reach the library.
|
||||
|
||||
**A module never links, includes, or hardcodes anything belonging to another module.** No peer headers, no shared struct layouts, no peer symbols. The only build dependency a module has is libdpm-core.so, and the only knowledge it holds about a peer is the peer's name and the command it wants to run. That is what allows every module to live in its own repository and be built with no peer present anywhere on the machine.
|
||||
|
||||
## What DPM Reads and Writes on Disk
|
||||
|
||||
**Configuration** lives in `/etc/dpm/conf.d/`. Each `.conf` file in that directory is one namespace named after the file: `core.conf` holds the library's own settings, and a module named `mymodule` reads `mymodule.conf`. Files are sectioned, and a value is addressed by namespace, section, and key. The library's own file carries the log level, whether to write a log file and where, and the default module directory.
|
||||
|
||||
**Modules** live in `/usr/lib/dpm/modules/`. A module's name is its filename without the `.so` extension, so `info.so` is the module named `info`. Discovery is the presence of a valid file in that directory — there is no registry, and no registration step.
|
||||
|
||||
**Logging** goes to the console always, and to a log file when configuration enables one; the default path is `/var/log/dpm/dpm.log`.
|
||||
|
||||
**The target root** defaults to `/`. Pointed elsewhere, it is the tree that package operations act on, which is what turns a scratch directory into a complete target for image assembly or a chroot build.
|
||||
|
||||
**Installed artifacts:**
|
||||
|
||||
| Artifact | Location |
|
||||
|---|---|
|
||||
| the `dpm` binary | `/usr/bin/dpm` |
|
||||
| `libdpm-core.so` | `/usr/lib/libdpm-core.so` |
|
||||
| public header | `/usr/include/dpm/core.h` |
|
||||
| modules | `/usr/lib/dpm/modules/<name>.so` |
|
||||
|
||||
## The Module Contract
|
||||
|
||||
*What a module author implements.*
|
||||
|
||||
A module is one `.so` in the module directory exporting three reserved symbols: a command entry point, its own version, and a one-line description. It includes `<dpm/core.h>` for those declarations and links `-ldpm-core`, and that is its entire build dependency.
|
||||
|
||||
The entry point receives the context that dispatched the call, the command name, and an argument vector, and returns an int. Everything a module offers the rest of the system is reachable through that one function, addressed by command name — which is what keeps a caller free of any compile-time knowledge of the module it is calling.
|
||||
|
||||
A module is built against the system-installed `libdpm-core.so` and is responsible for being correct against it. Where it needs to know what it is running on, `dpm_core_version()` reports the running version and the module acts on that itself.
|
||||
|
||||
## Load-Time Enforcement
|
||||
|
||||
libdpm-core.so validates a module completely before offering it to anyone:
|
||||
|
||||
1. Every reserved contract symbol resolves.
|
||||
2. The 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.
|
||||
|
||||
From the command line this shows up as a module missing from `--list-modules` with a logged reason, or as an itemized failure when the module is named directly.
|
||||
|
||||
## Versioning
|
||||
|
||||
Every version question belongs to the party that has to live with the answer.
|
||||
|
||||
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
|
||||
|
||||
### Because It Must Run on a Barren System
|
||||
|
||||
The founding constraint is that DPM has to work in an environment providing nothing but libc and libstdc++, using the same binaries that run on a fully populated system. That forbids libdpm-core.so from taking on any dependency beyond the baseline, which forbids package logic from living inside it, which produces the division the whole architecture rests on: **libdpm-core.so routes and hosts, modules implement.** Anything needing a database, compression, or TLS is a module that arrives later.
|
||||
|
||||
### Because Capability Has to Grow in Layers
|
||||
|
||||
Each layer of the system installs the dependencies of the next using only what already works. That requires upper layers to reach lower ones through a stable in-process interface rather than by re-implementing them, and it requires writes to flow strictly downward — a layer mutates the system only through the layer beneath it. Shared concerns like locking are then implemented once, at the bottom, and inherited by everything above.
|
||||
|
||||
### 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.so a C ABI library rather than an application with a library carved out of it. It is also why the command line and the C API stay in step: the flags are the override fields, one for one, so a program and a person redirect the same things by the same names.
|
||||
|
||||
### Because Nothing May Be Trusted That Has Not Been Verified
|
||||
|
||||
libdpm-core.so is the sole authority on module validity, and the contract is enforced by its validator rather than by an SDK a module author might skip, patch, or fall behind. That is why validation is all-or-nothing and why it happens at load: failures surface loudly and itemized at install time instead of halfway through an operation on a user's system.
|
||||
|
||||
### Because Coordination Happens at Load, Not at Build
|
||||
|
||||
A caller states the module name and command it needs and is told at load whether the module is there. That is what allows modules to be released independently: agreement is reached when the pieces meet, through declared versions each consumer judges for itself, instead of through lockstep builds.
|
||||
|
||||
### Because Modules Are Developed Independently
|
||||
|
||||
One repository per module, plus the libdpm-core.so repository. A module links libdpm-core.so and nothing else from DPM; peer modules never appear in its repository, build, or test environment. "Not all the pieces are there" is the normal, permanent condition at build time, so the architecture is arranged to make that a non-event:
|
||||
|
||||
- A module compiles against `<dpm/core.h>` and nothing else from DPM.
|
||||
- A peer is addressed by name and command string, so nothing about a peer needs to exist at compile time.
|
||||
- A harness that links the real libdpm-core.so and points the module path at build output plus fixture stubs exercises the real validation on a bare builder.
|
||||
- Only final integration needs the whole system, and because alternate roots are first-class, it needs a directory rather than a virtual machine.
|
||||
|
||||
This repository's own fixtures are deliberately broken stub modules — missing symbols, a malformed version — plus one known-good stub. Developing libdpm-core.so never requires a real package module to exist.
|
||||
|
||||
## 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.so, used to test and demonstrate full DPM system functionality where appropriate
|
||||
|
||||
Every other module is developed against libdpm-core.so and lives outside this repository.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. libdpm-core.so routes and hosts; modules implement. No package logic inside it, ever.
|
||||
2. `<dpm/core.h>` names no module and carries no module-specific type. It offers discovery and interaction only.
|
||||
3. A module never links, includes, or hardcodes anything belonging to another module. All module-to-module interaction passes through libdpm-core.so.
|
||||
4. Writes flow down, never sideways: a layer mutates the system only through the layer beneath it, in-process through mediated calls.
|
||||
5. A module is either fully valid or not loaded — no partial states, no consumer-side defense.
|
||||
6. Anything a linked program can redirect, a shell user can redirect too.
|
||||
60
docs/PROSE/STYLE.md
Normal file
60
docs/PROSE/STYLE.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Code Style Guide {#style}
|
||||
|
||||
Allman, with four modifications.
|
||||
|
||||
## Base
|
||||
|
||||
Allman: each brace on its own line, and the body indented one level inside them.
|
||||
|
||||
## Modification: Opening Braces Stay on the Line That Opens the Scope
|
||||
|
||||
An opening brace does not move to a line of its own. It stays on the line of the function definition, control statement, namespace, `extern "C"` block, struct, or enum that opens the scope. The closing brace keeps its own line.
|
||||
|
||||
## Modification: Every Scope Indents
|
||||
|
||||
Allman indents the body of a scope, and here that applies to every scope without exception — namespaces and `extern "C"` blocks included, not only functions and control statements. One level is four spaces.
|
||||
|
||||
```
|
||||
namespace dpm_core {
|
||||
bool parse_version(const char* s, long out[3]) {
|
||||
if (!s || !*s) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace dpm_core
|
||||
```
|
||||
|
||||
## Modification: `} else {` Stays on One Line
|
||||
|
||||
Allman would put the closing brace, the `else`, and the opening brace on three lines. They stay on one.
|
||||
|
||||
```
|
||||
if (level <= DPM_LOG_WARN) {
|
||||
std::fprintf(stderr, "%s: %s\n", level_name(level), message);
|
||||
} else {
|
||||
std::fprintf(stdout, "%s\n", message);
|
||||
}
|
||||
```
|
||||
|
||||
## Modification: A Short Guarded Statement May Occupy One Line
|
||||
|
||||
Where a condition guards a single short statement, the braces and the statement stay on the line with the condition. The braces are still written.
|
||||
|
||||
```
|
||||
if (v == "FATAL") { return DPM_LOG_FATAL; }
|
||||
if (v == "ERROR") { return DPM_LOG_ERROR; }
|
||||
if (v == "WARN") { return DPM_LOG_WARN; }
|
||||
```
|
||||
|
||||
## Modification: A Closing Brace Names the Scope It Ends
|
||||
|
||||
Where the opening line is far enough above to be off screen — a namespace or an `extern "C"` block — the closing brace carries a comment naming what it closes.
|
||||
|
||||
```
|
||||
} // namespace dpm_core
|
||||
```
|
||||
|
||||
```
|
||||
} /* extern "C" */
|
||||
```
|
||||
Reference in New Issue
Block a user