Module records and aliases

Installation records what a module reports about itself. dpm_install_module
opens a module once, writes its version and description to a .meta file in
/var/lib/dpm/metadata/, and records the alternate names it declared in
modules.aliases beside it. dpm_uninstall_module removes both, leaving the
module file in place.

dpm_list_modules reads those records and opens no module. A module with no
record lists with a version of <uninstalled> and still loads when a caller
names it. An unreadable or absent metadata directory costs the listing its
detail and costs nothing else.

Aliases give a module alternate names, declared through the new
dpm_module_aliases contract symbol or added with dpm_add_module_alias. A
name is recorded once: one already serving as an alias, or belonging to an
installed module, is refused rather than repointed. dpm_require matches a
name against the installed modules, then the alias table, then the module
path.

The metadata directory is a fifth override field and the -M flag, and
[modules] metadata in core.conf.

The test suite is four binaries covering context, modules, records, and
aliases, each a ctest case of its own, alongside the CLI cases.
This commit is contained in:
2026-08-24 03:51:39 -04:00
parent 71d409b5c7
commit 0291e61fd8
32 changed files with 2147 additions and 290 deletions

View File

@@ -96,6 +96,18 @@ typedef struct dpm_module dpm_module;
*/
typedef struct dpm_cursor dpm_cursor;
/**
* @brief A cursor over recorded module aliases
*
* Obtained from dpm_list_module_aliases(), advanced with
* dpm_alias_cursor_next(), and released with dpm_alias_cursor_free().
*
* The cursor holds its own copy of what it reports, so it stays readable
* after the alias table is changed, and releasing it leaves the table
* untouched.
*/
typedef struct dpm_alias_cursor dpm_alias_cursor;
/* ------------------------------------------------------------------ */
/* Log levels */
/* ------------------------------------------------------------------ */
@@ -145,6 +157,9 @@ typedef struct dpm_open_overrides {
/** -1 selects the configured value; otherwise a DPM_LOG_* level. */
int log_level;
/** NULL selects the configured value, then the built-in default. */
const char* metadata_dir;
} dpm_open_overrides;
/* ------------------------------------------------------------------ */
@@ -168,6 +183,20 @@ typedef struct dpm_module_info {
const char* description;
} dpm_module_info;
/**
* @brief One recorded alias
*
* Filled by dpm_alias_cursor_next(). The string pointers remain valid
* until the cursor is released.
*/
typedef struct dpm_alias_info {
/** The alternate name. */
const char* alias;
/** The module that name resolves to. */
const char* module;
} dpm_alias_info;
/* ------------------------------------------------------------------ */
/* Context lifecycle */
/* ------------------------------------------------------------------ */
@@ -209,10 +238,15 @@ void dpm_close(dpm_ctx* ctx);
/**
* @brief Loads and returns a validated module
*
* Resolves the named module in the module path and runs the full
* load-time validation sequence if it is not already loaded in this
* context. Modules are loaded at most once per context; repeated
* calls return the same handle.
* Resolves the name to a module file and runs the full load-time
* validation sequence if it is not already loaded in this context.
* Modules are loaded at most once per context; repeated calls return
* the same handle.
*
* The name is resolved against the installed modules first, then the
* alias table, then the module path directly. Reaching the module path
* means the module is not installed, which is logged and does not stop
* the load.
*
* Version compatibility is the caller's judgement: read the loaded
* module's version with dpm_get_module_info() and decide whether it is
@@ -267,18 +301,119 @@ DPM_PUBLIC_ABI_EXPORT
int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command,
int argc, char** argv);
/* ------------------------------------------------------------------ */
/* Installation */
/* ------------------------------------------------------------------ */
/**
* @brief Records what a module reports about itself
*
* Loads the named module once, reads its version, description, and
* declared aliases, and writes them into the metadata directory. From
* then on dpm_list_modules() reports that module from the record and
* opens nothing.
*
* Each declared alias is added when the name is free. A name already
* taken keeps its existing meaning and the installation continues.
*
* @param ctx The libdpm-core.so context
* @param name The module name, its filename minus .so
* @return 0 on success, nonzero on failure with the reason available
* from dpm_get_last_error()
*/
DPM_PUBLIC_ABI_EXPORT
int dpm_install_module(dpm_ctx* ctx, const char* name);
/**
* @brief Removes a module's record and every alias resolving to it
*
* Leaves the module file itself in place. The module is thereafter
* reported as uninstalled by dpm_list_modules(), and dpm_require() still
* loads it by its own name.
*
* @param ctx The libdpm-core.so context
* @param name The module name
* @return 0 on success, nonzero on failure with the reason available
* from dpm_get_last_error()
*/
DPM_PUBLIC_ABI_EXPORT
int dpm_uninstall_module(dpm_ctx* ctx, const char* name);
/* ------------------------------------------------------------------ */
/* Aliases */
/* ------------------------------------------------------------------ */
/**
* @brief Records an alternate name for a module
*
* A name is recorded once. A name already serving as an alias, or
* already belonging to an installed module, is refused, so an existing
* route to a module is never replaced by a later one. Changing where an
* alias points is a removal followed by an addition.
*
* @param ctx The libdpm-core.so context
* @param module The module the name resolves to
* @param alias The alternate name
* @return 0 on success, nonzero on failure with the reason available
* from dpm_get_last_error()
*/
DPM_PUBLIC_ABI_EXPORT
int dpm_add_module_alias(dpm_ctx* ctx, const char* module, const char* alias);
/**
* @brief Removes an alternate name
*
* @param ctx The libdpm-core.so context
* @param alias The alternate name, which identifies the entry on its own
* @return 0 on success, nonzero on failure with the reason available
* from dpm_get_last_error()
*/
DPM_PUBLIC_ABI_EXPORT
int dpm_remove_module_alias(dpm_ctx* ctx, const char* alias);
/**
* @brief Enumerates recorded aliases
*
* @param ctx The libdpm-core.so context
* @param module NULL for every alias on the system, or a module name for
* the aliases resolving to it
* @return A cursor over the matching aliases, or NULL on failure
*/
DPM_PUBLIC_ABI_EXPORT
dpm_alias_cursor* dpm_list_module_aliases(dpm_ctx* ctx, const char* module);
/**
* @brief Advances an alias cursor
*
* @param cur The cursor from dpm_list_module_aliases()
* @param out Receives the next alias and the module it resolves to
* @return 0 while entries remain; nonzero at end
*/
DPM_PUBLIC_ABI_EXPORT
int dpm_alias_cursor_next(dpm_alias_cursor* cur, dpm_alias_info* out);
/**
* @brief Releases an alias cursor
*
* @param cur The cursor to release; NULL is a no-op
*/
DPM_PUBLIC_ABI_EXPORT
void dpm_alias_cursor_free(dpm_alias_cursor* cur);
/* ------------------------------------------------------------------ */
/* Enumeration */
/* ------------------------------------------------------------------ */
/**
* @brief Enumerates the valid modules in the module path
* @brief Enumerates the modules in the module path
*
* Scans the module path and validates each candidate .so; failures
* are logged and excluded from the results.
* Reports each installed module from its record, and each module
* without one with a version of "&lt;uninstalled&gt;" and an empty
* description. No module is opened, so nothing in the module path is
* executed to produce a listing.
*
* @param ctx The libdpm-core.so context
* @return A cursor over all valid modules, or NULL on an unreadable
* @return A cursor over the modules present, or NULL on an unreadable
* module path
*/
DPM_PUBLIC_ABI_EXPORT
@@ -396,10 +531,16 @@ const char* dpm_get_last_error(dpm_ctx* ctx);
* int argc, char** argv);
* const char* dpm_module_version(void);
* const char* dpm_module_description(void);
* const char* dpm_module_aliases(void);
*
* dpm_module_execute is the only entry through which a module performs
* work. The other two are what it reports about itself; the library
* reads both at load and serves them through dpm_get_module_info.
* work. The other three are what it reports about itself; the library
* reads them at load, serves the version and description through
* dpm_get_module_info, and records the aliases at installation.
*
* dpm_module_aliases returns a comma-separated list of alternate names
* the module answers to, or NULL to declare none. NULL is an answer; the
* symbol's absence is a contract violation and the module is refused.
*/
#ifdef __cplusplus

