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.
This commit is contained in:
Lily Tsuru 2025-01-17 16:03:28 -05:00
parent 6a565e3244
commit 29a78724e8
9 changed files with 341 additions and 6 deletions

View file

@ -7,11 +7,14 @@
# #
function(europa_target target) function(europa_target target)
# Set binary products to output in the build directory for easier access # Set binary products to output in the build directory for easier access
set_target_properties( set_target_properties(
${target} PROPERTIES ${target} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}"
) )
# Require C++20
target_compile_features(${target} PUBLIC cxx_std_20)
endfunction() endfunction()
function(europa_set_alternate_linker) function(europa_set_alternate_linker)

View file

@ -20,4 +20,21 @@ add_executable(jsfscramble jsfscramble.cpp)
target_link_libraries(jsfscramble PUBLIC target_link_libraries(jsfscramble PUBLIC
europa europa
) )
europa_target(jsfscramble) 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)

View file

@ -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)

View file

@ -0,0 +1,58 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <format>
#include <stdexcept>
#include <toollib/ToolCommand.hpp>
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<IToolCommand> 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

View file

@ -0,0 +1,52 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#include <toollib/ToolCommand.hpp>
#include <toollib/ToolMain.hpp>
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

View file

@ -0,0 +1,77 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#pragma once
#include <argparse/argparse.hpp>
#include <memory>
#include <type_traits>
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<IToolCommand> Create() = 0;
// dont touch :)
IToolCommandObjectCreator* next;
std::string name;
};
/// Creates IToolCommand instances for clients.
struct ToolCommandFactory {
using FactoryMethod = std::shared_ptr<IToolCommand> (*)();
/// Creates a task.
static std::shared_ptr<IToolCommand> CreateNamed(const std::string& name);
private:
template <class T>
friend struct ToolCommandRegister;
static void RegisterToolCommand(IToolCommandObjectCreator* pCreate);
};
/// Helper template to register into the [ToolCommandFactory].
template <class T>
struct ToolCommandRegister : IToolCommandObjectCreator {
ToolCommandRegister(const std::string& name) {
static_assert(std::is_base_of_v<IToolCommand, T>, "what you doing sir.");
this->name = name;
ToolCommandFactory::RegisterToolCommand(this);
}
std::shared_ptr<IToolCommand> Create() override {
return std::make_shared<T>();
}
};
/// 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<TTask> __register__##TTask(Name)
} // namespace tool

View file

@ -0,0 +1,38 @@
//
// EuropaTools
//
// (C) 2021-2025 modeco80 <lily.modeco80@protonmail.ch>
//
// SPDX-License-Identifier: MIT
//
#pragma once
#include <memory>
#include <span>
#include <string_view>
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<std::shared_ptr<IToolCommand>> 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

View file

@ -0,0 +1,25 @@
// Test/excercise of new toollib system.
#include <argparse/argparse.hpp>
#include <toollib/ToolCommand.hpp>
#include <toollib/ToolMain.hpp>
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
}

View file

@ -0,0 +1,52 @@
#include <toollib/ToolCommand.hpp>
#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<bool>("--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);