Store the version in version.cpp and group the library under src/core

libdpm-core.so's version was reaching the source as a compile-line
definition, with a fallback in two translation units that would have let
the library build and report 0.0.0 if the build ever stopped supplying
it. The version is a literal in src/core/version.cpp, the file named for
it, alongside dpm_core_version() which returns it and the parser for the
X.Y.Z strings modules report.

The library's sources and its version script move to src/core, leaving
src/cli, src/bundled-modules, and the documentation source beside it.
This commit is contained in:
2026-08-15 22:57:46 -04:00
parent d3f134aabf
commit b641318663
5 changed files with 16 additions and 23 deletions

265
src/core/modules.cpp Normal file
View File

@@ -0,0 +1,265 @@
/**
* @file modules.cpp
* @brief Module discovery, load-time validation, routing, enumeration
*
* Implements the load-time enforcement sequence: libdpm-core.so is the
* sole authority on module validity. A module is either fully valid or
* not loaded — no partial states.
*
* Dispatch through dpm_execute is the only path into module code, so a
* caller never holds compile-time knowledge of what it is calling.
*
* @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/>.
*/
#include "internal/context.hpp"
#include "internal/version.hpp"
#include <algorithm>
#include <dlfcn.h>
#include <filesystem>
#include <new>
#include <set>
namespace fs = std::filesystem;
/* dlclose wrapper referenced from context.cpp so handle release stays
in one translation unit with the loader. */
void dpm_internal_unload(void* handle)
{
if (handle) {
dlclose(handle);
}
}
namespace {
using execute_fn = int (*)(dpm_ctx*, const char*, int, char**);
using string_fn = const char* (*)(void);
/** dlsym with dlerror() discipline; returns nullptr on any error. */
void* resolve(void* handle, const char* symbol)
{
dlerror();
void* addr = dlsym(handle, symbol);
if (dlerror() != nullptr) {
return nullptr;
}
return addr;
}
} // namespace
namespace dpm_core {
std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
const std::string& name,
std::string& reason)
{
std::string so_path = ctx->module_path + name + ".so";
std::error_code ec;
if (!fs::exists(so_path, ec)) {
reason = "not found at " + so_path;
return nullptr;
}
void* handle = dlopen(so_path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!handle) {
const char* err = dlerror();
reason = std::string("dlopen failed: ") + (err ? err : "unknown");
return nullptr;
}
/* Step 1: resolve all reserved contract symbols. */
static const char* required[] = {
"dpm_module_execute",
"dpm_module_version",
"dpm_module_description",
};
std::string missing;
for (const char* sym : required) {
if (!resolve(handle, sym)) {
if (!missing.empty()) {
missing += ", ";
}
missing += sym;
}
}
if (!missing.empty()) {
reason = "missing required contract symbols: " + missing;
dlclose(handle);
return nullptr;
}
auto exec_f = reinterpret_cast<execute_fn>(resolve(handle, "dpm_module_execute"));
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
/* Step 2: probe the cheap calls. */
long parsed[3];
const char* version = version_f();
if (!version || !parse_version(version, parsed)) {
reason = "dpm_module_version() returned a malformed version";
dlclose(handle);
return nullptr;
}
const char* description = desc_f();
if (!description || !*description) {
reason = "dpm_module_description() returned nothing";
dlclose(handle);
return nullptr;
}
auto mod = std::make_unique<dpm_module>();
mod->name = name;
mod->handle = handle;
mod->version = version;
mod->description = description;
mod->execute = exec_f;
return mod;
}
} // namespace dpm_core
extern "C" {
dpm_module* dpm_require(dpm_ctx* ctx, const char* name)
{
if (!ctx || !name || !*name) {
return nullptr;
}
auto it = ctx->modules.find(name);
if (it != ctx->modules.end()) {
return it->second.get();
}
std::string reason;
auto loaded = dpm_core::validate_and_load(ctx, name, reason);
if (!loaded) {
dpm_core::set_error(ctx, std::string("module '") + name +
"': " + reason);
return nullptr;
}
dpm_module* mod = loaded.get();
ctx->modules[name] = std::move(loaded);
return mod;
}
int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out)
{
if (!ctx || !mod || !out) {
return 1;
}
out->name = mod->name.c_str();
out->version = mod->version.c_str();
out->description = mod->description.c_str();
return 0;
}
int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command,
int argc, char** argv)
{
if (!ctx || !mod || !mod->execute) {
return 1;
}
return mod->execute(ctx, command, argc, argv);
}
dpm_cursor* dpm_list_modules(dpm_ctx* ctx)
{
if (!ctx) {
return nullptr;
}
std::error_code ec;
if (!fs::is_directory(ctx->module_path, ec)) {
dpm_core::set_error(ctx, "module path is not a readable directory: " +
ctx->module_path);
return nullptr;
}
std::set<std::string> names;
for (const auto& entry : fs::directory_iterator(ctx->module_path, ec)) {
if (ec) {
break;
}
if (!entry.is_regular_file() && !entry.is_symlink()) {
continue;
}
if (entry.path().extension() != ".so") {
continue;
}
names.insert(entry.path().stem().string());
}
auto* cur = new (std::nothrow) dpm_cursor;
if (!cur) {
return nullptr;
}
for (const std::string& name : names) {
dpm_module* mod = nullptr;
auto it = ctx->modules.find(name);
if (it != ctx->modules.end()) {
mod = it->second.get();
} else {
std::string reason;
auto loaded = dpm_core::validate_and_load(ctx, name, reason);
if (!loaded) {
dpm_log(ctx, DPM_LOG_WARN,
("module '" + name + "' failed validation: " +
reason).c_str());
continue;
}
mod = loaded.get();
ctx->modules[name] = std::move(loaded);
}
dpm_module_info info;
info.name = mod->name.c_str();
info.version = mod->version.c_str();
info.description = mod->description.c_str();
cur->infos.push_back(info);
}
return cur;
}
int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out)
{
if (!cur || !out || cur->idx >= cur->infos.size()) {
return 1;
}
*out = cur->infos[cur->idx];
cur->idx++;
return 0;
}
void dpm_cursor_free(dpm_cursor* cur)
{
delete cur;
}
} /* extern "C" */