View File

@@ -0,0 +1,85 @@
/**
* @file aliases.hpp
* @brief The alias table: alternate names a module answers to
*
* @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org>
*
* Part of the Dark Horse Linux Package Manager (DPM)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <dpm/core.h>
#include <string>
#include <vector>
struct dpm_ctx;
/** @brief Enumeration cursor over alias table entries */
struct dpm_alias_cursor {
/** One entry per alias the cursor reports. */
std::vector<dpm_alias_info> infos;
/** Backing strings, so the reported pointers stay valid. */
std::vector<std::pair<std::string, std::string>> storage;
/** Position of the next entry. */
size_t idx = 0;
};
namespace dpm_core {
/**
* @brief Reads the alias table out of a context's metadata directory
*
* The table is modules.aliases, one `alias = module` per line. A
* missing or unreadable file leaves the context holding no aliases,
* which costs those alternate names and costs nothing else.
*
* @param ctx The libdpm-core.so context
*/
void alias_load_table(dpm_ctx* ctx);
/**
* @brief Writes the context's alias table back to disk
*
* @param ctx The libdpm-core.so context
* @param reason Receives the failure reason when the write fails
* @return true on success
*/
bool alias_write_table(dpm_ctx* ctx, std::string& reason);
/**
* @brief Resolves an alias to the module it names
*
* @param ctx The libdpm-core.so context
* @param alias The alias to resolve
* @return The module name, or an empty string when the alias is unknown
*/
std::string alias_resolve(dpm_ctx* ctx, const std::string& alias);
/**
* @brief Splits a module's comma-separated alias list
*
* The form dpm_module_aliases() returns. Empty entries are dropped
* and each surviving name is trimmed, so trailing separators and
* spacing cost nothing.
*
* @param declared The value the module reported; NULL declares none
* @return One entry per name the module answers to
*/
std::vector<std::string> alias_split_declared(const char* declared);
} // namespace dpm_core

