Initial commit: DPM core library, CLI, bundled info module, and tests

This commit is contained in:
2026-08-09 18:00:05 -04:00
commit 19cf285cb7
29 changed files with 3722 additions and 0 deletions

265
src/cli/dpm.cpp Normal file
View File

@@ -0,0 +1,265 @@
/**
* @file dpm.cpp
* @brief The dpm CLI: argument parsing and printing over libdpm-core
*
* Thin client of libdpm-core. Every field of the dpm_open_overrides
* struct is exposed as a flag; subcommands map onto library calls; the
* command surface is exactly the set of loadable modules.
*
* @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 <dpm/core.h>
#include <cstdio>
#include <cstring>
#include <string>
#include <strings.h>
#include <vector>
namespace {
/**
* @brief Prints the CLI usage message
*/
void show_help()
{
std::printf(
"Usage: dpm [options] [module] [module args...]\n"
"\n"
"Options:\n"
" -c, --config-dir PATH Configuration directory (default /etc/dpm/conf.d/)\n"
" -m, --module-path PATH Module directory (overrides configuration)\n"
" -r, --root PATH Target root for package operations (default /)\n"
" -L, --log-level LEVEL FATAL, ERROR, WARN, INFO, or DEBUG\n"
" -l, --list-modules List available modules\n"
" -h, --help Show this help message\n"
"\n"
"For module-specific help, use: dpm <module> help\n");
}
/**
* @brief Parses a log level name
*
* @param name The level name (case-insensitive)
* @return The DPM_LOG_* level, or -1 if unrecognized
*/
int level_from_name(const char* name)
{
if (strcasecmp(name, "FATAL") == 0) { return DPM_LOG_FATAL; }
if (strcasecmp(name, "ERROR") == 0) { return DPM_LOG_ERROR; }
if (strcasecmp(name, "WARN") == 0) { return DPM_LOG_WARN; }
if (strcasecmp(name, "INFO") == 0) { return DPM_LOG_INFO; }
if (strcasecmp(name, "DEBUG") == 0) { return DPM_LOG_DEBUG; }
return -1;
}
/**
* @brief Matches an argument against an option's short and long forms
*
* Recognizes the separate-value forms and the --option=value form.
*
* @param arg The command-line argument to test
* @param short_form The option's short form (e.g. "-c")
* @param long_form The option's long form (e.g. "--config-dir")
* @param inline_value Receives the text after '=' for the inline
* form, or NULL when the value is in the next
* argument
* @return true if the argument is this option
*/
bool option_matches(const char* arg, const char* short_form,
const char* long_form, const char** inline_value)
{
if (std::strcmp(arg, short_form) == 0 ||
std::strcmp(arg, long_form) == 0) {
*inline_value = nullptr;
return true;
}
size_t len = std::strlen(long_form);
if (std::strncmp(arg, long_form, len) == 0 && arg[len] == '=') {
*inline_value = arg + len + 1;
return true;
}
return false;
}
/**
* @brief Prints the table of available modules
*
* @param ctx The core context
* @return 0 on success, 1 on failure
*/
int list_modules(dpm_ctx* ctx)
{
dpm_cursor* cur = dpm_list_modules(ctx);
if (!cur) {
const char* err = dpm_last_error(ctx);
std::fprintf(stderr, "dpm: %s\n", err ? err : "module listing failed");
return 1;
}
std::vector<dpm_module_info> infos;
dpm_module_info info;
while (dpm_cursor_next(cur, &info) == 0) {
infos.push_back(info);
}
dpm_cursor_free(cur);
if (infos.empty()) {
std::printf("No valid modules found in %s\n", dpm_module_path(ctx));
return 0;
}
size_t name_w = std::strlen("MODULE");
size_t version_w = std::strlen("VERSION");
for (const auto& i : infos) {
name_w = std::max(name_w, std::strlen(i.name));
version_w = std::max(version_w, std::strlen(i.version));
}
std::printf("Available DPM modules:\n\n");
std::printf("%-*s %-*s %s\n",
static_cast<int>(name_w), "MODULE",
static_cast<int>(version_w), "VERSION",
"DESCRIPTION");
for (const auto& i : infos) {
std::printf("%-*s %-*s %s\n",
static_cast<int>(name_w), i.name,
static_cast<int>(version_w), i.version,
i.description);
}
std::printf("\nUse 'dpm <module> help' for module-specific help.\n");
return 0;
}
} // namespace
int main(int argc, char** argv)
{
dpm_open_overrides overrides = {nullptr, nullptr, nullptr, -1};
bool list = false;
/* Holders for --opt=value forms. */
std::string config_dir;
std::string module_path;
std::string root;
int i = 1;
for (; i < argc; i++) {
const char* arg = argv[i];
if (arg[0] != '-') {
break;
}
/* Yields the option's value: the inline "=value" text if
present, otherwise the next argument. Prints an error and
returns nullptr when no value exists. */
const char* inline_value = nullptr;
auto take_value = [&]() -> const char* {
if (inline_value) {
return inline_value;
}
if (i + 1 < argc) {
return argv[++i];
}
std::fprintf(stderr, "dpm: option '%s' requires a value\n", arg);
return nullptr;
};
if (option_matches(arg, "-c", "--config-dir", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
config_dir = v;
overrides.config_dir = config_dir.c_str();
} else if (option_matches(arg, "-m", "--module-path", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
module_path = v;
overrides.module_path = module_path.c_str();
} else if (option_matches(arg, "-r", "--root", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
root = v;
overrides.root = root.c_str();
} else if (option_matches(arg, "-L", "--log-level", &inline_value)) {
const char* v = take_value();
if (!v) { return 1; }
int level = level_from_name(v);
if (level < 0) {
std::fprintf(stderr,
"dpm: unknown log level '%s' (use FATAL, ERROR, "
"WARN, INFO, or DEBUG)\n", v);
return 1;
}
overrides.log_level = level;
} else if (std::strcmp(arg, "-l") == 0 ||
std::strcmp(arg, "--list-modules") == 0) {
list = true;
} else if (std::strcmp(arg, "-h") == 0 ||
std::strcmp(arg, "--help") == 0) {
show_help();
return 0;
} else {
std::fprintf(stderr, "dpm: unknown option '%s'\n\n", arg);
show_help();
return 1;
}
}
dpm_ctx* ctx = dpm_open(&overrides);
if (!ctx) {
std::fprintf(stderr,
"dpm: initialization failed — check that override paths "
"exist and are readable\n");
return 1;
}
if (list) {
int rc = list_modules(ctx);
dpm_close(ctx);
return rc;
}
if (i >= argc) {
show_help();
dpm_close(ctx);
return 0;
}
const char* module_name = argv[i];
char** module_argv = &argv[i + 1];
int module_argc = argc - i - 1;
dpm_module* mod = dpm_require(ctx, module_name, nullptr);
if (!mod) {
const char* err = dpm_last_error(ctx);
std::fprintf(stderr, "dpm: %s\n",
err ? err : "module could not be loaded");
dpm_close(ctx);
return 1;
}
const char* command = module_argc > 0 ? module_argv[0] : nullptr;
int rc = dpm_execute(ctx, mod, command, module_argc, module_argv);
dpm_close(ctx);
return rc;
}