Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/prusa3d/PrusaSlicer.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/slic3r/Utils/PresetUpdater.cpp')
-rw-r--r--src/slic3r/Utils/PresetUpdater.cpp633
1 files changed, 633 insertions, 0 deletions
diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp
new file mode 100644
index 000000000..2e423dc5e
--- /dev/null
+++ b/src/slic3r/Utils/PresetUpdater.cpp
@@ -0,0 +1,633 @@
+#include "PresetUpdater.hpp"
+
+#include <algorithm>
+#include <thread>
+#include <unordered_map>
+#include <ostream>
+#include <stdexcept>
+#include <boost/format.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <boost/filesystem/fstream.hpp>
+#include <boost/log/trivial.hpp>
+
+#include <wx/app.h>
+#include <wx/event.h>
+#include <wx/msgdlg.h>
+
+#include "libslic3r/libslic3r.h"
+#include "libslic3r/Utils.hpp"
+#include "slic3r/GUI/GUI.hpp"
+#include "slic3r/GUI/PresetBundle.hpp"
+#include "slic3r/GUI/UpdateDialogs.hpp"
+#include "slic3r/GUI/ConfigWizard.hpp"
+#include "slic3r/Utils/Http.hpp"
+#include "slic3r/Config/Version.hpp"
+#include "slic3r/Config/Snapshot.hpp"
+
+namespace fs = boost::filesystem;
+using Slic3r::GUI::Config::Index;
+using Slic3r::GUI::Config::Version;
+using Slic3r::GUI::Config::Snapshot;
+using Slic3r::GUI::Config::SnapshotDB;
+
+
+namespace Slic3r {
+
+
+enum {
+ SLIC3R_VERSION_BODY_MAX = 256,
+};
+
+static const char *INDEX_FILENAME = "index.idx";
+static const char *TMP_EXTENSION = ".download";
+
+
+struct Update
+{
+ fs::path source;
+ fs::path target;
+ Version version;
+
+ Update(fs::path &&source, fs::path &&target, const Version &version) :
+ source(std::move(source)),
+ target(std::move(target)),
+ version(version)
+ {}
+
+ std::string name() const { return source.stem().string(); }
+
+ friend std::ostream& operator<<(std::ostream& os , const Update &self) {
+ os << "Update(" << self.source.string() << " -> " << self.target.string() << ')';
+ return os;
+ }
+};
+
+struct Incompat
+{
+ fs::path bundle;
+ Version version;
+
+ Incompat(fs::path &&bundle, const Version &version) :
+ bundle(std::move(bundle)),
+ version(version)
+ {}
+
+ std::string name() const { return bundle.stem().string(); }
+
+ friend std::ostream& operator<<(std::ostream& os , const Incompat &self) {
+ os << "Incompat(" << self.bundle.string() << ')';
+ return os;
+ }
+};
+
+struct Updates
+{
+ std::vector<Incompat> incompats;
+ std::vector<Update> updates;
+};
+
+
+struct PresetUpdater::priv
+{
+ int version_online_event;
+ std::vector<Index> index_db;
+
+ bool enabled_version_check;
+ bool enabled_config_update;
+ std::string version_check_url;
+ bool had_config_update;
+
+ fs::path cache_path;
+ fs::path rsrc_path;
+ fs::path vendor_path;
+
+ bool cancel;
+ std::thread thread;
+
+ priv(int version_online_event);
+
+ void set_download_prefs(AppConfig *app_config);
+ bool get_file(const std::string &url, const fs::path &target_path) const;
+ void prune_tmps() const;
+ void sync_version() const;
+ void sync_config(const std::set<VendorProfile> vendors) const;
+
+ void check_install_indices() const;
+ Updates get_config_updates() const;
+ void perform_updates(Updates &&updates, bool snapshot = true) const;
+
+ static void copy_file(const fs::path &from, const fs::path &to);
+};
+
+PresetUpdater::priv::priv(int version_online_event) :
+ version_online_event(version_online_event),
+ had_config_update(false),
+ cache_path(fs::path(Slic3r::data_dir()) / "cache"),
+ rsrc_path(fs::path(resources_dir()) / "profiles"),
+ vendor_path(fs::path(Slic3r::data_dir()) / "vendor"),
+ cancel(false)
+{
+ set_download_prefs(GUI::get_app_config());
+ check_install_indices();
+ index_db = std::move(Index::load_db());
+}
+
+// Pull relevant preferences from AppConfig
+void PresetUpdater::priv::set_download_prefs(AppConfig *app_config)
+{
+ enabled_version_check = app_config->get("version_check") == "1";
+ version_check_url = app_config->version_check_url();
+ enabled_config_update = app_config->get("preset_update") == "1" && !app_config->legacy_datadir();
+}
+
+// Downloads a file (http get operation). Cancels if the Updater is being destroyed.
+bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &target_path) const
+{
+ bool res = false;
+ fs::path tmp_path = target_path;
+ tmp_path += (boost::format(".%1%%2%") % get_current_pid() % TMP_EXTENSION).str();
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Get: `%1%`\n\t-> `%2%`\n\tvia tmp path `%3%`")
+ % url
+ % target_path.string()
+ % tmp_path.string();
+
+ Http::get(url)
+ .on_progress([this](Http::Progress, bool &cancel) {
+ if (cancel) { cancel = true; }
+ })
+ .on_error([&](std::string body, std::string error, unsigned http_status) {
+ (void)body;
+ BOOST_LOG_TRIVIAL(error) << boost::format("Error getting: `%1%`: HTTP %2%, %3%")
+ % url
+ % http_status
+ % error;
+ })
+ .on_complete([&](std::string body, unsigned /* http_status */) {
+ fs::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc);
+ file.write(body.c_str(), body.size());
+ file.close();
+ fs::rename(tmp_path, target_path);
+ res = true;
+ })
+ .perform_sync();
+
+ return res;
+}
+
+// Remove leftover paritally downloaded files, if any.
+void PresetUpdater::priv::prune_tmps() const
+{
+ for (fs::directory_iterator it(cache_path); it != fs::directory_iterator(); ++it) {
+ if (it->path().extension() == TMP_EXTENSION) {
+ BOOST_LOG_TRIVIAL(debug) << "Cache prune: " << it->path().string();
+ fs::remove(it->path());
+ }
+ }
+}
+
+// Get Slic3rPE version available online, save in AppConfig.
+void PresetUpdater::priv::sync_version() const
+{
+ if (! enabled_version_check) { return; }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Downloading Slic3rPE online version from: `%1%`") % version_check_url;
+
+ Http::get(version_check_url)
+ .size_limit(SLIC3R_VERSION_BODY_MAX)
+ .on_progress([this](Http::Progress, bool &cancel) {
+ cancel = this->cancel;
+ })
+ .on_error([&](std::string body, std::string error, unsigned http_status) {
+ (void)body;
+ BOOST_LOG_TRIVIAL(error) << boost::format("Error getting: `%1%`: HTTP %2%, %3%")
+ % version_check_url
+ % http_status
+ % error;
+ })
+ .on_complete([&](std::string body, unsigned /* http_status */) {
+ boost::trim(body);
+ BOOST_LOG_TRIVIAL(info) << boost::format("Got Slic3rPE online version: `%1%`. Sending to GUI thread...") % body;
+ wxCommandEvent* evt = new wxCommandEvent(version_online_event);
+ evt->SetString(body);
+ GUI::get_app()->QueueEvent(evt);
+ })
+ .perform_sync();
+}
+
+// Download vendor indices. Also download new bundles if an index indicates there's a new one available.
+// Both are saved in cache.
+void PresetUpdater::priv::sync_config(const std::set<VendorProfile> vendors) const
+{
+ BOOST_LOG_TRIVIAL(info) << "Syncing configuration cache";
+
+ if (!enabled_config_update) { return; }
+
+ // Donwload vendor preset bundles
+ for (const auto &index : index_db) {
+ if (cancel) { return; }
+
+ const auto vendor_it = vendors.find(VendorProfile(index.vendor()));
+ if (vendor_it == vendors.end()) {
+ BOOST_LOG_TRIVIAL(warning) << "No such vendor: " << index.vendor();
+ continue;
+ }
+
+ const VendorProfile &vendor = *vendor_it;
+ if (vendor.config_update_url.empty()) {
+ BOOST_LOG_TRIVIAL(info) << "Vendor has no config_update_url: " << vendor.name;
+ continue;
+ }
+
+ // Download a fresh index
+ BOOST_LOG_TRIVIAL(info) << "Downloading index for vendor: " << vendor.name;
+ const auto idx_url = vendor.config_update_url + "/" + INDEX_FILENAME;
+ const auto idx_path = cache_path / (vendor.id + ".idx");
+ if (! get_file(idx_url, idx_path)) { continue; }
+ if (cancel) { return; }
+
+ // Load the fresh index up
+ Index new_index;
+ new_index.load(idx_path);
+
+ // See if a there's a new version to download
+ const auto recommended_it = new_index.recommended();
+ if (recommended_it == new_index.end()) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("No recommended version for vendor: %1%, invalid index?") % vendor.name;
+ continue;
+ }
+ const auto recommended = recommended_it->config_version;
+
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Got index for vendor: %1%: current version: %2%, recommended version: %3%")
+ % vendor.name
+ % vendor.config_version.to_string()
+ % recommended.to_string();
+
+ if (vendor.config_version >= recommended) { continue; }
+
+ // Download a fresh bundle
+ BOOST_LOG_TRIVIAL(info) << "Downloading new bundle for vendor: " << vendor.name;
+ const auto bundle_url = (boost::format("%1%/%2%.ini") % vendor.config_update_url % recommended.to_string()).str();
+ const auto bundle_path = cache_path / (vendor.id + ".ini");
+ if (! get_file(bundle_url, bundle_path)) { continue; }
+ if (cancel) { return; }
+ }
+}
+
+// Install indicies from resources. Only installs those that are either missing or older than in resources.
+void PresetUpdater::priv::check_install_indices() const
+{
+ BOOST_LOG_TRIVIAL(info) << "Checking if indices need to be installed from resources...";
+
+ for (fs::directory_iterator it(rsrc_path); it != fs::directory_iterator(); ++it) {
+ const auto &path = it->path();
+ if (path.extension() == ".idx") {
+ const auto path_in_cache = cache_path / path.filename();
+
+ if (! fs::exists(path_in_cache)) {
+ BOOST_LOG_TRIVIAL(info) << "Install index from resources: " << path.filename();
+ copy_file(path, path_in_cache);
+ } else {
+ Index idx_rsrc, idx_cache;
+ idx_rsrc.load(path);
+ idx_cache.load(path_in_cache);
+
+ if (idx_cache.version() < idx_rsrc.version()) {
+ BOOST_LOG_TRIVIAL(info) << "Update index from resources: " << path.filename();
+ copy_file(path, path_in_cache);
+ }
+ }
+ }
+ }
+}
+
+// Generates a list of bundle updates that are to be performed
+Updates PresetUpdater::priv::get_config_updates() const
+{
+ Updates updates;
+
+ BOOST_LOG_TRIVIAL(info) << "Checking for cached configuration updates...";
+
+ for (const auto idx : index_db) {
+ auto bundle_path = vendor_path / (idx.vendor() + ".ini");
+
+ if (! fs::exists(bundle_path)) {
+ BOOST_LOG_TRIVIAL(info) << "Bundle not present for index, skipping: " << idx.vendor();
+ continue;
+ }
+
+ // Perform a basic load and check the version
+ const auto vp = VendorProfile::from_ini(bundle_path, false);
+
+ const auto ver_current = idx.find(vp.config_version);
+ if (ver_current == idx.end()) {
+ auto message = (boost::format("Preset bundle `%1%` version not found in index: %2%") % idx.vendor() % vp.config_version.to_string()).str();
+ BOOST_LOG_TRIVIAL(error) << message;
+ throw std::runtime_error(message);
+ }
+
+ // Getting a recommended version from the latest index, wich may have been downloaded
+ // from the internet, or installed / updated from the installation resources.
+ const auto recommended = idx.recommended();
+ if (recommended == idx.end()) {
+ BOOST_LOG_TRIVIAL(error) << boost::format("No recommended version for vendor: %1%, invalid index?") % idx.vendor();
+ }
+
+ BOOST_LOG_TRIVIAL(debug) << boost::format("Vendor: %1%, version installed: %2%, version cached: %3%")
+ % vp.name
+ % ver_current->config_version.to_string()
+ % recommended->config_version.to_string();
+
+ if (! ver_current->is_current_slic3r_supported()) {
+ BOOST_LOG_TRIVIAL(warning) << "Current Slic3r incompatible with installed bundle: " << bundle_path.string();
+ updates.incompats.emplace_back(std::move(bundle_path), *ver_current);
+ } else if (recommended->config_version > ver_current->config_version) {
+ // Config bundle update situation
+
+ // Check if the update is already present in a snapshot
+ const auto recommended_snap = SnapshotDB::singleton().snapshot_with_vendor_preset(vp.name, recommended->config_version);
+ if (recommended_snap != SnapshotDB::singleton().end()) {
+ BOOST_LOG_TRIVIAL(info) << boost::format("Bundle update %1% %2% already found in snapshot %3%, skipping...")
+ % vp.name
+ % recommended->config_version.to_string()
+ % recommended_snap->id;
+ continue;
+ }
+
+ auto path_src = cache_path / (idx.vendor() + ".ini");
+ auto path_in_rsrc = rsrc_path / (idx.vendor() + ".ini");
+ if (! fs::exists(path_src)) {
+ if (! fs::exists(path_in_rsrc)) {
+ BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update, but bundle found in neither cache nor resources")
+ % idx.vendor();
+ continue;
+ } else {
+ path_src = std::move(path_in_rsrc);
+ path_in_rsrc.clear();
+ }
+ }
+
+ auto new_vp = VendorProfile::from_ini(path_src, false);
+ bool found = false;
+ if (new_vp.config_version == recommended->config_version) {
+ updates.updates.emplace_back(std::move(path_src), std::move(bundle_path), *recommended);
+ found = true;
+ } else if (! path_in_rsrc.empty() && fs::exists(path_in_rsrc)) {
+ new_vp = VendorProfile::from_ini(path_in_rsrc, false);
+ if (new_vp.config_version == recommended->config_version) {
+ updates.updates.emplace_back(std::move(path_in_rsrc), std::move(bundle_path), *recommended);
+ found = true;
+ }
+ }
+ if (! found)
+ BOOST_LOG_TRIVIAL(warning) << boost::format("Index for vendor %1% indicates update (%2%) but the new bundle was found neither in cache nor resources")
+ % idx.vendor()
+ % recommended->config_version.to_string();
+ }
+ }
+
+ return updates;
+}
+
+void PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const
+{
+ if (updates.incompats.size() > 0) {
+ if (snapshot) {
+ BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
+ SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_DOWNGRADE);
+ }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Deleting %1% incompatible bundles") % updates.incompats.size();
+
+ for (const auto &incompat : updates.incompats) {
+ BOOST_LOG_TRIVIAL(info) << '\t' << incompat;
+ fs::remove(incompat.bundle);
+ }
+ }
+ else if (updates.updates.size() > 0) {
+ if (snapshot) {
+ BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
+ SnapshotDB::singleton().take_snapshot(*GUI::get_app_config(), Snapshot::SNAPSHOT_UPGRADE);
+ }
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Performing %1% updates") % updates.updates.size();
+
+ for (const auto &update : updates.updates) {
+ BOOST_LOG_TRIVIAL(info) << '\t' << update;
+
+ copy_file(update.source, update.target);
+
+ PresetBundle bundle;
+ bundle.load_configbundle(update.target.string(), PresetBundle::LOAD_CFGBNDLE_SYSTEM);
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Deleting %1% conflicting presets")
+ % (bundle.prints.size() + bundle.filaments.size() + bundle.printers.size());
+
+ auto preset_remover = [](const Preset &preset) {
+ BOOST_LOG_TRIVIAL(info) << '\t' << preset.file;
+ fs::remove(preset.file);
+ };
+
+ for (const auto &preset : bundle.prints) { preset_remover(preset); }
+ for (const auto &preset : bundle.filaments) { preset_remover(preset); }
+ for (const auto &preset : bundle.printers) { preset_remover(preset); }
+
+ // Also apply the `obsolete_presets` property, removing obsolete ini files
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Deleting %1% obsolete presets")
+ % (bundle.obsolete_presets.prints.size() + bundle.obsolete_presets.filaments.size() + bundle.obsolete_presets.printers.size());
+
+ auto obsolete_remover = [](const char *subdir, const std::string &preset) {
+ auto path = fs::path(Slic3r::data_dir()) / subdir / preset;
+ path += ".ini";
+ BOOST_LOG_TRIVIAL(info) << '\t' << path.string();
+ fs::remove(path);
+ };
+
+ for (const auto &name : bundle.obsolete_presets.prints) { obsolete_remover("print", name); }
+ for (const auto &name : bundle.obsolete_presets.filaments) { obsolete_remover("filament", name); }
+ for (const auto &name : bundle.obsolete_presets.filaments) { obsolete_remover("sla_material", name); }
+ for (const auto &name : bundle.obsolete_presets.printers) { obsolete_remover("printer", name); }
+ }
+ }
+}
+
+void PresetUpdater::priv::copy_file(const fs::path &source, const fs::path &target)
+{
+ static const auto perms = fs::owner_read | fs::owner_write | fs::group_read | fs::others_read; // aka 644
+
+ // Make sure the file has correct permission both before and after we copy over it
+ if (fs::exists(target)) {
+ fs::permissions(target, perms);
+ }
+ fs::copy_file(source, target, fs::copy_option::overwrite_if_exists);
+ fs::permissions(target, perms);
+}
+
+
+PresetUpdater::PresetUpdater(int version_online_event) :
+ p(new priv(version_online_event))
+{}
+
+
+// Public
+
+PresetUpdater::~PresetUpdater()
+{
+ if (p && p->thread.joinable()) {
+ // This will stop transfers being done by the thread, if any.
+ // Cancelling takes some time, but should complete soon enough.
+ p->cancel = true;
+ p->thread.join();
+ }
+}
+
+void PresetUpdater::sync(PresetBundle *preset_bundle)
+{
+ p->set_download_prefs(GUI::get_app_config());
+ if (!p->enabled_version_check && !p->enabled_config_update) { return; }
+
+ // Copy the whole vendors data for use in the background thread
+ // Unfortunatelly as of C++11, it needs to be copied again
+ // into the closure (but perhaps the compiler can elide this).
+ std::set<VendorProfile> vendors = preset_bundle->vendors;
+
+ p->thread = std::move(std::thread([this, vendors]() {
+ this->p->prune_tmps();
+ this->p->sync_version();
+ this->p->sync_config(std::move(vendors));
+ }));
+}
+
+void PresetUpdater::slic3r_update_notify()
+{
+ if (! p->enabled_version_check) { return; }
+
+ if (p->had_config_update) {
+ BOOST_LOG_TRIVIAL(info) << "New Slic3r version available, but there was a configuration update, notification won't be displayed";
+ return;
+ }
+
+ auto* app_config = GUI::get_app_config();
+ const auto ver_slic3r = Semver::parse(SLIC3R_VERSION);
+ const auto ver_online_str = app_config->get("version_online");
+ const auto ver_online = Semver::parse(ver_online_str);
+ const auto ver_online_seen = Semver::parse(app_config->get("version_online_seen"));
+ if (! ver_slic3r) {
+ throw std::runtime_error("Could not parse Slic3r version string: " SLIC3R_VERSION);
+ }
+
+ if (ver_online) {
+ // Only display the notification if the version available online is newer AND if we haven't seen it before
+ if (*ver_online > *ver_slic3r && (! ver_online_seen || *ver_online_seen < *ver_online)) {
+ GUI::MsgUpdateSlic3r notification(*ver_slic3r, *ver_online);
+ notification.ShowModal();
+ if (notification.disable_version_check()) {
+ app_config->set("version_check", "0");
+ p->enabled_version_check = false;
+ }
+ }
+ app_config->set("version_online_seen", ver_online_str);
+ }
+}
+
+bool PresetUpdater::config_update() const
+{
+ if (! p->enabled_config_update) { return true; }
+
+ auto updates = p->get_config_updates();
+ if (updates.incompats.size() > 0) {
+ BOOST_LOG_TRIVIAL(info) << boost::format("%1% bundles incompatible. Asking for action...") % updates.incompats.size();
+
+ std::unordered_map<std::string, wxString> incompats_map;
+ for (const auto &incompat : updates.incompats) {
+ auto vendor = incompat.name();
+
+ const auto min_slic3r = incompat.version.min_slic3r_version;
+ const auto max_slic3r = incompat.version.max_slic3r_version;
+ wxString restrictions;
+ if (min_slic3r != Semver::zero() && max_slic3r != Semver::inf()) {
+ restrictions = wxString::Format(_(L("requires min. %s and max. %s")),
+ min_slic3r.to_string(),
+ max_slic3r.to_string()
+ );
+ } else if (min_slic3r != Semver::zero()) {
+ restrictions = wxString::Format(_(L("requires min. %s")), min_slic3r.to_string());
+ } else {
+ restrictions = wxString::Format(_(L("requires max. %s")), max_slic3r.to_string());
+ }
+
+ incompats_map.emplace(std::make_pair(std::move(vendor), std::move(restrictions)));
+ }
+
+ p->had_config_update = true; // This needs to be done before a dialog is shown because of OnIdle() + CallAfter() in Perl
+
+ GUI::MsgDataIncompatible dlg(std::move(incompats_map));
+ const auto res = dlg.ShowModal();
+ if (res == wxID_REPLACE) {
+ BOOST_LOG_TRIVIAL(info) << "User wants to re-configure...";
+ p->perform_updates(std::move(updates));
+ GUI::ConfigWizard wizard(nullptr, GUI::ConfigWizard::RR_DATA_INCOMPAT);
+ if (! wizard.run(GUI::get_preset_bundle(), this)) {
+ return false;
+ }
+ GUI::load_current_presets();
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "User wants to exit Slic3r, bye...";
+ return false;
+ }
+ }
+ else if (updates.updates.size() > 0) {
+ BOOST_LOG_TRIVIAL(info) << boost::format("Update of %1% bundles available. Asking for confirmation ...") % updates.updates.size();
+
+ std::unordered_map<std::string, std::string> updates_map;
+ for (const auto &update : updates.updates) {
+ auto vendor = update.name();
+ auto ver_str = update.version.config_version.to_string();
+ if (! update.version.comment.empty()) {
+ ver_str += std::string(" (") + update.version.comment + ")";
+ }
+ updates_map.emplace(std::make_pair(std::move(vendor), std::move(ver_str)));
+ }
+
+ p->had_config_update = true; // Ditto, see above
+
+ GUI::MsgUpdateConfig dlg(std::move(updates_map));
+
+ const auto res = dlg.ShowModal();
+ if (res == wxID_OK) {
+ BOOST_LOG_TRIVIAL(debug) << "User agreed to perform the update";
+ p->perform_updates(std::move(updates));
+
+ // Reload global configuration
+ auto *app_config = GUI::get_app_config();
+ GUI::get_preset_bundle()->load_presets(*app_config);
+ GUI::load_current_presets();
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "User refused the update";
+ }
+ } else {
+ BOOST_LOG_TRIVIAL(info) << "No configuration updates available.";
+ }
+
+ return true;
+}
+
+void PresetUpdater::install_bundles_rsrc(std::vector<std::string> bundles, bool snapshot) const
+{
+ Updates updates;
+
+ BOOST_LOG_TRIVIAL(info) << boost::format("Installing %1% bundles from resources ...") % bundles.size();
+
+ for (const auto &bundle : bundles) {
+ auto path_in_rsrc = p->rsrc_path / bundle;
+ auto path_in_vendors = p->vendor_path / bundle;
+ updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version());
+ }
+
+ p->perform_updates(std::move(updates), snapshot);
+}
+
+
+}