View File

@@ -28,6 +28,7 @@
#include <memory>
#include <string>
#include "internal/metadata.hpp"
#include "internal/modules.hpp"
/** @brief A libdpm-core.so context: configuration, logging, module registry */
@@ -38,6 +39,9 @@ struct dpm_ctx {
/** Directory modules are loaded from. */
std::string module_path;
/** Directory the .meta records and modules.aliases were read from. */
std::string metadata_dir;
/** Target root for package operations. */
std::string root;
@@ -58,6 +62,15 @@ struct dpm_ctx {
/** Validated modules keyed by name; each is loaded at most once. */
std::map<std::string, std::unique_ptr<dpm_module>> modules;
/** Records of installed modules, keyed by module name. */
std::map<std::string, dpm_module_meta_records> meta_records;
/** Placeholder records a listing reports for modules carrying none. */
std::map<std::string, dpm_module_meta_records> uninstalled;
/** Alternate module names, keyed by alias. */
std::map<std::string, std::string> aliases;
/** Reason for the most recent failure. */
std::string last_error;
};

View File

@@ -0,0 +1,128 @@
/**
* @file metadata.hpp
* @brief Module records: what libdpm-core.so knows without loading a module
*
* @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org>
*
* Part of the Dark Horse Linux Package Manager (DPM)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <map>
#include <string>
struct dpm_ctx;
/**
* @brief The records one module's .meta file carries
*
* Written when a module is installed, from what the module itself
* reported at that moment. A listing reports these values, so a listing
* needs to open no module.
*/
struct dpm_module_meta_records {
/** Basename of the module file these records describe. */
std::string module_file;
/** Version the module reported when it was installed. */
std::string version;
/** Description the module reported when it was installed. */
std::string description;
};
namespace dpm_core {
/**
* @brief Reads a flat key/value file from the metadata directory
*
* The format every file in that directory uses: one `key = value`
* per line, '#' and ';' opening a comment, and every key and value
* trimmed. A line carrying no '=' is skipped, so a malformed line
* costs that line and no more.
*
* @param path The file to read
* @param out Receives the pairs the file carried
* @return true when the file was opened; false when it was not
*/
bool md_read_key_value_file(const std::string& path,
std::map<std::string, std::string>& out);
/**
* @brief Writes a flat key/value file into the metadata directory
*
* Creates the directory when it is absent, and replaces the file's
* whole contents.
*
* @param path The file to write
* @param values The pairs to write, one per line
* @param reason Receives the failure reason when the write fails
* @return true on success
*/
bool md_write_key_value_file(
const std::string& path,
const std::map<std::string, std::string>& values,
std::string& reason);
/**
* @brief Reads every module record in a context's metadata directory
*
* Each .meta file becomes one set of records named after the file
* without its extension. A missing or unreadable directory leaves
* the context holding no records, which costs a listing its detail
* and costs nothing else.
*
* @param ctx The libdpm-core.so context
*/
void md_load_records_dir(dpm_ctx* ctx);
/**
* @brief Finds the records for a module name
*
* @param ctx The libdpm-core.so context
* @param module_name The module name to look up
* @return The records, or nullptr when no module of that name is recorded
*/
const dpm_module_meta_records* md_find_module_record(
dpm_ctx* ctx, const std::string& module_name);
/**
* @brief Writes one module's records into the metadata directory
*
* Replaces the records of the same module name, and updates the
* context's own copy so the change is visible without reopening.
*
* @param ctx The libdpm-core.so context
* @param module_name The module name the records belong to
* @param records The records to write
* @param reason Receives the failure reason when the write fails
* @return true on success
*/
bool md_write_module_record(dpm_ctx* ctx, const std::string& module_name,
const dpm_module_meta_records& records,
std::string& reason);
/**
* @brief Removes one module's records from the metadata directory
*
* @param ctx The libdpm-core.so context
* @param module_name The module name whose records are removed
* @param reason Receives the failure reason when the removal fails
* @return true on success
*/
bool md_remove_module_record(dpm_ctx* ctx, const std::string& module_name,
std::string& reason);
} // namespace dpm_core

View File

@@ -42,6 +42,9 @@ struct dpm_module {
/** dpm_module_description() as read at load. */
std::string description;
/** dpm_module_aliases() as read at load; empty when it returned NULL. */
std::string aliases;
/** Resolved dpm_module_execute; the only path into module code. */
int (*execute)(dpm_ctx*, const char*, int, char**) = nullptr;
};

View File

@@ -32,4 +32,12 @@ namespace dpm_core {
* @return The path, ending in a slash unless it is empty
*/
std::string with_trailing_slash(std::string s);
/**
* @brief Strips leading and trailing whitespace
*
* @param s The string to trim
* @return The trimmed string; empty when the input is entirely whitespace
*/
std::string trim(const std::string& s);
} // namespace dpm_core