From 29a78724e846895debdcca8a3485d928ca273dff Mon Sep 17 00:00:00 2001 From: modeco80 Date: Fri, 17 Jan 2025 16:03:28 -0500 Subject: [PATCH] tools: Implement new "toollib" This essentially is a even-more refactored version of the new eupak "Task" setup, but done in a generic way, so multiple tools can be written like eupak easily, and I Don't Need to Duplicate It Every Time. This commit also comes with a new test binary, "toollib_test". This will be removed once it's no longer needed and I'm done sketching out the API set (and eupak is refactored to use toollib instead), but for now this provides a two-way thing: - Allows me to hack on toollib without breaking everything - Shows how to use toollib. --- cmake/ProjectFuncs.cmake | 13 ++-- src/tools/CMakeLists.txt | 19 +++++- src/tools/toollib/CMakeLists.txt | 13 ++++ src/tools/toollib/ToolCommand.cpp | 58 +++++++++++++++++ src/tools/toollib/ToolMain.cpp | 52 +++++++++++++++ src/tools/toollib/toollib/ToolCommand.hpp | 77 +++++++++++++++++++++++ src/tools/toollib/toollib/ToolMain.hpp | 38 +++++++++++ src/tools/toollib_test.cpp | 25 ++++++++ src/tools/toollib_test_cmd.cpp | 52 +++++++++++++++ 9 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 src/tools/toollib/CMakeLists.txt create mode 100644 src/tools/toollib/ToolCommand.cpp create mode 100644 src/tools/toollib/ToolMain.cpp create mode 100644 src/tools/toollib/toollib/ToolCommand.hpp create mode 100644 src/tools/toollib/toollib/ToolMain.hpp create mode 100644 src/tools/toollib_test.cpp create mode 100644 src/tools/toollib_test_cmd.cpp diff --git a/cmake/ProjectFuncs.cmake b/cmake/ProjectFuncs.cmake index 9f0d113..dd5ace5 100644 --- a/cmake/ProjectFuncs.cmake +++ b/cmake/ProjectFuncs.cmake @@ -7,11 +7,14 @@ # function(europa_target target) - # Set binary products to output in the build directory for easier access - set_target_properties( - ${target} PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" - ) + # Set binary products to output in the build directory for easier access + set_target_properties( + ${target} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" + ) + + # Require C++20 + target_compile_features(${target} PUBLIC cxx_std_20) endfunction() function(europa_set_alternate_linker) diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index e869065..672e23f 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -20,4 +20,21 @@ add_executable(jsfscramble jsfscramble.cpp) target_link_libraries(jsfscramble PUBLIC europa ) -europa_target(jsfscramble) \ No newline at end of file +europa_target(jsfscramble) + +# Toollib +add_subdirectory(toollib) + +# Temporary test target. +add_executable(toollib_test + toollib_test.cpp + toollib_test_cmd.cpp +) + +target_link_libraries(toollib_test PUBLIC + europa + toollib +) + +europa_target(toollib_test) + diff --git a/src/tools/toollib/CMakeLists.txt b/src/tools/toollib/CMakeLists.txt new file mode 100644 index 0000000..55e99e5 --- /dev/null +++ b/src/tools/toollib/CMakeLists.txt @@ -0,0 +1,13 @@ + +add_library(toollib + ToolCommand.cpp + ToolMain.cpp +) + +target_link_libraries(toollib PUBLIC + argparse::argparse +) + +target_include_directories(toollib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +europa_target(toollib) \ No newline at end of file diff --git a/src/tools/toollib/ToolCommand.cpp b/src/tools/toollib/ToolCommand.cpp new file mode 100644 index 0000000..c4e5cd0 --- /dev/null +++ b/src/tools/toollib/ToolCommand.cpp @@ -0,0 +1,58 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#include +#include +#include + +namespace tool { + + IToolCommandObjectCreator* pToolCommandListHead = nullptr; + + void ToolCommandFactory::RegisterToolCommand(IToolCommandObjectCreator* pCreate) { + if(pToolCommandListHead != nullptr) { + // Find the first linked creator with a + // nullptr next link (the end of the list). + // + // Once we do, attach the command creator + auto* p = pToolCommandListHead; + while(p->next != nullptr) { + p = p->next; + } + + p->next = pCreate; + } else { + pToolCommandListHead = pCreate; + } + } + + IToolCommandObjectCreator* FindCreator(const std::string& name) { + if(!pToolCommandListHead) + return nullptr; + + auto* p = pToolCommandListHead; + while(p != nullptr) { + if(p->name == name) + return p; + + p = p->next; + } + + return nullptr; + } + + std::shared_ptr ToolCommandFactory::CreateNamed(const std::string& name) { + if(auto pCreator = FindCreator(name); pCreator) { + auto cmd = pCreator->Create(); + return cmd; + } else { + throw std::runtime_error(std::format("Invalid tool command \"{}\"", name)); + } + } + +} // namespace tool \ No newline at end of file diff --git a/src/tools/toollib/ToolMain.cpp b/src/tools/toollib/ToolMain.cpp new file mode 100644 index 0000000..1ab1c39 --- /dev/null +++ b/src/tools/toollib/ToolMain.cpp @@ -0,0 +1,52 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#include +#include + +namespace tool { + int ToolMain(const ToolInfo& toolInfo, const ToolMainInput& mainInput) { + // :( Again, FUCK argparse. + argparse::ArgumentParser parser(std::string(toolInfo.name), std::string(toolInfo.version)); + parser.add_description(std::string(toolInfo.description)); + + // Add commands to this parser + for(auto& toolCmds : mainInput.toolCommands) { + toolCmds->Init(parser); + } + + try { + // No command was specified, display the help and then exit with a failure code. + // For some reason the `argparse` library does not have something like this on its own. + // + // I guess it's simple though so I can't really complain that much + if(mainInput.argc == 1) { + auto s = parser.help(); + printf("%s\n", s.str().c_str()); + return 1; + } + + parser.parse_args(mainInput.argc, mainInput.argv); + } catch(std::runtime_error& error) { + std::cout << error.what() << '\n' + << parser; + return 1; + } + + for(auto& toolCmds : mainInput.toolCommands) { + if(toolCmds->ShouldRun(parser)) { + if(auto res = toolCmds->Parse(); res != 0) + return res; + + return toolCmds->Run(); + } + } + + return 0; + } +} // namespace tool \ No newline at end of file diff --git a/src/tools/toollib/toollib/ToolCommand.hpp b/src/tools/toollib/toollib/ToolCommand.hpp new file mode 100644 index 0000000..596537b --- /dev/null +++ b/src/tools/toollib/toollib/ToolCommand.hpp @@ -0,0 +1,77 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#pragma once +#include +#include +#include + +namespace tool { + + /// Base-class for all ToolLib tasks. + struct IToolCommand { + virtual ~IToolCommand() = default; + + /// Do any creation-time initalizatiion. + virtual void Init(argparse::ArgumentParser& parentParser) = 0; + + /// Query if this task has been selected + virtual bool ShouldRun(argparse::ArgumentParser& parentParser) const = 0; + + /// Parse arguments from the user + virtual int Parse() = 0; + + /// Run the task. + virtual int Run() = 0; + }; + + struct IToolCommandObjectCreator { + virtual ~IToolCommandObjectCreator() = default; + + /// Creates the IToolCommand. + virtual std::shared_ptr Create() = 0; + + // dont touch :) + IToolCommandObjectCreator* next; + std::string name; + }; + + /// Creates IToolCommand instances for clients. + struct ToolCommandFactory { + using FactoryMethod = std::shared_ptr (*)(); + + /// Creates a task. + static std::shared_ptr CreateNamed(const std::string& name); + + private: + template + friend struct ToolCommandRegister; + + static void RegisterToolCommand(IToolCommandObjectCreator* pCreate); + }; + + /// Helper template to register into the [ToolCommandFactory]. + template + struct ToolCommandRegister : IToolCommandObjectCreator { + ToolCommandRegister(const std::string& name) { + static_assert(std::is_base_of_v, "what you doing sir."); + this->name = name; + ToolCommandFactory::RegisterToolCommand(this); + } + + std::shared_ptr Create() override { + return std::make_shared(); + } + }; + + /// Registers a tool command. + /// Should be put in the .cpp implementation source file of the tool command itself. +#define TOOLLIB_REGISTER_TOOLCOMMAND(Name, TTask) \ + static ::tool::ToolCommandRegister __register__##TTask(Name) + +} // namespace tool \ No newline at end of file diff --git a/src/tools/toollib/toollib/ToolMain.hpp b/src/tools/toollib/toollib/ToolMain.hpp new file mode 100644 index 0000000..ec11800 --- /dev/null +++ b/src/tools/toollib/toollib/ToolMain.hpp @@ -0,0 +1,38 @@ +// +// EuropaTools +// +// (C) 2021-2025 modeco80 +// +// SPDX-License-Identifier: MIT +// + +#pragma once + +#include +#include +#include + +namespace tool { + + struct IToolCommand; + + struct ToolInfo { + std::string_view name; // "Eupak" + std::string_view version; // v1.0.0 + std::string_view description; // "bla" + // FIXME: (authors?) + }; + + struct ToolMainInput { + /// Tool commands to run. + std::span> toolCommands; + + // C arguments + int argc; + char** argv; + }; + + /// The shared toollib main. When in doubt, use this. + int ToolMain(const ToolInfo& toolInfo, const ToolMainInput& mainInput); + +} // namespace tool \ No newline at end of file diff --git a/src/tools/toollib_test.cpp b/src/tools/toollib_test.cpp new file mode 100644 index 0000000..fe424bc --- /dev/null +++ b/src/tools/toollib_test.cpp @@ -0,0 +1,25 @@ + + +// Test/excercise of new toollib system. + +#include +#include +#include + +int main(int argc, char** argv) { + auto tasks = std::vector { + tool::ToolCommandFactory::CreateNamed("test") + }; + + // clang-format off + return tool::ToolMain(tool::ToolInfo { + .name = "toollib_test", + .version = "0.0.1", + .description = "A test/excercise of the new toollib APIs." + }, tool::ToolMainInput { + .toolCommands = tasks, + .argc = argc, + .argv = argv + }); + // clang-format on +} \ No newline at end of file diff --git a/src/tools/toollib_test_cmd.cpp b/src/tools/toollib_test_cmd.cpp new file mode 100644 index 0000000..e61bd78 --- /dev/null +++ b/src/tools/toollib_test_cmd.cpp @@ -0,0 +1,52 @@ + +#include + +#include "argparse/argparse.hpp" + +struct TestCmd : tool::IToolCommand { + TestCmd() + : parser("test", "", argparse::default_arguments::help) { + // clang-format off + parser + .add_description("Does something cool"); + + + // FIXME: Probably just print this always, in a thinner format, but use + // the existing thicker format for verbosity. + parser + .add_argument("--verbose") + .help("Increase information output verbosity") + .default_value(false) + .implicit_value(true); + // clang-format on + } + + void Init(argparse::ArgumentParser& parent) override { + parent.add_subparser(parser); + } + + bool ShouldRun(argparse::ArgumentParser& parent) const override { + return parent.is_subcommand_used("test"); + } + + int Parse() override { + verbose = parser.get("--verbose"); + return 0; + } + + int Run() override { + std::printf("Pretend this does something useful\n"); + + if(verbose) + std::printf("I printed you a cake! it might be a lie thogh x3\n"); + return 0; + } + + private: + argparse::ArgumentParser parser; + + // Parsed arguments + bool verbose = false; +}; + +TOOLLIB_REGISTER_TOOLCOMMAND("test", TestCmd); \ No newline at end of file