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.
411 lines
14 KiB
C++
411 lines
14 KiB
C++
/**
|
|
* @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>
|
|
|
|
/**
|
|
* @brief Helpers private to this translation unit
|
|
*
|
|
* Argument matching and printing, used by main() alone. An unnamed
|
|
* namespace gives them internal linkage, which keeps them out of the
|
|
* binary's symbol table and out of reach of anything linked with it.
|
|
*/
|
|
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"
|
|
" -M, --metadata-dir PATH Module record 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"
|
|
"Module records:\n"
|
|
" --install-module NAME Record what a module reports about itself\n"
|
|
" --uninstall-module NAME Remove a module's record and its aliases\n"
|
|
" --add-alias MODULE ALIAS Record an alternate name for a module\n"
|
|
" --remove-alias ALIAS Remove an alternate name\n"
|
|
" --list-aliases [MODULE] List recorded aliases\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 log_level_str2enum(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 libdpm-core.so 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_get_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 modules found in %s\n",
|
|
dpm_get_resolved_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;
|
|
}
|
|
|
|
/**
|
|
* @brief Prints the table of recorded aliases
|
|
*
|
|
* @param ctx The libdpm-core.so context
|
|
* @param module NULL for every alias, or a module name to filter by
|
|
* @return 0 on success, 1 on failure
|
|
*/
|
|
int list_aliases(dpm_ctx* ctx, const char* module) {
|
|
dpm_alias_cursor* cur = dpm_list_module_aliases(ctx, module);
|
|
if (!cur) {
|
|
const char* err = dpm_get_last_error(ctx);
|
|
std::fprintf(stderr, "dpm: %s\n", err ? err : "alias listing failed");
|
|
return 1;
|
|
}
|
|
|
|
std::vector<dpm_alias_info> infos;
|
|
dpm_alias_info info;
|
|
while (dpm_alias_cursor_next(cur, &info) == 0) {
|
|
infos.push_back(info);
|
|
}
|
|
|
|
if (infos.empty()) {
|
|
std::printf("No aliases recorded\n");
|
|
dpm_alias_cursor_free(cur);
|
|
return 0;
|
|
}
|
|
|
|
size_t alias_w = std::strlen("ALIAS");
|
|
for (const auto& i : infos) {
|
|
alias_w = std::max(alias_w, std::strlen(i.alias));
|
|
}
|
|
|
|
std::printf("%-*s %s\n", static_cast<int>(alias_w), "ALIAS",
|
|
"MODULE");
|
|
for (const auto& i : infos) {
|
|
std::printf("%-*s %s\n", static_cast<int>(alias_w), i.alias,
|
|
i.module);
|
|
}
|
|
|
|
dpm_alias_cursor_free(cur);
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
dpm_open_overrides overrides = {nullptr, nullptr, nullptr, -1, nullptr};
|
|
bool list = false;
|
|
|
|
/* Holders for --opt=value forms. */
|
|
std::string config_dir;
|
|
std::string module_path;
|
|
std::string metadata_dir;
|
|
std::string root;
|
|
|
|
/* Record operations, each holding what its flag was given. */
|
|
const char* install_module = nullptr;
|
|
const char* uninstall_module = nullptr;
|
|
const char* alias_module = nullptr;
|
|
const char* alias_name = nullptr;
|
|
const char* remove_alias = nullptr;
|
|
const char* list_alias_of = nullptr;
|
|
bool aliases_requested = false;
|
|
|
|
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, "-M", "--metadata-dir", &inline_value)) {
|
|
const char* v = take_value();
|
|
if (!v) { return 1; }
|
|
metadata_dir = v;
|
|
overrides.metadata_dir = metadata_dir.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 = log_level_str2enum(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 (option_matches(arg, "--install-module", "--install-module",
|
|
&inline_value)) {
|
|
install_module = take_value();
|
|
if (!install_module) { return 1; }
|
|
} else if (option_matches(arg, "--uninstall-module",
|
|
"--uninstall-module", &inline_value)) {
|
|
uninstall_module = take_value();
|
|
if (!uninstall_module) { return 1; }
|
|
} else if (std::strcmp(arg, "--add-alias") == 0) {
|
|
if (i + 2 >= argc) {
|
|
std::fprintf(stderr,
|
|
"dpm: --add-alias requires MODULE and ALIAS\n");
|
|
return 1;
|
|
}
|
|
alias_module = argv[++i];
|
|
alias_name = argv[++i];
|
|
} else if (option_matches(arg, "--remove-alias", "--remove-alias",
|
|
&inline_value)) {
|
|
remove_alias = take_value();
|
|
if (!remove_alias) { return 1; }
|
|
} else if (std::strcmp(arg, "--list-aliases") == 0) {
|
|
aliases_requested = true;
|
|
|
|
// The module argument is optional, so a following word is
|
|
// taken only when it is not another option.
|
|
if (i + 1 < argc && argv[i + 1][0] != '-') {
|
|
list_alias_of = argv[++i];
|
|
}
|
|
} 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 (install_module) {
|
|
int rc = dpm_install_module(ctx, install_module);
|
|
if (rc != 0) {
|
|
const char* err = dpm_get_last_error(ctx);
|
|
std::fprintf(stderr, "dpm: %s\n",
|
|
err ? err : "module could not be installed");
|
|
}
|
|
dpm_close(ctx);
|
|
return rc;
|
|
}
|
|
|
|
if (uninstall_module) {
|
|
int rc = dpm_uninstall_module(ctx, uninstall_module);
|
|
if (rc != 0) {
|
|
const char* err = dpm_get_last_error(ctx);
|
|
std::fprintf(stderr, "dpm: %s\n",
|
|
err ? err : "module could not be uninstalled");
|
|
}
|
|
dpm_close(ctx);
|
|
return rc;
|
|
}
|
|
|
|
if (alias_module) {
|
|
int rc = dpm_add_module_alias(ctx, alias_module, alias_name);
|
|
if (rc != 0) {
|
|
const char* err = dpm_get_last_error(ctx);
|
|
std::fprintf(stderr, "dpm: %s\n",
|
|
err ? err : "alias could not be recorded");
|
|
}
|
|
dpm_close(ctx);
|
|
return rc;
|
|
}
|
|
|
|
if (remove_alias) {
|
|
int rc = dpm_remove_module_alias(ctx, remove_alias);
|
|
if (rc != 0) {
|
|
const char* err = dpm_get_last_error(ctx);
|
|
std::fprintf(stderr, "dpm: %s\n",
|
|
err ? err : "alias could not be removed");
|
|
}
|
|
dpm_close(ctx);
|
|
return rc;
|
|
}
|
|
|
|
if (aliases_requested) {
|
|
int rc = list_aliases(ctx, list_alias_of);
|
|
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);
|
|
if (!mod) {
|
|
const char* err = dpm_get_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;
|
|
}
|