diff --git a/icons/translations.png b/icons/translations.png new file mode 100644 index 0000000..3664bd7 Binary files /dev/null and b/icons/translations.png differ diff --git a/mob.ini b/mob.ini index b32c74d..0f81c0d 100644 --- a/mob.ini +++ b/mob.ini @@ -18,9 +18,13 @@ install_message = never host = [aliases] -super = cmake_common modorganizer* githubpp +super = cmake_common modorganizer* plugins = check_fnis bsapacker bsa_extractor diagnose_basic installer_* plugin_python preview_base preview_bsa tool_* game_* +[translations] +mo2-translations = organizer uibase plugin_python +mo2-game-bethesda = game_creation game_enderal game_enderalse game_fallout3 game_fallout4 game_fallout4vr game_falloutNV game_gamebryo game_morrowind game_nehrim game_oblivion game_skyrim game_skyrimse game_skyrimvr game_ttw + [task] enabled = true mo_org = ModOrganizer2 @@ -139,6 +143,7 @@ python = release third_party = prefix = cache = +icons = patches = licenses = build = @@ -150,11 +155,12 @@ install_pdbs = install_dlls = install_loot = install_plugins = +install_extensions = install_stylesheets = install_licenses = install_pythoncore = -install_translations = vs = +vcpkg = qt_install = qt_bin = qt_translations = diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b87b84b..67237f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ file(GLOB_RECURSE source_files *.cpp) file(GLOB_RECURSE header_files *.h) add_executable(mob ${source_files} ${header_files}) -set_target_properties(mob PROPERTIES CXX_STANDARD 20) +set_target_properties(mob PROPERTIES CXX_STANDARD 23) target_compile_definitions(mob PUBLIC NOMINMAX) target_compile_options(mob PUBLIC "/MT") diff --git a/src/cmd/cmake.cpp b/src/cmd/cmake.cpp deleted file mode 100644 index f6ef356..0000000 --- a/src/cmd/cmake.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "pch.h" -#include "../tasks/tasks.h" -#include "commands.h" - -namespace mob { - - cmake_command::cmake_command() : command(requires_options) {} - - command::meta_t cmake_command::meta() const - { - return {"cmake", "runs cmake in a directory"}; - } - - clipp::group cmake_command::do_group() - { - return clipp::group( - clipp::command("cmake").set(picked_), - - (clipp::option("-h", "--help") >> help_) % "shows this message", - - (clipp::option("-G", "--generator") & clipp::value("GEN") >> gen_) % - ("sets the -G option for cmake [default: VS]"), - - (clipp::option("-c", "--cmd") & clipp::value("CMD") >> cmd_) % - "overrides the cmake command line [default: \"..\"]", - - (clipp::option("--x64").set(x64_, true) | - clipp::option("--x86").set(x64_, false)) % - "whether to use the x64 or x86 vcvars; if -G is not set, " - "whether to pass \"-A Win32\" or \"-A x64\" for the default " - "VS generator [default: x64]", - - (clipp::option("--install-prefix") & clipp::value("PATH") >> prefix_) % - "sets CMAKE_INSTALL_PREFIX [default: empty]", - - (clipp::option("-d", "--debug").set(debug_, true)) % - "whether to configure for debug mode [default: false]", - - (clipp::value("PATH") >> path_) % "path from which to run `cmake`"); - } - - int cmake_command::do_run() - { - auto t = tasks::modorganizer::create_cmake_tool( - fs::path(utf8_to_utf16(path_)), mob::cmake::generate, - debug_ ? config::debug : config::relwithdebinfo); - - t.generator(gen_); - t.cmd(cmd_); - t.prefix(prefix_); - t.output(path_); - - if (!x64_) - t.architecture(arch::x86); - - // copy the global context, the tool will modify it - context cxcopy(gcx()); - - t.run(cxcopy); - - return 0; - } - - std::string cmake_command::do_doc() - { - return "Runs `cmake ..` in the given directory with the same command line\n" - "as the one used for modorganizer projects."; - } - -} // namespace mob diff --git a/src/cmd/commands.h b/src/cmd/commands.h index 267a133..beb177c 100644 --- a/src/cmd/commands.h +++ b/src/cmd/commands.h @@ -367,27 +367,6 @@ namespace mob { std::vector get_repos() const; }; - // runs cmake in a directory with the same parameters as `build` would - // - class cmake_command : public command { - public: - cmake_command(); - meta_t meta() const override; - - protected: - clipp::group do_group() override; - int do_run() override; - std::string do_doc() override; - - private: - std::string gen_; - std::string cmd_; - bool x64_ = true; - bool debug_ = false; - std::string prefix_; - std::string path_; - }; - // lists the inis found by mob // class inis_command : public command { diff --git a/src/core/conf.cpp b/src/core/conf.cpp index e122f13..1801d99 100644 --- a/src/core/conf.cpp +++ b/src/core/conf.cpp @@ -51,24 +51,32 @@ namespace mob::details { // returns a string from conf, bails out if it doesn't exist // - std::string get_string(std::string_view section, std::string_view key) + std::string get_string(std::string_view section, std::string_view key, + std::optional default_) { auto sitor = g_conf.find(section); if (sitor == g_conf.end()) gcx().bail_out(context::conf, "[{}] doesn't exist", section); auto kitor = sitor->second.find(key); - if (kitor == sitor->second.end()) - gcx().bail_out(context::conf, "no key '{}' in [{}]", key, section); + if (kitor == sitor->second.end()) { + if (!default_.has_value()) { + gcx().bail_out(context::conf, "no key '{}' in [{}]", key, section); + } + return *default_; + } return kitor->second; } // calls get_string(), converts to int // - int get_int(std::string_view section, std::string_view key) + int get_int(std::string_view section, std::string_view key, + std::optional default_) { - const auto s = get_string(section, key); + const auto s = get_string(section, key, default_.transform([](auto v) { + return std::to_string(v); + })); try { return std::stoi(s); @@ -80,9 +88,12 @@ namespace mob::details { // calls get_string(), converts to bool // - bool get_bool(std::string_view section, std::string_view key) + bool get_bool(std::string_view section, std::string_view key, + std::optional default_) { - const auto s = get_string(section, key); + const auto s = get_string(section, key, default_.transform([](auto v) { + return v ? "true" : "false"; + })); return bool_from_string(s); } @@ -428,13 +439,8 @@ namespace mob { MOB_ASSERT(!tasks.empty()); - for (auto& t : tasks) { - if (t->name() != task && - details::find_string_for_task(t->name(), key)) { - continue; - } + for (auto& t : tasks) details::set_string_for_task(t->name(), key, value); - } } else { // global task option @@ -509,8 +515,10 @@ namespace mob { set_path_if_empty("pf_x86", find_program_files_x86); set_path_if_empty("pf_x64", find_program_files_x64); set_path_if_empty("vs", find_vs); + set_path_if_empty("vcpkg", find_vcpkg); // set after vs as it will use the VS set_path_if_empty("qt_install", find_qt); set_path_if_empty("temp_dir", find_temp_dir); + set_path_if_empty("icons", find_in_root("icons")); set_path_if_empty("patches", find_in_root("patches")); set_path_if_empty("licenses", find_in_root("licenses")); set_path_if_empty("qt_bin", qt::installation_path() / "bin"); @@ -530,7 +538,7 @@ namespace mob { resolve_path("install", p.prefix(), "install"); resolve_path("install_installer", p.install(), "installer"); resolve_path("install_bin", p.install(), "bin"); - resolve_path("install_libs", p.install(), "libs"); + resolve_path("install_libs", p.install(), "lib"); resolve_path("install_pdbs", p.install(), "pdb"); resolve_path("install_dlls", p.install_bin(), "dlls"); resolve_path("install_loot", p.install_bin(), "loot"); @@ -538,7 +546,7 @@ namespace mob { resolve_path("install_licenses", p.install_bin(), "licenses"); resolve_path("install_pythoncore", p.install_bin(), "pythoncore"); resolve_path("install_stylesheets", p.install_bin(), "stylesheets"); - resolve_path("install_translations", p.install_bin(), "translations"); + resolve_path("install_extensions", p.install_bin(), "extensions"); // finally, resolve the tools that are unlikely to be in PATH; all the // other tools (7z, jom, patch, etc.) are assumed to be in PATH (which @@ -684,6 +692,11 @@ namespace mob { return {}; } + conf_translations conf::translation() + { + return {}; + } + conf_prebuilt conf::prebuilt() { return {}; @@ -777,6 +790,8 @@ namespace mob { conf_build_types::conf_build_types() : conf_section("build-types") {} + conf_translations::conf_translations() : conf_section("translations") {} + conf_prebuilt::conf_prebuilt() : conf_section("prebuilt") {} conf_paths::conf_paths() : conf_section("paths") {} diff --git a/src/core/conf.h b/src/core/conf.h index 2589394..40ebbda 100644 --- a/src/core/conf.h +++ b/src/core/conf.h @@ -9,7 +9,8 @@ namespace mob::details { // returns an option named `key` from the given `section` // - std::string get_string(std::string_view section, std::string_view key); + std::string get_string(std::string_view section, std::string_view key, + std::optional default_ = {}); // convert a string to the given type template @@ -25,11 +26,13 @@ namespace mob::details { // calls get_string(), converts to bool // - bool get_bool(std::string_view section, std::string_view key); + bool get_bool(std::string_view section, std::string_view key, + std::optional default_ = {}); // calls get_string(), converts to in // - int get_int(std::string_view section, std::string_view key); + int get_int(std::string_view section, std::string_view key, + std::optional default_ = {}); // sets the given option, bails out if the option doesn't exist // @@ -60,9 +63,17 @@ namespace mob { template class conf_section { public: - DefaultType get(std::string_view key) const + DefaultType get(std::string_view key, + std::optional default_ = {}) const { - const auto value = details::get_string(name_, key); + const auto value = [=] { + if constexpr (std::is_same_v) { + return details::get_string(name_, key, default_); + } + else { + return details::get_string(name_, key); + } + }(); if constexpr (std::is_convertible_v) { return value; @@ -74,18 +85,18 @@ namespace mob { // undefined template - T get(std::string_view key) const; + T get(std::string_view key, std::optional default_ = {}) const; template <> - bool get(std::string_view key) const + bool get(std::string_view key, std::optional default_) const { - return details::get_bool(name_, key); + return details::get_bool(name_, key, default_); } template <> - int get(std::string_view key) const + int get(std::string_view key, std::optional default_) const { - return details::get_int(name_, key); + return details::get_int(name_, key, default_); } void set(std::string_view key, std::string_view value) @@ -223,6 +234,13 @@ namespace mob { conf_build_types(); }; + // options in [translations] + // + class conf_translations : public conf_section { + public: + conf_translations(); + }; + // options in [prebuilt] // class conf_prebuilt : public conf_section { @@ -245,6 +263,7 @@ namespace mob { VALUE(third_party); VALUE(prefix); VALUE(cache); + VALUE(icons); VALUE(patches); VALUE(licenses); VALUE(build); @@ -257,13 +276,13 @@ namespace mob { VALUE(install_dlls); VALUE(install_loot); - VALUE(install_plugins); + VALUE(install_extensions); VALUE(install_stylesheets); VALUE(install_licenses); VALUE(install_pythoncore); - VALUE(install_translations); VALUE(vs); + VALUE(vcpkg); VALUE(qt_install); VALUE(qt_bin); VALUE(qt_translations); @@ -285,6 +304,7 @@ namespace mob { conf_cmake cmake(); conf_tools tool(); conf_transifex transifex(); + conf_translations translation(); conf_prebuilt prebuilt(); conf_versions version(); conf_build_types build_types(); diff --git a/src/core/op.cpp b/src/core/op.cpp index bf0a051..62fc7e9 100644 --- a/src/core/op.cpp +++ b/src/core/op.cpp @@ -116,6 +116,30 @@ namespace mob::op { } } + void delete_file_glob_recurse(const context& cx, const fs::path& directory, + const fs::path& glob, flags f) + { + cx.trace(context::fs, "deleting glob {}", glob); + + const auto native = glob.native(); + + if (!fs::exists(directory)) + return; + + for (auto&& e : fs::recursive_directory_iterator(directory)) { + const auto p = e.path(); + const auto name = p.filename().native(); + + if (!PathMatchSpecW(name.c_str(), native.c_str())) { + cx.trace(context::fs, "{} did not match {}; skipping", name, glob); + + continue; + } + + delete_file(cx, p, f); + } + } + void remove_readonly(const context& cx, const fs::path& dir, flags f) { cx.trace(context::fs, "removing read-only from {}", dir); diff --git a/src/core/op.h b/src/core/op.h index eafd58a..15d0a4b 100644 --- a/src/core/op.h +++ b/src/core/op.h @@ -65,6 +65,11 @@ namespace mob::op { // void delete_file_glob(const context& cx, const fs::path& glob, flags f = noflags); + // deletes all files matching the glob in the given directory and its subdirectories + // + void delete_file_glob_recurse(const context& cx, const fs::path& directory, + const fs::path& glob, flags f = noflags); + // removes the readonly flag for all files in `dir`, recursive // void remove_readonly(const context& cx, const fs::path& dir, flags f = noflags); diff --git a/src/core/paths.cpp b/src/core/paths.cpp index 8652459..9248a64 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -254,6 +254,24 @@ namespace mob { } } + fs::path find_vcpkg() + { + const auto env_path = this_env::get().get("VCPKG_ROOT"); + + if (!env_path.empty()) { + return fs::absolute(env_path); + } + + const auto vs_path = conf().path().vs(); + const auto vcpkg_vs_path = vs_path / "VC" / "vcpkg"; + if (!exists(vcpkg_vs_path)) { + gcx().bail_out(context::conf, "vcpkg is not part of VS installation at {}", + vs_path); + } + + return vcpkg_vs_path; + } + fs::path find_qt() { // check from the ini first diff --git a/src/core/paths.h b/src/core/paths.h index 597823a..b36d806 100644 --- a/src/core/paths.h +++ b/src/core/paths.h @@ -33,6 +33,11 @@ namespace mob { // fs::path find_vs(); + // returns the absolute path to VCPKG root directory to be used as VCPKG_ROOT when + // building + // + fs::path find_vcpkg(); + // returns the absolute path to Qt's root directory, the one that contains // bin, include, etc.; bails if not found // diff --git a/src/main.cpp b/src/main.cpp index ce35867..f5be353 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,33 +26,33 @@ namespace mob { // third-party tasks - add_task() - .add_task() - .add_task() - .add_task() - .add_task() - .add_task() - .add_task() - .add_task() - .add_task(); - - add_task() - .add_task() - .add_task() - .add_task(); - - add_task() - .add_task() - .add_task() - .add_task(); - - add_task() - .add_task() - .add_task() - .add_task() - .add_task() - .add_task() - .add_task(); + // add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task(); + + // add_task() + // .add_task() + // .add_task() + // .add_task(); + + // add_task() + // .add_task() + // .add_task() + // .add_task(); + + // add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task() + // .add_task(); // super tasks @@ -61,9 +61,9 @@ namespace mob { // most of the alternate names below are from the transifex slugs, which // are sometimes different from the project names, for whatever reason - add_task() - .add_task("cmake_common") - .add_task("modorganizer-uibase"); + add_task().add_task().add_task("cmake_common"); + + add_task("modorganizer-uibase"); add_task() .add_task("modorganizer-archive") @@ -72,34 +72,12 @@ namespace mob { .add_task("modorganizer-bsatk") .add_task("modorganizer-nxmhandler") .add_task("modorganizer-helper") - .add_task("githubpp") - .add_task("modorganizer-game_gamebryo") .add_task({"modorganizer-bsapacker", "bsa_packer"}) - .add_task("modorganizer-preview_bsa"); - - // the gamebryo flag must be set for all game plugins that inherit from - // the gamebryo classes; this will merge the .ts file from gamebryo with - // the one from the specific plugin - add_task() - .add_task("modorganizer-game_oblivion", mo::gamebryo) - .add_task("modorganizer-game_nehrim", mo::gamebryo) - .add_task("modorganizer-game_fallout3", mo::gamebryo) - .add_task("modorganizer-game_fallout4", mo::gamebryo) - .add_task("modorganizer-game_fallout4vr", mo::gamebryo) - .add_task("modorganizer-game_fallout76", mo::gamebryo) - .add_task("modorganizer-game_falloutnv", mo::gamebryo) - .add_task("modorganizer-game_morrowind", mo::gamebryo) - .add_task("modorganizer-game_skyrim", mo::gamebryo) - .add_task("modorganizer-game_skyrimse", mo::gamebryo) - .add_task("modorganizer-game_skyrimvr", mo::gamebryo) - .add_task("modorganizer-game_starfield", mo::gamebryo) - .add_task("modorganizer-game_ttw", mo::gamebryo) - .add_task("modorganizer-game_enderal", mo::gamebryo) - .add_task("modorganizer-game_enderalse", mo::gamebryo); + .add_task("modorganizer-preview_bsa") + .add_task("modorganizer-game_bethesda"); add_task() .add_task({"modorganizer-tool_inieditor", "inieditor"}) - .add_task({"modorganizer-tool_inibakery", "inibakery"}) .add_task("modorganizer-preview_base") .add_task("modorganizer-diagnose_basic") .add_task("modorganizer-check_fnis") @@ -109,7 +87,7 @@ namespace mob { .add_task("modorganizer-installer_quick") .add_task("modorganizer-installer_fomod") .add_task("modorganizer-installer_fomod_csharp") - .add_task("modorganizer-installer_omod", mo::nuget) + .add_task("modorganizer-installer_omod") .add_task("modorganizer-installer_wizard") .add_task("modorganizer-bsa_extractor") .add_task("modorganizer-plugin_python"); @@ -147,7 +125,6 @@ namespace mob { std::make_unique(), std::make_unique(), std::make_unique(), - std::make_unique(), std::make_unique(), std::make_unique()}; diff --git a/src/tasks/modorganizer.cpp b/src/tasks/modorganizer.cpp index 2ad8f9d..9a053d1 100644 --- a/src/tasks/modorganizer.cpp +++ b/src/tasks/modorganizer.cpp @@ -3,6 +3,15 @@ namespace mob::tasks { + // build CMAKE_PREFIX_PATH for MO2 tasks + // + std::string cmake_prefix_path() + { + return conf().path().qt_install().string() + ";" + + (modorganizer::super_path() / "cmake_common").string() + ";" + + (conf().path().install() / "lib" / "cmake").string(); + } + // given a vector of names (some projects have more than one, see add_tasks() in // main.cpp), this prepends the simplified name to the vector and returns it // @@ -59,18 +68,18 @@ namespace mob::tasks { g.init_repo(); } - modorganizer::modorganizer(std::string long_name, flags f) - : modorganizer(std::vector{long_name}, f) + modorganizer::modorganizer(std::string long_name) + : modorganizer(std::vector{long_name}) { } - modorganizer::modorganizer(std::vector names, flags f) - : modorganizer(std::vector(names.begin(), names.end()), f) + modorganizer::modorganizer(std::vector names) + : modorganizer(std::vector(names.begin(), names.end())) { } - modorganizer::modorganizer(std::vector names, flags f) - : task(make_names(names)), repo_(names[0]), flags_(f) + modorganizer::modorganizer(std::vector names) + : task(make_names(names)), repo_(names[0]) { if (names.size() > 1) { project_ = names[1]; @@ -80,34 +89,15 @@ namespace mob::tasks { } } - bool modorganizer::is_gamebryo_plugin() const - { - return is_set(flags_, gamebryo); - } - - bool modorganizer::is_nuget_plugin() const - { - return is_set(flags_, nuget); - } - fs::path modorganizer::source_path() const { // something like build/modorganizer_super/uibase return super_path() / name(); } - fs::path modorganizer::project_file_path() const - { - // ask cmake for the build path it would use - const auto build_path = create_cmake_tool(source_path()).build_path(); - - // use the INSTALL project - return build_path / (project_ + ".sln"); - } - fs::path modorganizer::super_path() { - return conf().path().build() / "modorganizer_super"; + return conf().path().build(); } url modorganizer::git_url() const @@ -137,11 +127,7 @@ namespace mob::tasks { // cmake clean if (is_set(c, clean::reconfigure)) - run_tool(create_cmake_tool(cmake::clean)); - - // msbuild clean - if (is_set(c, clean::rebuild)) - run_tool(create_msbuild_tool(msbuild::clean)); + run_tool(cmake(cmake::clean).root(source_path())); } void modorganizer::do_fetch() @@ -165,7 +151,7 @@ namespace mob::tasks { void modorganizer::do_build_and_install() { - // adds a git submodule in modorganizer_super for this project; note that + // adds a git submodule in build for this project; note that // git_submodule_adder runs a thread because adding submodules is slow, but // can happen while stuff is building git_submodule_adder::instance().queue( @@ -177,65 +163,42 @@ namespace mob::tasks { // not all modorganizer projects need to actually be built, such as // cmake_common, so don't try if there's no cmake file - if (!fs::exists(source_path() / "CMakeLists.txt")) { + if (!exists(source_path() / "CMakeLists.txt")) { cx().trace(context::generic, "{} has no CMakeLists.txt, not building", repo_); return; } - // run cmake - run_tool(create_cmake_tool()); - - // run restore for nuget - // - // until https://gitlab.kitware.com/cmake/cmake/-/issues/20646 is resolved, - // we need a manual way of running the msbuild -t:restore - if (is_nuget_plugin()) - run_tool(create_msbuild_tool().targets({"restore"})); - - // run msbuild - run_tool(create_msbuild_tool()); - } - - cmake modorganizer::create_cmake_tool(cmake::ops o) - { - return create_cmake_tool(source_path(), o, task_conf().configuration()); - } - - cmake modorganizer::create_cmake_tool(const fs::path& root, cmake::ops o, config c) - { - return std::move( - cmake(o) - .generator(cmake::vs) - .def("CMAKE_INSTALL_PREFIX:PATH", conf().path().install()) - .def("DEPENDENCIES_DIR", conf().path().build()) - .def("BOOST_ROOT", boost::source_path()) - .def("BOOST_LIBRARYDIR", boost::lib_path(arch::x64)) - .def("SPDLOG_ROOT", spdlog::source_path()) - .def("LOOT_PATH", libloot::source_path()) - .def("LZ4_ROOT", lz4::source_path()) - .def("QT_ROOT", qt::installation_path()) - .def("ZLIB_ROOT", zlib::source_path()) - .def("PYTHON_ROOT", python::source_path()) - .def("SEVENZ_ROOT", sevenz::source_path()) - .def("LIBBSARCH_ROOT", libbsarch::source_path()) - .def("BOOST_DI_ROOT", boost_di::source_path()) - // gtest has no RelWithDebInfo, so simply use Debug/Release - .def("GTEST_ROOT", - gtest::build_path(arch::x64, c == config::debug ? config::debug - : config::release)) - .def("OPENSSL_ROOT_DIR", openssl::source_path()) - .def("DIRECTXTEX_ROOT", directxtex::source_path()) - .root(root)); - } + // if there is a CMakeLists.txt, there must be a CMakePresets.json otherwise + // we cannot build + if (!exists(source_path() / "CMakePresets.json")) { + gcx().bail_out(context::generic, + "{} has no CMakePresets.txt, aborting build", repo_); + } - msbuild modorganizer::create_msbuild_tool(msbuild::ops o) - { - return std::move(msbuild(o) - .solution(project_file_path()) - .configuration(task_conf().configuration()) - .architecture(arch::x64)); + // run cmake + run_tool(cmake(cmake::generate) + .generator(cmake::vs) + .def("CMAKE_INSTALL_PREFIX:PATH", conf().path().install()) + .def("CMAKE_PREFIX_PATH", cmake_prefix_path()) + .preset("vs2022-windows") + .root(source_path())); + + // run cmake --build with default target + // TODO: handle rebuild by adding `--clean-first` + // TODO: have a way to specify the `--parallel` value - 16 is useful to build + // game_bethesda that has 15 games, so 15 projects + run_tool(cmake(cmake::build) + .root(source_path()) + .arg("--parallel") + .arg("16") + .configuration(mob::config::relwithdebinfo)); + + // run cmake --install + run_tool(cmake(cmake::install) + .root(source_path()) + .configuration(mob::config::relwithdebinfo)); } } // namespace mob::tasks diff --git a/src/tasks/sevenz.cpp b/src/tasks/sevenz.cpp index f552508..1a107dd 100644 --- a/src/tasks/sevenz.cpp +++ b/src/tasks/sevenz.cpp @@ -79,7 +79,7 @@ namespace mob::tasks { void sevenz::build() { - build_loop(cx(), [&](bool mp) { + build_loop(cx(), [&]([[maybe_unused]] bool mp) { const int exit_code = run_tool(nmake() .path(module_to_build()) .def("CPU=x64") diff --git a/src/tasks/task.cpp b/src/tasks/task.cpp index 451366b..5f411ee 100644 --- a/src/tasks/task.cpp +++ b/src/tasks/task.cpp @@ -413,6 +413,7 @@ namespace mob { cx().info(context::generic, "build and install"); do_build_and_install(); + cx().info(context::generic, "done"); } void task::check_bailed() diff --git a/src/tasks/tasks.h b/src/tasks/tasks.h index 8b9b116..390258c 100644 --- a/src/tasks/tasks.h +++ b/src/tasks/tasks.h @@ -216,34 +216,13 @@ namespace mob::tasks { cmake::ops o = cmake::generate, config config = config::relwithdebinfo); - // flags for some MO projects - enum flags { - noflags = 0x00, - - // gamebryo project, used by the translations task because these - // projects have multiple .ts files that have to be merged - gamebryo = 0x01, - - // project that uses nuget, cmake doesn't support those right now, so - // `msbuild -t:restore` has to be run manually - nuget = 0x02, - }; - // some mo tasks have more than one name, mostly because the transifex slugs // are different than the names on github; the std::string and const char* // overloads are because they're constructed from initializer lists and it's // more convenient that way - modorganizer(std::string name, flags f = noflags); - modorganizer(std::vector names, flags f = noflags); - modorganizer(std::vector names, flags f = noflags); - - // whether this project has the gamebryo flag on - // - bool is_gamebryo_plugin() const; - - // whether this project has the nuget flag on - // - bool is_nuget_plugin() const; + modorganizer(std::string name); + modorganizer(std::vector names); + modorganizer(std::vector names); // url to the git repo // @@ -271,27 +250,6 @@ namespace mob::tasks { private: std::string repo_; std::string project_; - flags flags_; - - // creates the cmake tool for this MO project - // - cmake create_cmake_tool(cmake::ops o = cmake::generate); - - // creates the msbuild tool for this MO project - // - msbuild create_msbuild_tool(msbuild::ops o = msbuild::build); - - // this is the file targeted by the msbuild tool - // - // it's not actually the .sln file because the cmake files have historically - // been inconsistent in what the main project in the solution is and whether - // the INSTALL project was enabled or not, so just building the .sln itself - // might not actually build everything - // - // by targeting the INSTALL project directly, everything will always be - // built correctly, regardless of how the solution file is generated - // - fs::path project_file_path() const; }; class ncc : public basic_task { @@ -623,11 +581,6 @@ namespace mob::tasks { // duplicate warnings std::set warned_; - // whether the given project name is a gamebryo task, `dir` is just for - // logging - // - bool is_gamebryo_plugin(const std::string& dir, const std::string& project); - // parses the directory name, walks all the .ts files, returns a project // object for them // @@ -636,7 +589,7 @@ namespace mob::tasks { // returns a lang object that contains at least the given main_ts_file, // but might contain more if it's a gamebryo plugin // - lang create_lang(bool gamebryo, const std::string& project_name, + lang create_lang(const std::string& project_name, const fs::path& main_ts_file); }; @@ -671,6 +624,7 @@ namespace mob::tasks { void fetch_from_source(); void build_and_install_from_source(); + cmake create_cmake_tool(arch, cmake::ops = cmake::generate) const; msbuild create_msbuild_tool(arch, msbuild::ops = msbuild::build, config = config::release) const; }; diff --git a/src/tasks/translations.cpp b/src/tasks/translations.cpp index bc29c50..bb0980b 100644 --- a/src/tasks/translations.cpp +++ b/src/tasks/translations.cpp @@ -1,6 +1,8 @@ #include "pch.h" #include "../core/env.h" +#include "../utility/string.h" #include "../utility/threading.h" +#include "nlohmann/json.hpp" #include "task_manager.h" #include "tasks.h" @@ -125,9 +127,6 @@ namespace mob::tasks { // walks all the .ts files in the project, creates a `lang` object for // each // - // each project directory is named "mod-organizer-2.project_name", so this - // splits on the dot to get the project name, checks if it's a gamebryo - // plugin, and adds the gamebryo .ts file as well if necessary // splitting const auto dir_name = path_to_utf8(dir.filename()); @@ -150,7 +149,6 @@ namespace mob::tasks { // project project p(project_name); - const bool gamebryo = is_gamebryo_plugin(dir_name, project_name); // for each file for (auto f : fs::directory_iterator(dir)) { @@ -168,14 +166,14 @@ namespace mob::tasks { } // add a new `lang` object for it - p.langs.push_back(create_lang(gamebryo, project_name, f.path())); + p.langs.push_back(create_lang(project_name, f.path())); } return p; } translations::projects::lang - translations::projects::create_lang(bool gamebryo, const std::string& project_name, + translations::projects::create_lang(const std::string& project_name, const fs::path& main_ts_file) { lang lg(path_to_utf8(main_ts_file.stem())); @@ -183,64 +181,9 @@ namespace mob::tasks { // every lang has the .ts file from the project, gamebryo plugins have more lg.ts_files.push_back(main_ts_file); - if (gamebryo) { - // this is a gamebryo plugin, so it needs the gamebryo .ts file as well, - // find it - - // the .ts files for gamebryo are in mod-organizer-2.game_gamebryo/ - const fs::path gamebryo_dir = - conf().transifex().get("project") + "." + "game_gamebryo"; - - // the .ts file has the same name, it's just "lang.ts" - const auto gamebryo_ts = root_ / gamebryo_dir / main_ts_file.filename(); - - if (fs::exists(gamebryo_ts)) { - // found, add it - lg.ts_files.push_back(gamebryo_ts); - } - else { - // not found, that means the plugin was translated into a language, - // but the gamebryo project wasn't; warn once - if (!warned_.contains(gamebryo_ts)) { - warned_.insert(gamebryo_ts); - - warnings_.push_back(::std::format( - "{} is a gamebryo plugin but there is no '{}'; the " - ".qm file will be missing some translations (will " - "only warn once)", - project_name, path_to_utf8(gamebryo_ts))); - } - } - } - return lg; } - bool translations::projects::is_gamebryo_plugin(const std::string& dir, - const std::string& project) - { - const auto* t = task_manager::instance().find_one(project); - - if (!t) { - warnings_.push_back( - ::std::format("directory '{}' was parsed as project '{}', but there's " - "no task with this name", - dir, project)); - - return false; - } - - // gamebryo plugins are all `modorganizer` tasks - const auto* mo_task = static_cast(t); - if (!mo_task) { - // not an mo task, can't be a gamebryo plugin - return false; - } - - // check the flag - return mo_task->is_gamebryo_plugin(); - } - translations::translations() : task("translations") {} fs::path translations::source_path() @@ -256,8 +199,8 @@ namespace mob::tasks { // remove the .qm files in the translations/ directory if (is_set(c, clean::rebuild)) { - op::delete_file_glob(cx(), conf().path().install_translations() / "*.qm", - op::optional); + op::delete_file_glob_recurse(cx(), conf().path().install_extensions(), + "*.qm", op::optional); } } @@ -310,46 +253,139 @@ namespace mob::tasks { } } + void generate_translations_metadata( + std::filesystem::path const& path, + std::vector const& languages) + { + using json = nlohmann::ordered_json; + + json metadata = json::parse(R"( +{ + "id": "mo2-translations", + "name": "Translations for ModOrganizer2", + "version": "1.0.0", + "description": "Multi-language translations for ModOrganizer2 itself.", + "author": { + "name": "Mod Organizer 2", + "homepage": "https://www.modorganizer.org/" + }, + "icon": "translations.png", + "type": "translation", + "content": { + "translations": {} + } +} +)"); + + // fix version + json translations; + for (auto&& lang : languages) { + std::vector files; + files.push_back("translations/" + lang.name + "/*.qm"); + json jsonlang; + jsonlang["files"] = files; + translations[lang.name] = jsonlang; + } + metadata["content"]["translations"] = translations; + + std::ofstream ofs(path); + ofs << metadata.dump(2); + } + void translations::do_build_and_install() { // 1) build the list of projects, languages and .ts files // 2) run `lrelease` for every language in every project // 3) copy builtin qt translations - const auto root = source_path() / "translations"; - const auto dest = conf().path().install_translations(); + const auto root = source_path() / "translations"; + const auto extensions = conf().path().install_extensions(); const projects ps(root); - op::create_directories(cx(), dest); + op::create_directories(cx(), extensions / "mo2-translations"); // log all the warnings added while walking the projects for (auto&& w : ps.warnings()) cx().warning(context::generic, "{}", w); + std::map project_to_extension; + + // go through the list of extensions and find the matching projects + for (auto& p : fs::directory_iterator(extensions)) { + if (!fs::is_directory(p)) + continue; + + const auto name = + mob::replace_all(p.path().filename().string(), "mo2-", ""); + + project_to_extension[name] = p.path().filename(); + project_to_extension[mob::replace_all(name, "-", "_")] = + p.path().filename(); + + const auto s_projects = + conf().translation().get(p.path().filename().string(), ""); + if (!s_projects.empty()) { + const auto extension_projects = mob::split(s_projects, " "); + for (const auto& project : extension_projects) { + project_to_extension[project] = p.path().filename(); + } + } + } + // run `lrelease` in a thread pool parallel_functions v; // for each project for (auto& p : ps.get()) { + if (!project_to_extension.contains(p.name)) { + cx().warning(context::generic, + "found project {} but no matching extension", p.name); + continue; + } + + const auto base = extensions / project_to_extension[p.name]; + + if (!fs::exists(base)) { + cx().warning( + context::generic, + "found project {} for extension {} but extension is not built", + p.name, project_to_extension[p.name]); + continue; + } + + const auto dest = base / "translations"; + op::create_directories(cx(), dest); + // for each language for (auto& lg : p.langs) { + op::create_directories(cx(), dest / lg.name); // add a functor that will run lrelease - v.push_back( - {lg.name + "." + p.name, [&] { - // run release for the given project name and list of .ts files - run_tool( - lrelease().project(p.name).sources(lg.ts_files).out(dest)); - }}); + v.push_back({lg.name + "." + p.name, [=] { + // run release for the given project name and list of + // .ts files + run_tool(lrelease() + .project(p.name) + .sources(lg.ts_files) + .out(dest / lg.name)); + }}); } } // run all the functors in parallel parallel(v); - if (auto p = ps.find("organizer")) - copy_builtin_qt_translations(*p, dest); + if (auto p = ps.find("organizer")) { + // copy Qt builting translation, icon and generate metadata + copy_builtin_qt_translations(*p, extensions / "mo2-translations" / + "translations"); + op::copy_file_to_file_if_better( + cx(), conf().path().icons() / "translations.png", + extensions / "mo2-translations" / "translations.png", op::unsafe); + generate_translations_metadata( + extensions / "mo2-translations" / "metadata.json", p->langs); + } else - cx().bail_out(context::generic, "organizer project not found"); + cx().warning(context::generic, "organizer project not found"); } void translations::copy_builtin_qt_translations(const projects::project& p, @@ -366,7 +402,8 @@ namespace mob::tasks { if (!fs::exists(src)) return false; - op::copy_file_to_dir_if_better(cx(), src, dest, op::unsafe); + op::create_directories(cx(), dest / lang); + op::copy_file_to_dir_if_better(cx(), src, dest / lang, op::unsafe); return true; }; diff --git a/src/tasks/usvfs.cpp b/src/tasks/usvfs.cpp index a61e277..e2bfe3d 100644 --- a/src/tasks/usvfs.cpp +++ b/src/tasks/usvfs.cpp @@ -40,6 +40,11 @@ namespace mob::tasks { return; } + if (is_set(c, clean::reconfigure)) { + run_tool(create_cmake_tool(arch::x64)); + run_tool(create_cmake_tool(arch::x86)); + } + if (is_set(c, clean::rebuild)) { // msbuild clean run_tool(create_msbuild_tool(arch::x86, msbuild::clean, @@ -69,35 +74,28 @@ namespace mob::tasks { void usvfs::build_and_install_from_source() { - run_tool(create_msbuild_tool(arch::x86)); + run_tool(create_cmake_tool(arch::x64)); + run_tool(create_cmake_tool(arch::x86)); run_tool(create_msbuild_tool(arch::x64)); + run_tool(create_msbuild_tool(arch::x86)); } - msbuild usvfs::create_msbuild_tool(arch a, msbuild::ops o, config c) const + cmake usvfs::create_cmake_tool(arch a, cmake::ops o) const { - // usvfs doesn't use "Win32" for 32-bit, it uses "x86" - // - // note that usvfs_proxy has a custom build step in Release that runs - // usvfs/vsbuild/stage_helper.cmd, which copies everything into - // install/ - - const std::string plat = (a == arch::x64 ? "x64" : "x86"); - - // udis requires python in its custom build step, so make sure it's on the - // path - // - // BOOST_PATH is in the env because external_dependencies.props will - // use it if it exists or reverts to a hardcoded path which might not be using - // the same version as mob if it wasn't updated return std::move( - msbuild(o) - .platform(plat) - .configuration(c) - .targets({"usvfs_proxy"}) - .solution(source_path() / "vsbuild" / "usvfs.sln") - .env(env::vs(a) - .prepend_path(python::build_path()) - .set("BOOST_PATH", path_to_utf8(boost::source_path())))); + cmake(o) + .root(source_path()) + .def("CMAKE_INSTALL_PREFIX:PATH", conf().path().install()) + .generator(cmake::vs) + .preset(a == arch::x64 ? "vs2022-windows-x64" : "vs2022-windows-x86") + .arg("-DBUILD_TESTING=OFF")); + } + + msbuild usvfs::create_msbuild_tool(arch a, msbuild::ops o, config c) const + { + const std::string vsbuild = (a == arch::x64 ? "vsbuild64" : "vsbuild32"); + return std::move(msbuild(o).architecture(a).configuration(c).solution( + source_path() / vsbuild / "usvfs.sln")); } } // namespace mob::tasks diff --git a/src/tools/cmake.cpp b/src/tools/cmake.cpp index 0f277e0..3d8e629 100644 --- a/src/tools/cmake.cpp +++ b/src/tools/cmake.cpp @@ -4,8 +4,23 @@ namespace mob { + namespace { + std::string config_to_string(config c) + { + switch (c) { + case config::debug: + return "Debug"; + case config::release: + return "Release"; + case config::relwithdebinfo: + return "RelWithDebInfo"; + } + gcx().bail_out(context::generic, "unknow configuration type {}", c); + } + } // namespace + cmake::cmake(ops o) - : basic_process_runner("cmake"), op_(o), gen_(jom), arch_(arch::def) + : basic_process_runner("cmake"), op_(o), gen_(vs), arch_(arch::def) { } @@ -62,6 +77,12 @@ namespace mob { return *this; } + cmake& cmake::preset(const std::string& s) + { + preset_ = s; + return *this; + } + cmake& cmake::arg(std::string s) { std::replace(s.begin(), s.end(), '\\', '/'); @@ -75,6 +96,24 @@ namespace mob { return *this; } + cmake& cmake::targets(const std::string& target) + { + targets_ = {target}; + return *this; + } + + cmake& cmake::targets(const std::vector& targets) + { + targets_ = targets; + return *this; + } + + cmake& cmake::configuration(mob::config config) + { + config_ = config; + return *this; + } + cmake& cmake::cmd(const std::string& s) { cmd_ = s; @@ -110,6 +149,16 @@ namespace mob { break; } + case build: { + do_build(); + break; + } + + case install: { + do_install(); + break; + } + default: { cx().bail_out(context::generic, "bad cmake op {}", op_); } @@ -126,43 +175,83 @@ namespace mob { auto p = process() .stdout_encoding(encodings::utf8) .stderr_encoding(encodings::utf8) - .binary(binary()) - .arg("-DCMAKE_BUILD_TYPE=Release") - .arg("-DCMAKE_INSTALL_MESSAGE=" + - conf_cmake::to_string(conf().cmake().install_message())) - .arg("--log-level=ERROR") - .arg("--no-warn-unused-cli"); - - if (genstring_.empty()) { - // there's always a generator name, but some generators don't need - // an architecture flag, like jom, so get_arch() might return an empty - // string - p.arg("-G", "\"" + g.name + "\"") - .arg(g.get_arch(arch_)) - .arg(g.get_host(conf().cmake().host())); - } - else { - // verbatim generator string - p.arg("-G", "\"" + genstring_ + "\""); + .binary(binary()); + + if (!preset_.empty()) { + p = p.arg("--preset").arg(preset_); } + p = p.arg("-DCMAKE_INSTALL_MESSAGE=" + + conf_cmake::to_string(conf().cmake().install_message())) + .arg("--log-level=ERROR") + .arg("--no-warn-unused-cli"); + // prefix if (!prefix_.empty()) p.arg("-DCMAKE_INSTALL_PREFIX=", prefix_); p.args(args_); - // `..` by default, overriden by cmd() - if (cmd_.empty()) - p.arg(".."); - else - p.arg(cmd_); + if (preset_.empty()) { + + if (genstring_.empty()) { + // there's always a generator name, but some generators don't need + // an architecture flag, like jom, so get_arch() might return an empty + // string + p.arg("-G", "\"" + g.name + "\"") + .arg(g.get_arch(arch_)) + .arg(g.get_host(conf().cmake().host())); + } + else { + // verbatim generator string + p.arg("-G", "\"" + genstring_ + "\""); + } + + // `..` by default, overriden by cmd() + if (cmd_.empty()) + p.arg(".."); + else + p.arg(cmd_); + } + + p.env(env::vs(arch_) + .set("CXXFLAGS", "/wd4566") + .set("VCPKG_ROOT", absolute(conf().path().vcpkg()).string())) + .cwd(preset_.empty() ? build_path() : root_); + + execute_and_join(p); + } + + void cmake::do_build() + { + auto p = process() + .stdout_encoding(encodings::utf8) + .stderr_encoding(encodings::utf8) + .binary(binary()) + .arg("--build") + .arg(build_path()) + .arg("--config") + .arg(config_to_string(config_)); - p.env(env::vs(arch_).set("CXXFLAGS", "/wd4566")).cwd(build_path()); + for (auto& target : targets_) { + p = p.arg("--target").arg(target); + } execute_and_join(p); } + void cmake::do_install() + { + execute_and_join(process() + .stdout_encoding(encodings::utf8) + .stderr_encoding(encodings::utf8) + .binary(binary()) + .arg("--install") + .arg(build_path()) + .arg("--config") + .arg(config_to_string(config_))); + } + void cmake::do_clean() { cx().trace(context::rebuild, "deleting all generator directories"); diff --git a/src/tools/cmake.h b/src/tools/cmake.h index ea8a3a6..9420fc5 100644 --- a/src/tools/cmake.h +++ b/src/tools/cmake.h @@ -29,6 +29,12 @@ namespace mob { // generates the build files generate = 1, + // build + build, + + // install + install, + // cleans the build files so they're regenerated from scratch clean }; @@ -54,6 +60,15 @@ namespace mob { // cmake& root(const fs::path& p); + // set the targets for build + // + cmake& targets(const std::string& target); + cmake& targets(const std::vector& target); + + // set the configuration to build or install + // + cmake& configuration(mob::config config); + // overrides the directory in which cmake will write build files // // by default, this is a directory inside what was given in root() with a @@ -75,6 +90,10 @@ namespace mob { cmake& def(const std::string& name, const fs::path& p); cmake& def(const std::string& name, const char* s); + // set a preset to run with cmake --preset + // + cmake& preset(const std::string& s); + // adds an arbitrary argument, passed verbatim // cmake& arg(std::string s); @@ -146,6 +165,9 @@ namespace mob { // what run() does ops op_; + // preset to run + std::string preset_; + // directory where CMakeLists.txt is fs::path root_; @@ -156,6 +178,12 @@ namespace mob { // passed as -DCMAKE_INSTALL_PREFIX fs::path prefix_; + // targets + std::vector targets_; + + // configuration + mob::config config_{mob::config::relwithdebinfo}; + // passed verbatim std::vector args_; @@ -175,6 +203,8 @@ namespace mob { // runs cmake // void do_generate(); + void do_build(); + void do_install(); // returns a list of generators handled by this tool, same ones as in the // `generators` enum on top