Updated and refactored code

- Added `doxygen` API documentation with theme
- Added error-compatible logging function
- Added more error codes
- Added `non_copyable` function
- Added test function from exporting package list
- Changed how errors are handled with returns
- Change `gdpm remote` API to reflect `git`
- Change most functions to accept a vector of arguments instead of a single parameter
- Updated `.gitignore` and `README.md` files
- Fixed issue with `gdpm export` command crashing
This commit is contained in:
David Allen 2023-05-14 19:30:33 -06:00
parent 8e82a22ebd
commit ba23299777
14 changed files with 2950 additions and 93 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@ build/**
builds/**
bin/gdpm
bin/gdpm-tests
docs/doxygen
cache/**
tests/*
!tests/*.cpp

6
.gitmodules vendored Normal file
View file

@ -0,0 +1,6 @@
[submodule "doxygen-awesome-css"]
path = doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git
[submodule "modules/doxygen-awesome-css"]
path = modules/doxygen-awesome-css
url = https://github.com/jothepro/doxygen-awesome-css.git

2736
Doxyfile Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# Godot Package Manager (GDPM)
GDPM is an attempt to make a simple, front-end, command-line, package manager designed to handle assets from the Godot Game Engine's official asset library. It is written in C++ to be lightwight and fast with a few common dependencies found in most Linux distributions and can be used completely independent of Godot. It is designed to add more functionality not included with the official AssetLib with the ability to automate downloads for different platforms. So far, the package manager is capable of searching, downloading, installing, and removing packages and makes managing Godot assets across multiple projects much easier. It stores "packages" in a global directories and is capable of linking or copying global packages to local projects. 
GDPM is an attempt to make a simple, front-end, command-line, package manager designed to handle assets from the Godot Game Engine's official asset library. It is written in C++ to be lightwight and fast with a few common dependencies found in most Linux distributions and can be used completely independent of Godot. It is designed to add more functionality not included with the official AssetLib with the ability to automate downloads for different platforms. So far, the package manager is capable of searching, downloading, installing, and removing packages and makes managing Godot assets across multiple projects much easier. It stores "packages" in a global directories and is capable of linking or copying global packages to local projects.
\*GDPM has not been tested for Windows or Mac.
@ -121,6 +121,12 @@ endif()
| GDPM\_ENABLE\_TIMESTAMP | 0 or 1 (true/false) | Enable/disable timestamp output to stdout. (default: true) |
| GDPM\_TIMESTAMP\_FORMAT | strftime formated string (default: ":%I:%M:%S %p; %Y-%m-%d") | Set the time format to use in logging via strftime(). Uses ISO 8601 date and time standard as default. |
### API Documentation
There is now support for generating API documentation with `doxygen` for the project. Simply run the command in the root directory of the project and documentation will be generated in the `docs/doxygen/` directory. View it by opening the `html/index.html` file in a browser.
NOTE: The API documentation is still a work-in-progress. Over time, the code base will be updated to generate the important parts of the source code.
## Usage Examples
GDPM takes a single command such as install, remove, search, or list followed by a list of package names as the following argument. The command specified is ran then the program exits reporting an errors that may occur. Each command can be modified by specifying additional optional parameters such as '--file' to specify a file to use or '--max-results' to set the number of max results to return from a search.
@ -135,11 +141,14 @@ To see more help information:
gdpm help
```
Packages can be installed using the `install` command and providing a list of comma-delimited package names with no spaces or by providing a one-package-per-line file using the `--file` option. Using the `-y` option will bypass the user prompt. The `--no-sync` option with bypass syncing the local package database with the AssetLib API and use locally stored information only. See the `search` command to find a list of available packages.
Packages can be installed using the `install` command and providing a list of comma-delimited package names with no spaces or by providing a one-package-per-line file using the `--file` option. Additionally, a package list can be exported to reinstall packages using the `install` command. Using the `-y` option will bypass the user prompt. The `--no-sync` option with bypass syncing the local package database with the AssetLib API and use locally stored information only. See the `search` command to find a list of available packages.
```bash
gdpm install "Flappy Godot" "GodotNetworking" -y
gdpm install -f packages.txt --config config.json --no-sync --no-prompt
gdpm export path/to/packages.txt
gdpm install -f path/to/packages.txt --no-sync --no-prompt
```
Packages can be removed similiarly to installing.
@ -182,11 +191,13 @@ gdpm clean "GodotNetworking"
gdpm clean
```
Planned: Add a custom remote AssetLib repository using [this](https://github.com/godotengine/godot-asset-library) API. You can set the priority for each remote repo with the '--set-priority' option or through the 'config.json' file.
Planned: Add a custom remote AssetLib repository using [this](https://github.com/godotengine/godot-asset-library) API. 
Remote repositories can be added and removed similar to Git. Be aware that this API is still in the process of being changed and may not always work as expected.
```bash
gdpm add-remote https://custom-assetlib/asset-library/api --set-priority 0
gdpm delete-remote https://custom-assetlib/asset-library/api
gdpm remote add godot-official https://custom-assetlib/asset-library/api --set-priority 0
gdpm remote remove https://custom-assetlib/asset-library/api
```
Search for available packages from all added repositories. The results can be tweaked using a variety of options like '--sort' or '--support'. See the '--help' command for more details.

1
doxygen-awesome-css Submodule

@ -0,0 +1 @@
Subproject commit 245c7c94c20eac22730ef89035967f78b77bf405

View file

@ -1,12 +1,15 @@
#include <fmt/core.h>
#include <string>
#include "log.hpp"
namespace gdpm::error_codes{
enum {
UNKNOWN = 0,
NOT_FOUND = 1,
NONE = 0,
UNKNOWN = 1,
NOT_FOUND = 2,
FILE_EXISTS = 3,
HOST_UNREACHABLE = 4,
};
inline std::string to_string(int error_code) {
@ -15,7 +18,7 @@ namespace gdpm::error_codes{
};
namespace gdpm{
class error{
class error {
public:
error(int code = 0, const std::string& message = "", bool print_message = false):
m_code(code), m_message(message)
@ -36,4 +39,17 @@ namespace gdpm{
int m_code;
std::string m_message;
};
// Add logging function that can handle error objects
namespace log {
template <typename S, typename...Args>
static constexpr void error(const gdpm::error& e){
#if GDPM_LOG_LEVEL > 1
vlog(
fmt::format(GDPM_COLOR_LOG_ERROR "[ERROR {}" GDPM_COLOR_LOG_RESET, e.get_message()),
fmt::make_format_args("" /*e.get_message()*/)
);
#endif
}
}
}

