fix(cli): resolve inherited presets through vendor manifests (#15438)

* fix(cli): resolve inherited presets through vendor manifests

* fix(cli): resolve typeless inherited presets

Probe the configured preset collections when a preset JSON omits its type. Reject missing, cross-type, and duplicate identities instead of silently selecting a candidate.

* fix(cli): allow missing app config during preset resolution

* fix(cli): tolerate malformed app config during preset resolution
This commit is contained in:
Maximilian Ghazanfar
2026-09-08 12:12:55 +08:00
committed by GitHub
parent c61d2fe5d1
commit bcb4f17d9a
8 changed files with 718 additions and 44 deletions
+197 -24
View File
@@ -453,6 +453,158 @@ PresetBundle::PresetBundle()
this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options);
}
bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Type type,
const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_source_manifest)
{
if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent)
compatibility_rule = ForwardCompatibilitySubstitutionRule::EnableSilent;
else if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem)
compatibility_rule = ForwardCompatibilitySubstitutionRule::Disable;
auto collection_for_type = [](PresetBundle &bundle, Preset::Type preset_type) -> PresetCollection * {
switch (preset_type) {
case Preset::TYPE_PRINT: return &bundle.prints;
case Preset::TYPE_FILAMENT: return &bundle.filaments;
case Preset::TYPE_PRINTER: return &bundle.printers;
default: return nullptr;
}
};
PresetCollection *collection = collection_for_type(*this, type);
if (collection == nullptr) {
error = "Unsupported preset type";
return false;
}
const boost::filesystem::path source_path = boost::filesystem::absolute(source_file).lexically_normal();
auto find_loaded = [&](PresetBundle &bundle) -> const Preset * {
PresetCollection *loaded_collection = collection_for_type(bundle, type);
const Preset *resolved = nullptr;
for (const Preset &preset : loaded_collection->get_presets()) {
if (preset.file.empty())
continue;
boost::system::error_code ec;
const bool same_file = boost::filesystem::equivalent(source_path, boost::filesystem::path(preset.file), ec);
if (ec || !same_file)
continue;
if (resolved != nullptr) {
error = "Preset identity is ambiguous";
return nullptr;
}
resolved = &preset;
}
return resolved;
};
if (const Preset *resolved = find_loaded(*this)) {
config = resolved->config;
error.clear();
return true;
}
if (error == "Preset identity is ambiguous")
return false;
if (!allow_source_manifest) {
error = "Preset was not found in the loaded bundle";
return false;
}
// A manifest-backed source file can be resolved without requiring the vendor
// to have been copied into data_dir()/system. Find the nearest ancestor whose
// sibling manifest names it, then let the canonical vendor loader flatten the
// complete tree (including nested sub_path entries and library inheritance).
for (boost::filesystem::path vendor_dir = source_path.parent_path(); !vendor_dir.empty(); vendor_dir = vendor_dir.parent_path()) {
const std::string vendor_id = vendor_dir.filename().string();
if (vendor_id.empty())
continue;
const boost::filesystem::path root_dir = vendor_dir.parent_path();
const boost::filesystem::path manifest = root_dir / (vendor_id + ".json");
if (!boost::filesystem::is_regular_file(manifest))
continue;
const boost::filesystem::path manifest_relative = source_path.lexically_relative(vendor_dir);
if (manifest_relative.empty() || *manifest_relative.begin() == "..")
continue;
try {
PresetBundle library_bundle;
const PresetBundle *base_bundle = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
library_bundle.m_preserve_vendor_source_paths = true;
library_bundle.load_vendor_configs_from_json(root_dir.string(), ORCA_FILAMENT_LIBRARY, LoadSystem,
compatibility_rule, nullptr, false);
if (library_bundle.error_count() != 0) {
error = "OrcaFilamentLibrary contains invalid presets";
return false;
}
base_bundle = &library_bundle;
}
PresetBundle source_bundle;
source_bundle.m_preserve_vendor_source_paths = true;
source_bundle.load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem,
compatibility_rule, base_bundle, false);
if (source_bundle.error_count() != 0) {
error = "Vendor bundle contains invalid presets";
return false;
}
const Preset *resolved = find_loaded(source_bundle);
if (resolved == nullptr) {
if (error.empty())
error = "Source file is not an instantiated preset in its vendor manifest";
return false;
}
config = resolved->config;
error.clear();
return true;
} catch (const std::exception &ex) {
error = ex.what();
return false;
}
}
error = "Preset was not found in the loaded bundle";
return false;
}
bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type,
const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_source_manifest)
{
std::optional<std::pair<Preset::Type, DynamicPrintConfig>> resolved;
for (Preset::Type candidate_type : types_list(ptFFF)) {
DynamicPrintConfig candidate_config(config);
std::string candidate_error;
if (!resolve_preset_config(candidate_config, candidate_type, source_file, compatibility_rule,
candidate_error, allow_source_manifest)) {
if (candidate_error == "Preset identity is ambiguous") {
error = std::move(candidate_error);
return false;
}
continue;
}
if (resolved) {
error = "Preset type is ambiguous";
return false;
}
resolved.emplace(candidate_type, std::move(candidate_config));
}
if (!resolved) {
error = "Preset type could not be resolved";
return false;
}
type = resolved->first;
config = std::move(resolved->second);
error.clear();
return true;
}
PresetBundle::PresetBundle(const PresetBundle &rhs)
{
*this = rhs;
@@ -574,7 +726,8 @@ void PresetBundle::copy_files(const std::string& from)
}
PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule,
const PresetPreferences& preferred_selection/* = PresetPreferences()*/)
const PresetPreferences& preferred_selection/* = PresetPreferences()*/,
std::string *errors, bool read_only)
{
// First load the vendor specific system presets.
PresetsConfigSubstitutions substitutions;
@@ -585,16 +738,20 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward
const auto startup_t0 = std::chrono::steady_clock::now();
//BBS: change system config to json
std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule);
std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule, !read_only);
if (errors != nullptr)
*errors = errors_cummulative;
// BBS load preset from user's folder, load system default if
// BBS: change directories by design
std::string dir_user_presets = config.get("preset_folder");
if (dir_user_presets.empty()) {
load_user_presets(DEFAULT_USER_FOLDER_NAME, substitution_rule);
load_user_presets(DEFAULT_USER_FOLDER_NAME, substitution_rule, read_only);
} else {
load_user_presets(dir_user_presets, substitution_rule);
load_user_presets(dir_user_presets, substitution_rule, read_only);
}
if (errors != nullptr && errors->empty() && m_errors != 0)
*errors = "Preset loading reported " + std::to_string(m_errors) + " error(s)";
// Rewrite renamed compatible_printers / compatible_prints references before selection. Skipped
// in validation mode so the profile validator (has_errors -> check_preset_references) sees the
@@ -1010,18 +1167,26 @@ std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_n
return out;
}
PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule)
PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule, bool read_only)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry and user is: " << user;
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
fs::path user_folder(data_dir() + "/" + PRESET_USER_DIR);
if (!fs::exists(user_folder)) fs::create_directory(user_folder);
if (!fs::exists(user_folder)) {
if (read_only)
return substitutions;
fs::create_directory(user_folder);
}
std::string dir_user_presets = data_dir() + "/" + PRESET_USER_DIR + "/" + user;
fs::path folder(user_folder / user);
if (!fs::exists(folder)) fs::create_directory(folder);
if (!fs::exists(folder)) {
if (read_only)
return substitutions;
fs::create_directory(folder);
}
bundles.WriteLock();
bundles.m_bundles.clear();
@@ -1049,13 +1214,13 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id));
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.filament_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id));
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.printer_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id));
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
metadata.bundle_type = BundleType::Local;
metadata.path = metadata_file.string();
@@ -1085,13 +1250,13 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id));
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.filament_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id));
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.printer_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id));
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
metadata.bundle_type = BundleType::Subscribed;
metadata.path = metadata_file.string();
@@ -1110,17 +1275,20 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
const auto json_t0 = std::chrono::steady_clock::now();
try {
std::string sel = prints.get_selected_preset().name;
this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule);
this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule,
nullptr, PresetOrigin(), read_only);
prints.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
try {
std::string sel = filaments.get_selected_preset().name;
this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule);
this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule,
nullptr, PresetOrigin(), read_only);
filaments.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
try {
std::string sel = printers.get_selected_preset().name;
this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule);
this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule,
nullptr, PresetOrigin(), read_only);
printers.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative);
@@ -2266,7 +2434,8 @@ void PresetBundle::clear_printer_hold_aliases()
}
//BBS: add json related logic, load system presets from json
std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule)
std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache)
{
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule;
@@ -2288,7 +2457,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
// The vendors below are loaded whole and against each other — the filament
// library first, then every other vendor with it as the base — so each parse
// is complete enough to be worth caching.
m_generate_vendor_caches = m_generate_vendor_caches || ! validation_mode;
m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode);
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
@@ -2318,7 +2487,8 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
// state into this load.
this->clear_printer_hold_aliases();
this->m_errors = 0;
append(substitutions, this->load_vendor_configs_from_json(dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule).first);
append(substitutions, this->load_vendor_configs_from_json(
dir.string(), orca_lib_vendor, PresetBundle::LoadSystem, compatibility_rule, nullptr, allow_cache).first);
first = false;
} catch (const std::runtime_error &err) {
if (validation_mode)
@@ -2343,7 +2513,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
bundle->set_generate_vendor_caches(m_generate_vendor_caches);
try {
auto result = bundle->load_vendor_configs_from_json(
dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this);
dir.string(), other_vendors[i], PresetBundle::LoadSystem, compatibility_rule, this, allow_cache);
parallel_substitutions[i] = std::move(result.first);
parallel_bundles[i] = std::move(bundle);
} catch (const std::runtime_error &err) {
@@ -5280,9 +5450,11 @@ std::string PresetBundle::load_vendor_preset(
return reason;
}
auto file_path = (boost::filesystem::path(data_dir()) /PRESET_SYSTEM_DIR/ vendor_name / entry.sub_path).make_preferred();
if(validation_mode)
auto file_path = (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / vendor_name / entry.sub_path).make_preferred();
if (validation_mode)
file_path = (boost::filesystem::path(data_dir()) / vendor_name / entry.sub_path).make_preferred();
if (m_preserve_vendor_source_paths)
file_path = (boost::filesystem::path(path) / vendor_name / entry.sub_path).make_preferred();
// Load the preset into the list of presets, save it to disk.
Preset &loaded = presets_collection->load_preset(file_path.string(), preset_name, std::move(config), false);
@@ -5348,7 +5520,8 @@ std::string PresetBundle::load_vendor_preset(
//BBS: Load a config bundle file from json
std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_from_json(
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle)
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags,
ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle, bool allow_cache)
{
// Enable substitutions for user config bundle, throw an exception when loading a system profile.
ConfigSubstitutionContext substitution_context { compatibility_rule };
@@ -5366,7 +5539,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
// Orca: only a whole-vendor load has a cache — the vendor-only and filament-only
// scans want a slice of one. Validation reads the JSONs whatever is cached.
const boost::filesystem::path dir_path(dir);
const bool cacheable = flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly);
const bool cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly);
if (cacheable && ! validation_mode && this->load_vendor_cache(dir_path, vendor_name, base_bundle)) {
size_t presets_loaded = 0;
for (const PresetCollection* coll : std::initializer_list<const PresetCollection*>{