Added mod manifest generation to mod tool

This commit is contained in:
Mr-Wiseguy 2024-09-08 00:11:00 -04:00
parent b8dcb21dec
commit e81a8526ee

View file

@ -5,17 +5,35 @@
#include <numeric> #include <numeric>
#include <cctype> #include <cctype>
#include "fmt/format.h" #include "fmt/format.h"
#include "fmt/ostream.h"
#include "n64recomp.h" #include "n64recomp.h"
#include <toml++/toml.hpp> #include <toml++/toml.hpp>
struct ModConfig { constexpr std::string_view symbol_filename = "mod_syms.bin";
std::filesystem::path output_syms_path; constexpr std::string_view binary_filename = "mod_binary.bin";
std::filesystem::path output_binary_path; constexpr std::string_view manifest_filename = "manifest.json";
struct ModManifest {
std::string mod_id;
std::string version_string;
std::vector<std::string> authors;
std::string game_id;
std::string minimum_recomp_version;
std::unordered_map<std::string, std::vector<std::string>> native_libraries;
std::vector<std::string> dependencies;
std::vector<std::string> full_dependency_strings;
};
struct ModInputs {
std::filesystem::path elf_path; std::filesystem::path elf_path;
std::filesystem::path func_reference_syms_file_path; std::filesystem::path func_reference_syms_file_path;
std::vector<std::filesystem::path> data_reference_syms_file_paths; std::vector<std::filesystem::path> data_reference_syms_file_paths;
std::vector<std::string> dependencies; std::vector<std::filesystem::path> additional_files;
std::vector<std::string> full_dependency_strings; };
struct ModConfig {
ModManifest manifest;
ModInputs inputs;
}; };
static std::filesystem::path concat_if_not_empty(const std::filesystem::path& parent, const std::filesystem::path& child) { static std::filesystem::path concat_if_not_empty(const std::filesystem::path& parent, const std::filesystem::path& child) {
@ -25,14 +43,13 @@ static std::filesystem::path concat_if_not_empty(const std::filesystem::path& pa
return child; return child;
} }
static bool validate_version_string(std::string_view str) { static bool validate_version_string(std::string_view str, bool& has_label) {
std::array<size_t, 2> period_indices; std::array<size_t, 2> period_indices;
size_t num_periods = 0; size_t num_periods = 0;
size_t cur_pos = 0; size_t cur_pos = 0;
uint16_t major; uint16_t major;
uint16_t minor; uint16_t minor;
uint16_t patch; uint16_t patch;
std::string suffix;
// Find the 2 required periods. // Find the 2 required periods.
cur_pos = str.find('.', cur_pos); cur_pos = str.find('.', cur_pos);
@ -69,19 +86,20 @@ static bool validate_version_string(std::string_view str) {
// Allow a plus or minus directly after the third number. // Allow a plus or minus directly after the third number.
if (parse_results[2].ptr != str.data() + parse_ends[2]) { if (parse_results[2].ptr != str.data() + parse_ends[2]) {
if (*parse_results[2].ptr == '+' || *parse_results[2].ptr == '-') { has_label = true;
suffix = str.substr(std::distance(str.data(), parse_results[2].ptr)); if (*parse_results[2].ptr != '+' && *parse_results[2].ptr != '-') {
} // Failed to parse, as nothing is allowed directly after the last number besides a plus or minus.
// Failed to parse, as nothing is allowed directly after the last number besides a plus or minus.
else {
return false; return false;
} }
} }
else {
has_label = false;
}
return true; return true;
} }
static bool validate_dependency_string(const std::string& val, size_t& name_length) { static bool validate_dependency_string(const std::string& val, size_t& name_length, bool& has_label) {
std::string ret; std::string ret;
size_t name_length_temp; size_t name_length_temp;
@ -100,6 +118,7 @@ static bool validate_dependency_string(const std::string& val, size_t& name_leng
validated_name = N64Recomp::validate_mod_id(std::string_view{val}); validated_name = N64Recomp::validate_mod_id(std::string_view{val});
name_length_temp = val.size(); name_length_temp = val.size();
validated_version = true; validated_version = true;
has_label = false;
} }
else { else {
// Version present, validate it. // Version present, validate it.
@ -113,7 +132,7 @@ static bool validate_dependency_string(const std::string& val, size_t& name_leng
// Validate the dependency's id and version. // Validate the dependency's id and version.
validated_name = N64Recomp::validate_mod_id(std::string_view{val.begin(), val.begin() + colon_pos}); validated_name = N64Recomp::validate_mod_id(std::string_view{val.begin(), val.begin() + colon_pos});
validated_version = validate_version_string(std::string_view{val.begin() + colon_pos + 1, val.end()}); validated_version = validate_version_string(std::string_view{val.begin() + colon_pos + 1, val.end()}, has_label);
} }
if (validated_name && validated_version) { if (validated_name && validated_version) {
@ -124,6 +143,48 @@ static bool validate_dependency_string(const std::string& val, size_t& name_leng
return false; return false;
} }
template <typename T>
static T read_toml_value(const toml::table& data, std::string_view key, bool required) {
const toml::node* value_node = data.get(key);
if (value_node == nullptr) {
if (required) {
throw toml::parse_error(("Missing required field " + std::string{key}).c_str(), data.source());
}
else {
return T{};
}
}
std::optional<T> opt = value_node->value_exact<T>();
if (opt.has_value()) {
return opt.value();
}
else {
throw toml::parse_error(("Incorrect type for field " + std::string{key}).c_str(), data.source());
}
}
static const toml::array& read_toml_array(const toml::table& data, std::string_view key, bool required) {
static const toml::array empty_array = toml::array{};
const toml::node* value_node = data.get(key);
if (value_node == nullptr) {
if (required) {
throw toml::parse_error(("Missing required field " + std::string{ key }).c_str(), data.source());
}
else {
return empty_array;
}
}
if (!value_node->is_array()) {
throw toml::parse_error(("Incorrect type for field " + std::string{ key }).c_str(), value_node->source());
}
return *value_node->as_array();
}
static std::vector<std::filesystem::path> get_toml_path_array(const toml::array* toml_array, const std::filesystem::path& basedir) { static std::vector<std::filesystem::path> get_toml_path_array(const toml::array* toml_array, const std::filesystem::path& basedir) {
std::vector<std::filesystem::path> ret; std::vector<std::filesystem::path> ret;
@ -141,6 +202,135 @@ static std::vector<std::filesystem::path> get_toml_path_array(const toml::array*
return ret; return ret;
} }
ModManifest parse_mod_config_manifest(const std::filesystem::path& basedir, const toml::table& manifest_table) {
ModManifest ret;
// Mod ID
ret.mod_id = read_toml_value<std::string_view>(manifest_table, "id", true);
// Mod version
ret.version_string = read_toml_value<std::string_view>(manifest_table, "version", true);
bool version_has_label;
if (!validate_version_string(ret.version_string, version_has_label)) {
throw toml::parse_error("Invalid mod version", manifest_table["version"].node()->source());
}
// Authors
const toml::array& authors_array = read_toml_array(manifest_table, "authors", true);
authors_array.for_each([&ret](auto&& el) {
if constexpr (toml::is_string<decltype(el)>) {
ret.authors.emplace_back(el.ref<std::string>());
}
else {
throw toml::parse_error("Invalid type for author entry", el.source());
}
});
// Game ID
ret.game_id = read_toml_value<std::string_view>(manifest_table, "game_id", true);
// Minimum recomp version
ret.minimum_recomp_version = read_toml_value<std::string_view>(manifest_table, "minimum_recomp_version", true);
bool minimum_recomp_version_has_label;
if (!validate_version_string(ret.minimum_recomp_version, minimum_recomp_version_has_label)) {
throw toml::parse_error("Invalid minimum recomp version", manifest_table["minimum_recomp_version"].node()->source());
}
if (minimum_recomp_version_has_label) {
throw toml::parse_error("Minimum recomp version may not have a label", manifest_table["minimum_recomp_version"].node()->source());
}
// Native libraries (optional)
const toml::array& native_libraries = read_toml_array(manifest_table, "native_libraries", false);
if (!native_libraries.empty()) {
native_libraries.for_each([&ret](const auto& el) {
if constexpr (toml::is_table<decltype(el)>) {
const toml::table& el_table = *el.as_table();
std::string_view library_name = read_toml_value<std::string_view>(el_table, "name", true);
const toml::array funcs_array = read_toml_array(el_table, "funcs", true);
std::vector<std::string> cur_funcs{};
funcs_array.for_each([&ret, &cur_funcs](const auto& func_el) {
if constexpr (toml::is_string<decltype(func_el)>) {
cur_funcs.emplace_back(func_el.ref<std::string>());
}
else {
throw toml::parse_error("Invalid type for native library function entry", func_el.source());
}
});
ret.native_libraries.emplace(std::string{library_name}, std::move(cur_funcs));
}
else {
throw toml::parse_error("Invalid type for native library entry", el.source());
}
});
}
// Dependency list (optional)
const toml::array& dependency_array = read_toml_array(manifest_table, "dependencies", false);
if (!dependency_array.empty()) {
// Reserve room for all the dependencies.
ret.dependencies.reserve(dependency_array.size());
dependency_array.for_each([&ret](const auto& el) {
if constexpr (toml::is_string<decltype(el)>) {
size_t dependency_id_length;
bool dependency_version_has_label;
if (!validate_dependency_string(el.ref<std::string>(), dependency_id_length, dependency_version_has_label)) {
throw toml::parse_error("Invalid dependency entry", el.source());
}
if (dependency_version_has_label) {
throw toml::parse_error("Dependency versions may not have labels", el.source());
}
std::string dependency_id = el.ref<std::string>().substr(0, dependency_id_length);
ret.dependencies.emplace_back(dependency_id);
ret.full_dependency_strings.emplace_back(el.ref<std::string>());
}
else {
throw toml::parse_error("Invalid type for dependency entry", el.source());
}
});
}
return ret;
}
ModInputs parse_mod_config_inputs(const std::filesystem::path& basedir, const toml::table& inputs_table) {
ModInputs ret;
// Elf file
std::optional<std::string> elf_path_opt = inputs_table["elf_path"].value<std::string>();
if (elf_path_opt.has_value()) {
ret.elf_path = concat_if_not_empty(basedir, elf_path_opt.value());
}
else {
throw toml::parse_error("Mod toml input section is missing elf file", inputs_table.source());
}
// Function reference symbols file
std::optional<std::string> func_reference_syms_file_opt = inputs_table["func_reference_syms_file"].value<std::string>();
if (func_reference_syms_file_opt.has_value()) {
ret.func_reference_syms_file_path = concat_if_not_empty(basedir, func_reference_syms_file_opt.value());
}
else {
throw toml::parse_error("Mod toml input section is missing function reference symbol file", inputs_table.source());
}
// Data reference symbols files
toml::node_view data_reference_syms_file_data = inputs_table["data_reference_syms_files"];
if (data_reference_syms_file_data.is_array()) {
const toml::array* array = data_reference_syms_file_data.as_array();
ret.data_reference_syms_file_paths = get_toml_path_array(array, basedir);
}
else {
if (data_reference_syms_file_data) {
throw toml::parse_error("Mod toml input section is missing data reference symbol file list", inputs_table.source());
}
else {
throw toml::parse_error("Invalid data reference symbol file list", data_reference_syms_file_data.node()->source());
}
}
return ret;
}
ModConfig parse_mod_config(const std::filesystem::path& config_path, bool& good) { ModConfig parse_mod_config(const std::filesystem::path& config_path, bool& good) {
ModConfig ret{}; ModConfig ret{};
good = false; good = false;
@ -151,83 +341,30 @@ ModConfig parse_mod_config(const std::filesystem::path& config_path, bool& good)
toml_data = toml::parse_file(config_path.native()); toml_data = toml::parse_file(config_path.native());
std::filesystem::path basedir = config_path.parent_path(); std::filesystem::path basedir = config_path.parent_path();
const auto config_data = toml_data["config"]; // Find the manifest section and validate its type.
const toml::node* manifest_data_ptr = toml_data.get("manifest");
if (manifest_data_ptr == nullptr) {
throw toml::parse_error("Mod toml is missing manifest section", {});
}
if (!manifest_data_ptr->is_table()) {
throw toml::parse_error("Incorrect type for mod toml manifest section", manifest_data_ptr->source());
}
const toml::table& manifest_table = *manifest_data_ptr->as_table();
// Output symbol file path // Find the inputs section and validate its type.
std::optional<std::string> output_syms_path_opt = config_data["output_syms_path"].value<std::string>(); const toml::node* inputs_data_ptr = toml_data.get("inputs");
if (output_syms_path_opt.has_value()) { if (inputs_data_ptr == nullptr) {
ret.output_syms_path = concat_if_not_empty(basedir, output_syms_path_opt.value()); throw toml::parse_error("Mod toml is missing inputs section", {});
} }
else { if (!inputs_data_ptr->is_table()) {
throw toml::parse_error("Mod toml is missing output symbol file path", config_data.node()->source()); throw toml::parse_error("Incorrect type for mod toml inputs section", inputs_data_ptr->source());
} }
const toml::table& inputs_table = *inputs_data_ptr->as_table();
// Output binary file path // Parse the manifest.
std::optional<std::string> output_binary_path_opt = config_data["output_binary_path"].value<std::string>(); ret.manifest = parse_mod_config_manifest(basedir, manifest_table);
if (output_binary_path_opt.has_value()) { // Parse the inputs.
ret.output_binary_path = concat_if_not_empty(basedir, output_binary_path_opt.value()); ret.inputs = parse_mod_config_inputs(basedir, inputs_table);
}
else {
throw toml::parse_error("Mod toml is missing output binary file path", config_data.node()->source());
}
// Elf file
std::optional<std::string> elf_path_opt = config_data["elf_path"].value<std::string>();
if (elf_path_opt.has_value()) {
ret.elf_path = concat_if_not_empty(basedir, elf_path_opt.value());
}
else {
throw toml::parse_error("Mod toml is missing elf file", config_data.node()->source());
}
// Function reference symbols file
std::optional<std::string> func_reference_syms_file_opt = config_data["func_reference_syms_file"].value<std::string>();
if (func_reference_syms_file_opt.has_value()) {
ret.func_reference_syms_file_path = concat_if_not_empty(basedir, func_reference_syms_file_opt.value());
}
else {
throw toml::parse_error("Mod toml is missing function reference symbol file", config_data.node()->source());
}
// Data reference symbols files
toml::node_view data_reference_syms_file_data = config_data["data_reference_syms_files"];
if (data_reference_syms_file_data.is_array()) {
const toml::array* array = data_reference_syms_file_data.as_array();
ret.data_reference_syms_file_paths = get_toml_path_array(array, basedir);
}
else {
if (data_reference_syms_file_data) {
throw toml::parse_error("Mod toml is missing data reference symbol file list", config_data.node()->source());
}
else {
throw toml::parse_error("Invalid data reference symbol file list", data_reference_syms_file_data.node()->source());
}
}
// Dependency list (optional)
toml::node_view dependency_data = config_data["dependencies"];
if (dependency_data.is_array()) {
const toml::array* dependency_array = dependency_data.as_array();
// Reserve room for all the dependencies.
ret.dependencies.reserve(dependency_array->size());
dependency_array->for_each([&ret](auto&& el) {
if constexpr (toml::is_string<decltype(el)>) {
size_t dependency_id_length;
if (!validate_dependency_string(el.ref<std::string>(), dependency_id_length)) {
throw toml::parse_error("Invalid dependency entry", el.source());
}
std::string dependency_id = el.ref<std::string>().substr(0, dependency_id_length);
ret.dependencies.emplace_back(dependency_id);
ret.full_dependency_strings.emplace_back(el.ref<std::string>());
}
else {
throw toml::parse_error("Invalid toml type for dependency", el.source());
}
});
}
else if (dependency_data) {
throw toml::parse_error("Invalid mod dependency list", dependency_data.node()->source());
}
} }
catch (const toml::parse_error& err) { catch (const toml::parse_error& err) {
std::cerr << "Syntax error parsing toml: " << *err.source().path << " (" << err.source().begin << "):\n" << err.description() << std::endl; std::cerr << "Syntax error parsing toml: " << *err.source().path << " (" << err.source().begin << "):\n" << err.description() << std::endl;
@ -261,6 +398,59 @@ bool parse_callback_name(std::string_view data, std::string& dependency_name, st
return true; return true;
} }
void print_vector_elements(std::ostream& output_file, const std::vector<std::string>& vec, bool compact) {
char separator = compact ? ' ' : '\n';
for (size_t i = 0; i < vec.size(); i++) {
const std::string& val = vec[i];
fmt::print(output_file, "{}\"{}\"{}{}",
compact ? "" : " ", val, i == vec.size() - 1 ? "" : ",", separator);
}
}
void write_manifest(const std::filesystem::path& path, const ModManifest& manifest) {
std::ofstream output_file(path);
fmt::print(output_file,
"{{\n"
" \"game_id\": \"{}\",\n"
" \"id\": \"{}\",\n"
" \"version\": \"{}\",\n"
" \"authors\": [\n",
manifest.game_id, manifest.mod_id, manifest.version_string);
print_vector_elements(output_file, manifest.authors, false);
fmt::print(output_file,
" ],\n"
" \"minimum_recomp_version\": \"{}\"",
manifest.minimum_recomp_version);
if (!manifest.native_libraries.empty()) {
fmt::print(output_file, ",\n"
" \"native_libraries\": {{\n");
size_t library_index = 0;
for (const auto& [library, funcs] : manifest.native_libraries) {
fmt::print(output_file, " \"{}\": [ ",
library);
print_vector_elements(output_file, funcs, true);
fmt::print(output_file, "]{}\n",
library_index == manifest.native_libraries.size() - 1 ? "" : ",");
library_index++;
}
fmt::print(output_file, " }}");
}
if (!manifest.full_dependency_strings.empty()) {
fmt::print(output_file, ",\n"
" \"dependencies\": [\n");
print_vector_elements(output_file, manifest.full_dependency_strings, false);
fmt::print(output_file, " ]");
}
fmt::print(output_file, "\n}}\n");
}
N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bool& good) { N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bool& good) {
N64Recomp::Context ret{}; N64Recomp::Context ret{};
good = false; good = false;
@ -689,12 +879,24 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo
} }
int main(int argc, const char** argv) { int main(int argc, const char** argv) {
if (argc != 2) { if (argc != 3) {
fmt::print("Usage: {} [mod toml]\n", argv[0]); fmt::print("Usage: {} [mod toml] [output folder]\n", argv[0]);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
bool config_good; bool config_good;
std::filesystem::path output_dir{ argv[2] };
if (!std::filesystem::exists(output_dir)) {
fmt::print(stderr, "Specified output folder does not exist!\n");
return EXIT_FAILURE;
}
if (!std::filesystem::is_directory(output_dir)) {
fmt::print(stderr, "Specified output folder is not a folder!\n");
return EXIT_FAILURE;
}
ModConfig config = parse_mod_config(argv[1], config_good); ModConfig config = parse_mod_config(argv[1], config_good);
if (!config_good) { if (!config_good) {
@ -709,7 +911,7 @@ int main(int argc, const char** argv) {
// Create a new temporary context to read the function reference symbol file into, since it's the same format as the recompilation symbol file. // Create a new temporary context to read the function reference symbol file into, since it's the same format as the recompilation symbol file.
std::vector<uint8_t> dummy_rom{}; std::vector<uint8_t> dummy_rom{};
N64Recomp::Context reference_context{}; N64Recomp::Context reference_context{};
if (!N64Recomp::Context::from_symbol_file(config.func_reference_syms_file_path, std::move(dummy_rom), reference_context, false)) { if (!N64Recomp::Context::from_symbol_file(config.inputs.func_reference_syms_file_path, std::move(dummy_rom), reference_context, false)) {
fmt::print(stderr, "Failed to load provided function reference symbol file\n"); fmt::print(stderr, "Failed to load provided function reference symbol file\n");
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -721,7 +923,7 @@ int main(int argc, const char** argv) {
} }
} }
for (const std::filesystem::path& cur_data_sym_path : config.data_reference_syms_file_paths) { for (const std::filesystem::path& cur_data_sym_path : config.inputs.data_reference_syms_file_paths) {
if (!context.read_data_reference_syms(cur_data_sym_path)) { if (!context.read_data_reference_syms(cur_data_sym_path)) {
fmt::print(stderr, "Failed to load provided data reference symbol file: {}\n", cur_data_sym_path.string()); fmt::print(stderr, "Failed to load provided data reference symbol file: {}\n", cur_data_sym_path.string());
return EXIT_FAILURE; return EXIT_FAILURE;
@ -729,7 +931,7 @@ int main(int argc, const char** argv) {
} }
// Copy the dependencies from the config into the context. // Copy the dependencies from the config into the context.
context.add_dependencies(config.dependencies); context.add_dependencies(config.manifest.dependencies);
N64Recomp::ElfParsingConfig elf_config { N64Recomp::ElfParsingConfig elf_config {
.bss_section_suffix = {}, .bss_section_suffix = {},
@ -743,7 +945,7 @@ int main(int argc, const char** argv) {
}; };
bool dummy_found_entrypoint; bool dummy_found_entrypoint;
N64Recomp::DataSymbolMap dummy_syms_map; N64Recomp::DataSymbolMap dummy_syms_map;
bool elf_good = N64Recomp::Context::from_elf_file(config.elf_path, context, elf_config, false, dummy_syms_map, dummy_found_entrypoint); bool elf_good = N64Recomp::Context::from_elf_file(config.inputs.elf_path, context, elf_config, false, dummy_syms_map, dummy_found_entrypoint);
if (!elf_good) { if (!elf_good) {
fmt::print(stderr, "Failed to parse mod elf\n"); fmt::print(stderr, "Failed to parse mod elf\n");
@ -763,11 +965,24 @@ int main(int argc, const char** argv) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
std::ofstream output_syms_file{ config.output_syms_path, std::ios::binary }; std::filesystem::path output_syms_path = output_dir / symbol_filename;
output_syms_file.write(reinterpret_cast<const char*>(symbols_bin.data()), symbols_bin.size()); std::filesystem::path output_binary_path = output_dir / binary_filename;
std::filesystem::path output_manifest_path = output_dir / manifest_filename;
std::ofstream output_binary_file{ config.output_binary_path, std::ios::binary }; // Write the symbol file.
output_binary_file.write(reinterpret_cast<const char*>(mod_context.rom.data()), mod_context.rom.size()); {
std::ofstream output_syms_file{ output_syms_path, std::ios::binary };
output_syms_file.write(reinterpret_cast<const char*>(symbols_bin.data()), symbols_bin.size());
}
// Write the binary file.
{
std::ofstream output_binary_file{ output_binary_path, std::ios::binary };
output_binary_file.write(reinterpret_cast<const char*>(mod_context.rom.data()), mod_context.rom.size());
}
// Write the manifest.
write_manifest(output_manifest_path, config.manifest);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }