/** * @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 * * 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 . */ #include #include #include #include #include #include /** * @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 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 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(name_w), "MODULE", static_cast(version_w), "VERSION", "DESCRIPTION"); for (const auto& i : infos) { std::printf("%-*s %-*s %s\n", static_cast(name_w), i.name, static_cast(version_w), i.version, i.description); } std::printf("\nUse 'dpm 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 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(alias_w), "ALIAS", "MODULE"); for (const auto& i : infos) { std::printf("%-*s %s\n", static_cast(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; }