View file

@ -12,10 +12,30 @@
#include <fmt/printf.h>
#include <fmt/format.h>
/*
TODO: Allow setting the logging *prefix*
TODO: Write log information to file
*/
namespace gdpm::log
{
template <typename...Args> concept RequireMinArgs = requires (std::size_t min){ sizeof...(Args) > min; };
enum level{
NONE,
INFO,
WARNING,
DEBUG,
ERROR
};
struct config {
static int level;
static std::string prefix;
static std::string path;
static bool print_to_stdout;
static bool print_to_stderr;
};
static void vlog(fmt::string_view format, fmt::format_args args){
fmt::vprint(format, args);
}

View file

@ -6,6 +6,7 @@
#include <cxxopts.hpp>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <rapidjson/document.h>
@ -59,6 +60,12 @@ namespace gdpm::package_manager{
none
};
using package_list = std::vector<package_info>;
using package_titles = std::vector<std::string>;
using cl_arg = std::variant<int, bool, float, std::string>;
using cl_args = std::vector<cl_arg>;
using cl_opts = std::unordered_map<std::string, cl_args>;
GDPM_DLL_EXPORT int initialize(int argc, char **argv);
GDPM_DLL_EXPORT int execute();
GDPM_DLL_EXPORT void finalize();
@ -69,17 +76,18 @@ namespace gdpm::package_manager{
GDPM_DLL_EXPORT error remove_all_packages();
GDPM_DLL_EXPORT error update_packages(const std::vector<std::string>& package_titles, bool skip_prompt = false);
GDPM_DLL_EXPORT error search_for_packages(const std::vector<std::string>& package_titles, bool skip_prompt = false);
GDPM_DLL_EXPORT error export_packages(const std::string& path);
GDPM_DLL_EXPORT error export_packages(const std::vector<std::string>& paths);
GDPM_DLL_EXPORT std::vector<std::string> list_information(const std::vector<std::string>& opts, bool print_list = true);
GDPM_DLL_EXPORT void clean_temporary(const std::vector<std::string>& package_titles);
GDPM_DLL_EXPORT void link_packages(const std::vector<std::string>& package_titles, const std::vector<std::string>& paths);
GDPM_DLL_EXPORT void clone_packages(const std::vector<std::string>& package_titles, const std::vector<std::string>& paths);
/* Remote API */
GDPM_DLL_EXPORT void _handle_remote(const std::string& repository);
GDPM_DLL_EXPORT void remote_add_repository(const std::string& repository, ssize_t offset = -1);
GDPM_DLL_EXPORT void remote_remove_respository(const std::string& repository);
GDPM_DLL_EXPORT error _handle_remote(const std::vector<std::string>& args, const std::vector<std::string>& opts);
GDPM_DLL_EXPORT void remote_add_repository(const std::vector<std::string>& repositories);
GDPM_DLL_EXPORT void remote_remove_respository(const std::vector<std::string>& repositories);
GDPM_DLL_EXPORT void remote_remove_respository(ssize_t index);
GDPM_DLL_EXPORT void remote_move_repository(int old_position, int new_position);
/* Auxiliary Functions */
GDPM_DLL_EXPORT cxxargs _parse_arguments(int argc, char **argv);

23
include/types.hpp Normal file
View file

@ -0,0 +1,23 @@
#pragma once
namespace gdpm{
/*
Base class to prevent derived class from creating copies.
*/
class non_copyable{
public:
non_copyable(){}
private:
non_copyable(const non_copyable&);
non_copyable& operator=(const non_copyable&);
};
/*
Base class to prevent derived classes from moving objects.
*/
class non_movable{
non_movable(const non_movable&) = delete;
non_movable(non_movable&&) = delete;
};
}

View file

@ -104,3 +104,19 @@ namespace gdpm::utils{
void delay(std::chrono::milliseconds milliseconds = GDPM_REQUEST_DELAY);
// TODO: Add function to get size of decompressed zip
}
namespace gdpm{
class non_copyable{
public:
non_copyable(){}
private:
non_copyable(const non_copyable&);
non_copyable& operator=(const non_copyable&);
};
class non_movable{
non_movable(const non_movable&) = delete;
non_movable(non_movable&&) = delete;
};
}

@ -0,0 +1 @@
Subproject commit a7f7891706c656903326f79baf74beb2b711688d

View file

@ -87,8 +87,6 @@ namespace gdpm::package_manager{
error install_packages(const std::vector<std::string>& package_titles, bool skip_prompt){
using namespace rapidjson;
params.verbose = config.verbose;
error error;
/* TODO: Need a way to use remote sources from config until none left */
@ -115,11 +113,9 @@ namespace gdpm::package_manager{
/* Found nothing to install so there's nothing to do at this point. */
if(p_found.empty()){
const char *message = "No packages found to install.";
constexpr const char *message = "No packages found to install.";
log::error(message);
error.set_code(-1);
error.set_message(message);
return error;
return error(error_codes::NOT_FOUND, message);
}
log::println("Packages to install: ");
@ -131,7 +127,7 @@ namespace gdpm::package_manager{
if(!skip_prompt){
if(!utils::prompt_user_yn("Do you want to install these packages? (y/n)"))
return error;
return error();
}
using ss_pair = std::pair<std::string, std::string>;
@ -158,10 +154,9 @@ namespace gdpm::package_manager{
params.verbose = config.verbose;
doc = rest_api::get_asset(url, p.asset_id, params);
if(doc.HasParseError() || doc.IsNull()){
log::println("");
log::error("Error parsing HTTP response. (error code: {})", doc.GetParseError());
error.set_code(doc.GetParseError());
return error;
constexpr const char *message = "\nError parsing HTTP response.";
log::error(message);
return error(doc.GetParseError(), message);
}
p.category = doc["category"].GetString();
p.description = doc["description"].GetString();
@ -221,10 +216,9 @@ namespace gdpm::package_manager{
if(response.code == 200){
log::println("Done.");
}else{
log::error("Something went wrong...(code {})", response.code);
error.set_code(response.code);
error.set_message("Error in HTTP response.");
return error;
constexpr const char *message = "Error in HTTP response.";
log::error(message);
return error(response.code, message);
}
}
@ -246,7 +240,7 @@ namespace gdpm::package_manager{
// );
}
return error;
return error();
}
@ -336,14 +330,12 @@ namespace gdpm::package_manager{
/* Get the list of all packages to remove then remove */
std::vector<package_info> p_installed = cache::get_installed_packages();
std::vector<std::string> p_titles = get_package_titles(p_installed);
error error = remove_packages(p_titles);
return error;
return remove_packages(p_titles);
}
error update_packages(const std::vector<std::string>& package_titles, bool skip_prompt){
using namespace rapidjson;
error error;
/* If no package titles provided, update everything and then exit */
if(package_titles.empty()){
@ -351,13 +343,11 @@ namespace gdpm::package_manager{
url += rest_api::endpoints::GET_AssetId;
Document doc = rest_api::get_assets_list(url, params);
if(doc.IsNull()){
std::string message("Could not get response from server. Aborting.");
constexpr const char *message = "Could not get response from server. Aborting.";
log::error(message);
error.set_code(-1);
error.set_message(message);
return error;
return error(error_codes::HOST_UNREACHABLE, message);
}
return error;
return error();
}
/* Fetch remote asset data and compare to see if there are package updates */
@ -382,22 +372,21 @@ namespace gdpm::package_manager{
if(!skip_prompt){
if(!utils::prompt_user_yn("Do you want to update the following packages? (y/n)"))
return error;
return error();
}
remove_packages(p_updates);
install_packages(p_updates);
return error;
return error();
}
error search_for_packages(const std::vector<std::string> &package_titles, bool skip_prompt){
std::vector<package_info> p_cache = cache::get_package_info_by_title(package_titles);
error error;
if(!p_cache.empty() && !config.enable_sync){
print_package_list(p_cache);
return error;
return error();
}
for(const auto& p_title : package_titles){
using namespace rapidjson;
@ -411,39 +400,45 @@ namespace gdpm::package_manager{
request_url += rest_api::endpoints::GET_Asset;
Document doc = rest_api::get_assets_list(request_url, params);
if(doc.IsNull()){
std::string message("Could not search for packages.");
constexpr const char *message = "Could not fetch metadata.";
log::error(message);
error.set_code(-1);
error.set_message(message);
return error;
return error(error_codes::HOST_UNREACHABLE, message);
}
log::info("{} package(s) found...", doc["total_items"].GetInt());
print_package_list(doc);
}
return error;
return error();
}
error export_packages(const std::string& path){
error error;
error export_packages(const std::vector<std::string>& paths){
/* Get all installed package information for export */
std::vector<package_info> p_installed = cache::get_installed_packages();
std::vector<std::string> p_titles = get_package_titles(p_installed);
/* Build string of contents with one package title per line */
std::string output;
std::string output{};
std::for_each(p_titles.begin(), p_titles.end(), [&output](const std::string& p){
output += p + "\n";
});
/* Write contents of installed packages in reusable format */
std::filesystem::path filepath(path);
std::ofstream of(filepath);
of << output;
for(const auto& path : paths ){
std::ofstream of(path);
if(std::filesystem::exists(path)){
constexpr const char *message = "File or directory exists!";
log::error(message);
of.close();
return error(error_codes::FILE_EXISTS, message);
}
log::println("writing contents to file");
of << output;
of.close();
}
return error;
return error();
}
@ -582,26 +577,42 @@ namespace gdpm::package_manager{
}
void _handle_remote(const std::string& repository){
error _handle_remote(const std::vector<std::string>& args, const std::vector<std::string>&){
/* Check if enough arguments are supplied */
size_t argc = args.size();
if (argc < 0){
constexpr const char *message = "No arguments supplied. Aborting.";
log::error(message);
return error(0, message);
}
/* Check which subcommand is supplied */
std::string sub = args.front();
std::vector<std::string> argv(args.begin()+1, args.end());
if(sub == "add") remote_add_repository(argv);
else if (sub == "remove") remote_remove_respository(argv);
else if (sub == "list") print_remote_sources();
else{
constexpr const char *message = "Unknown sub-command. Try 'gdpm help remote' for options.";
log::error(message);
return error(error_codes::UNKNOWN, message);
}
return error();
}
void remote_add_repository(const std::string& repository, ssize_t offset){
auto& s = config.remote_sources;
// auto iter = (offset > 0) ? s.begin() + offset : s.end() - offset;
// config.remote_sources.insert(iter, repository);
config.remote_sources.insert(repository);
void remote_add_repository(const std::vector<std::string>& repos){
std::for_each(repos.begin(), repos.end(), [](const std::string& repo){
config.remote_sources.insert(repo);
});
}
void remote_remove_respository(const std::string& repository){
void remote_remove_respository(const std::vector<std::string>& repos){
auto& s = config.remote_sources;
s.erase(repository);
// std::erase(s, repository);
// (void)std::remove_if(s.begin(), s.end(), [&repository](const std::string& rs){
// return repository == rs;
// });
std::for_each(repos.end(), repos.begin(), [](const std::string& repo){
s.erase(repo);
});
}
@ -612,6 +623,11 @@ namespace gdpm::package_manager{
}
void remote_move_respository(int old_position, int new_position){
}
std::vector<std::string> resolve_dependencies(const std::vector<std::string>& package_titles){
std::vector<std::string> p_deps = {};
std::vector<package_info> p_cache = cache::get_package_info_by_title(package_titles);
@ -819,22 +835,22 @@ namespace gdpm::package_manager{
/* Used to run the command AFTER parsing and setting all command line args. */
void run_command(command_e c, const std::vector<std::string>& package_titles, const std::vector<std::string>& opts){
void run_command(command_e c, const std::vector<std::string>& args, const std::vector<std::string>& opts){
switch(c){
case install: install_packages(package_titles, skip_prompt); break;
case remove: remove_packages(package_titles, skip_prompt); break;
case update: update_packages(package_titles, skip_prompt); break;
case search: search_for_packages(package_titles, skip_prompt); break;
case p_export: export_packages(opts[0]); break;
case list: list_information(package_titles); break;
case install: install_packages(args, skip_prompt); break;
case remove: remove_packages(args, skip_prompt); break;
case update: update_packages(args, skip_prompt); break;
case search: search_for_packages(args, skip_prompt); break;
case p_export: export_packages(args); break;
case list: list_information(args); break;
/* ...opts are the paths here */
case link: link_packages(package_titles, opts); break;
case clone: clone_packages(package_titles, opts); break;
case clean: clean_temporary(package_titles); break;
case sync: synchronize_database(package_titles); break;
case remote: _handle_remote(opts[0]); break;
case help: /* ...runs in handle_arguments() */ break;
case none: /* ...here to run with no command */ break;
case link: link_packages(args, opts); break;
case clone: clone_packages(args, opts); break;
case clean: clean_temporary(args); break;
case sync: synchronize_database(args); break;
case remote: _handle_remote(args, opts); break;
case help: /* ...runs in handle_arguments() */ break;
case none: /* ...here to run with no command */ break;
}
}

View file

@ -179,4 +179,4 @@ namespace gdpm::utils{
sleep_for(millis);
// sleep_until(system_clock::now() + millis);
}
} // namespace towk::utils
} // namespace gdpm::utils

View file

@ -8,7 +8,7 @@
#include <doctest.h>
TEST_SUITE("Test database functions"){
TEST_SUITE("Cache functions"){
TEST_CASE("Test cache database functions"){
gdpm::cache::create_package_database();
@ -16,11 +16,12 @@ TEST_SUITE("Test database functions"){
}
TEST_SUITE("Package manager function"){
TEST_SUITE("Command functions"){
using namespace gdpm;
using namespace gdpm::package_manager;
std::vector<std::string> packages{"ResolutionManagerPlugin","godot-hmac", "Godot"};
config::context config = config::make_context();
std::vector<std::string> packages{"ResolutionManagerPlugin","godot-hmac", "Godot"};
auto check_error = [](const error& error){
if(error.has_error()){
@ -32,20 +33,21 @@ TEST_SUITE("Package manager function"){
TEST_CASE("Test install packages"){
error error = package_manager::install_packages(packages, true);
check_error(error);
check_error(install_packages(packages, true));
}
TEST_CASE("Test searching packages"){
error error = package_manager::search_for_packages(packages, true);
check_error(error);
check_error(search_for_packages(packages, true));
}
TEST_CASE("Test remove packages"){
error error = package_manager::remove_packages(packages, true);
check_error(error);
check_error(remove_packages(packages, true));
}
TEST_CASE("Test exporting installed package list"){
check_error(export_packages({"tests/gdpm/.tmp/packages.txt"}));
}
}