fix: Create Printer finds no system vendors in release builds (#16175)

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
Kris Austin
2026-10-07 17:30:36 -03:00
committed by GitHub
co-authored by Rodrigo Faselli
parent e027f8b225
commit c33515914d
6 changed files with 190 additions and 81 deletions
+6 -5
View File
@@ -39,7 +39,7 @@ presets are never serialized — they have their own storage and their own lifec
| Location | Contents on a shipped build | Role |
|---|---|---|
| `resources/profiles/` | `<vendor>.opc` alone — the profile and its preset JSONs both pruned | What the app ships with; what installing copies from, and the only thing it is read for |
| `resources/profiles/` | `<vendor>.opc` alone — the profile and its preset JSONs both pruned | What the app ships with and what installing copies from; read directly for vendors not installed |
| `<data_dir>/system/` | `<vendor>.opc` alone, or `<vendor>.json` + `<vendor>/` after an update | What the user has installed |
| `<data_dir>/system/` (dev build) | `<vendor>.json` + `<vendor>/` + `<vendor>.opc` written at runtime | A developer tree caches as it parses |
| `<data_dir>/cache/wizard_profile_data.json` | The wizard's derived vendor catalog plus the stamps it was built from | Written and read by the setup wizard only; never shipped (see "The wizard's profile-data cache") |
@@ -182,10 +182,11 @@ one startup.
**A vendor is loaded from where it is installed and nowhere else.** For startup that
is `<data_dir>/system/`; resources reaches the app by being *installed* into that
directory first, never by being loaded from. (The setup wizard is the one caller with
a different notion of "where": it also shows vendors the user has not installed, and
loads those from `resources/profiles` — see "The wizard's profile-data cache".) There
is one lookup tier and one parse source:
directory first, never by being loaded from. (The setup wizard and the Create Printer
dialog also offer vendors the user has not installed, and load those from
`resources/profiles`; see "The wizard's profile-data cache". The dialog's vendor-only and
filament-only scans read a cache only where it is the whole installation, and never write
one.) There is one lookup tier and one parse source:
```
load vendor V from <data_dir>/system:
+60 -52
View File
@@ -2806,19 +2806,13 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_mod
boost::filesystem::path dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) {
std::string vendor_file = dir_entry.path().string();
if (Slic3r::is_json_file(vendor_file)) {
std::string vendor_name = dir_entry.path().filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
try {
// Load the config bundle, flatten it.
append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
for (const std::string &vendor_name : vendor_names_in(dir)) {
try {
// Load the config bundle, flatten it.
append(substitutions, load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadVendorOnly, compatibility_rule).first);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
}
@@ -2841,35 +2835,33 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_fil
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
bool first = true;
for (auto &dir_entry : boost::filesystem::directory_iterator(dir)) {
std::string vendor_file = dir_entry.path().string();
if (Slic3r::is_json_file(vendor_file)) {
std::string vendor_name = dir_entry.path().filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
try {
if (first) {
// Reset this PresetBundle and load the first vendor config.
append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first);
first = false;
} else {
// Load the other vendor configs, merge them with this PresetBundle.
// Report duplicate profiles.
PresetBundle other;
append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first);
std::vector<std::string> duplicates = std::move(this->merge_presets({ &other }).front());
if (!duplicates.empty()) {
errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: ";
for (size_t i = 0; i < duplicates.size(); ++i) {
if (i > 0) errors_cummulative += ", ";
errors_cummulative += duplicates[i];
}
// The filament library loads first, so the filaments that inherit its bases resolve.
std::vector<std::string> vendor_names;
for (const std::string &vendor_name : vendor_names_in(dir))
vendor_names.insert(vendor_name == ORCA_FILAMENT_LIBRARY ? vendor_names.begin() : vendor_names.end(), vendor_name);
for (const std::string &vendor_name : vendor_names) {
try {
if (first) {
// Reset this PresetBundle and load the first vendor config.
append(substitutions, this->load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule).first);
first = false;
} else {
// Load the other vendor configs, merge them with this PresetBundle.
// Report duplicate profiles.
PresetBundle other;
append(substitutions, other.load_vendor_configs_from_json(dir.string(), vendor_name, PresetBundle::LoadSystem | PresetBundle::LoadFilamentOnly, compatibility_rule, this).first);
std::vector<std::string> duplicates = std::move(this->merge_presets({ &other }).front());
if (!duplicates.empty()) {
errors_cummulative += "Found duplicated settings in vendor " + vendor_name + "'s json file lists: ";
for (size_t i = 0; i < duplicates.size(); ++i) {
if (i > 0) errors_cummulative += ", ";
errors_cummulative += duplicates[i];
}
}
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
}
@@ -7149,22 +7141,38 @@ PresetBundle::VendorRead PresetBundle::read_vendor(const std::string& dir, const
// Reset this bundle, delete user profile files if SaveImported.
this->reset(flags.has(LoadConfigBundleAttribute::SaveImported));
// 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.
read.cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! flags.has(LoadConfigBundleAttribute::LoadFilamentOnly);
if (read.cacheable && ! validation_mode) {
// A vendor is loaded from where it is installed and nowhere else; resources
// reaches the app by being installed into `dir` first. The cache there is
// judged against the profile beside it — or, where the cache is the whole
// of the installation, against nothing, since nothing on disk can then be
// Orca: only a whole-vendor load writes a cache, and a vendor-only or filament-only
// scan reads one only where it is the whole installation, as in a shipped build.
// Validation reads the JSONs whatever is cached.
const bool vendor_only = flags.has(LoadConfigBundleAttribute::LoadVendorOnly);
const bool filament_only = flags.has(LoadConfigBundleAttribute::LoadFilamentOnly);
read.cacheable = allow_cache && flags.has(LoadConfigBundleAttribute::LoadSystem) && ! filament_only;
const boost::filesystem::path dir_path(dir);
const boost::filesystem::path profile = dir_path / (vendor_name + ".json");
const bool has_profile = boost::filesystem::exists(profile);
if (allow_cache && ! validation_mode && (read.cacheable || ((vendor_only || filament_only) && ! has_profile))) {
// The cache is judged against the profile beside it, or, where the cache is the
// whole of the installation, against nothing, since nothing on disk can then be
// newer than it. That state is Semver::inf(), which no real profile carries.
const boost::filesystem::path dir_path(dir);
const boost::filesystem::path profile = dir_path / (vendor_name + ".json");
const Semver version = boost::filesystem::exists(profile) ? get_version_from_json(profile.string()) : Semver::inf();
const Semver version = has_profile ? get_version_from_json(profile.string()) : Semver::inf();
read.cache_path = (dir_path / (vendor_name + ".opc")).string();
read.from_cache = VendorCacheFile::load(read.cache_path, vendor_name, version, read.data);
if (read.from_cache)
if (vendor_only) {
VendorProfile vendor_profile;
if (VendorCacheFile::load_vendor_profile(read.cache_path, vendor_name, version, vendor_profile)) {
this->vendors.emplace(vendor_name, std::move(vendor_profile));
read.vendor_only = true;
return read;
}
} else if (VendorCacheFile::load(read.cache_path, vendor_name, version, read.data)) {
read.from_cache = true;
if (filament_only) {
read.data.process_entries.clear();
read.data.machine_entries.clear();
// The stamp counts errors across the whole vendor.
read.data.parse_errors = 0;
}
return read;
}
}
this->parse_vendor_json(read);
return read;
+33 -9
View File
@@ -471,9 +471,11 @@ bool VendorCacheFile::save(const std::string& path, const std::string& vendor_na
}
}
// static
bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorCacheData& data)
// Reads a cache through its vendor profiles under the checks load() documents, then
// calls read_rest on the archive.
template<class ReadRest>
static bool read_cache_up_to_presets(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorMap& vendors, ReadRest&& read_rest)
{
std::string blob;
if (! read_cache_blob(path, blob))
@@ -488,12 +490,9 @@ bool VendorCacheFile::load(const std::string& path, const std::string& expected_
return false;
CacheDictionary dict;
dict.load(ar);
ar(data.vendors);
load_entries(ar, data.process_entries, dict);
load_entries(ar, data.filament_entries, dict);
load_entries(ar, data.machine_entries, dict);
ar(data.parse_errors);
if (data.vendors.find(expected_vendor_name) == data.vendors.end())
ar(vendors);
read_rest(ar, dict);
if (vendors.find(expected_vendor_name) == vendors.end())
throw std::runtime_error("vendor cache does not carry its own vendor profile");
return true;
} catch (const std::exception& e) {
@@ -502,6 +501,31 @@ bool VendorCacheFile::load(const std::string& path, const std::string& expected_
}
}
// static
bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorCacheData& data)
{
return read_cache_up_to_presets(path, expected_vendor_name, expected_vendor_version, data.vendors,
[&data](cereal::BinaryInputArchive& ar, const CacheDictionary& dict) {
load_entries(ar, data.process_entries, dict);
load_entries(ar, data.filament_entries, dict);
load_entries(ar, data.machine_entries, dict);
ar(data.parse_errors);
});
}
// static
bool VendorCacheFile::load_vendor_profile(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorProfile& vendor)
{
VendorMap vendors;
if (! read_cache_up_to_presets(path, expected_vendor_name, expected_vendor_version, vendors,
[](cereal::BinaryInputArchive&, const CacheDictionary&) {}))
return false;
vendor = std::move(vendors.find(expected_vendor_name)->second);
return true;
}
// static
std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name)
{
+5
View File
@@ -169,6 +169,11 @@ public:
static bool load(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorCacheData& data);
// Read only the vendor's own profile, under the same checks as load(),
// without deserializing its presets.
static bool load_vendor_profile(const std::string& path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, VendorProfile& vendor);
// Read the profile version a cache was stamped with, without deserializing
// its presets. Empty if the file is unreadable, not a cache this build
// understands, or not this vendor's. This is how an installed vendor's
+35 -15
View File
@@ -540,12 +540,9 @@ static std::string calculate_md5(const std::string &input)
return md5;
}
static std::string get_filament_id(std::string vendor_typr_serial)
// Loads every system filament and the user's own, which a new filament id must not collide with.
static void load_filament_id_sources(PresetBundle &temp_preset_bundle)
{
std::unordered_map<std::string, std::set<std::string>> filament_id_to_filament_name;
// temp filament presets
PresetBundle temp_preset_bundle;
temp_preset_bundle.load_system_filaments_json(Slic3r::ForwardCompatibilitySubstitutionRule::EnableSilent);
std::string dir_user_presets = wxGetApp().app_config->get("preset_folder");
if (dir_user_presets.empty()) {
@@ -553,6 +550,13 @@ static std::string get_filament_id(std::string vendor_typr_serial)
} else {
temp_preset_bundle.load_user_presets(dir_user_presets, ForwardCompatibilitySubstitutionRule::EnableSilent);
}
}
static std::string get_filament_id(std::string vendor_typr_serial, const PresetBundle &temp_preset_bundle)
{
std::unordered_map<std::string, std::set<std::string>> filament_id_to_filament_name;
// temp filament presets
const std::deque<Preset> &filament_presets = temp_preset_bundle.filaments.get_presets();
for (const Preset &preset : filament_presets) {
@@ -1193,7 +1197,9 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
if (wxID_YES != dlg.ShowModal()) { return; }
}
std::string user_filament_id = get_filament_id(filament_preset_name);
PresetBundle filament_id_sources;
load_filament_id_sources(filament_id_sources);
std::string user_filament_id = get_filament_id(filament_preset_name, filament_id_sources);
const wxString &curr_create_type = curr_create_filament_type();
@@ -2259,13 +2265,19 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre
} else {
selected_vendor_id = m_printer_preset_vendor_selected.id;
// The vendor list is built from the bundled profiles, so a vendor the user never
// installed loads from them.
const boost::filesystem::path vendor_dir = is_vendor_installed(selected_vendor_id) ?
boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR :
boost::filesystem::path(Slic3r::resources_dir()) / PRESET_PROFILES_DIR;
try {
// Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base
// bundle so vendor filaments that inherit OFL bases resolve via the existing
// cross-vendor inheritance path.
// Orca: served from the vendor's preset cache where one covers it — a shipped
// build carries that instead of the raw preset JSONs — and parsed otherwise.
temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(),
temp_preset_bundle.load_vendor_configs_from_json(vendor_dir.string(),
selected_vendor_id,
PresetBundle::LoadConfigBundleAttribute::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent,
@@ -2873,13 +2885,21 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
return;
}
// One load serves every clone below, since cloning assigns all filament ids before it saves a preset.
PresetBundle filament_id_sources;
if (!selected_filament_presets.empty())
load_filament_id_sources(filament_id_sources);
auto create_filament_id = [&filament_id_sources](std::string vendor_typr_serial) {
return get_filament_id(vendor_typr_serial, filament_id_sources);
};
std::vector<std::string> successful_preset_names;
if (curr_selected_preset_type == m_create_type.base_template) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " base template";
/****************************** clone filament preset ********************************/
std::vector<std::string> failures;
if (!selected_filament_presets.empty()) {
bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, get_filament_id, rewritten);
bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, create_filament_id, rewritten);
if (!create_preset_result) {
std::string message;
for (const std::string &failure : failures) { message += "\t" + failure + "\n"; }
@@ -2889,7 +2909,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
int res = dlg.ShowModal();
if (wxID_YES == res) {
create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name,
get_filament_id, true);
create_filament_id, true);
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but filament has same preset, user cancel create the printer preset";
return;
@@ -2905,7 +2925,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
if (!selected_process_presets.empty()) {
generate_process_presets_data(selected_process_presets, printer_nozzle_name);
bool create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name,
get_filament_id, rewritten);
create_filament_id, rewritten);
if (!create_preset_result) {
std::string message;
for (const std::string &failure : failures) { message += "\t" + failure + "\n"; }
@@ -2914,7 +2934,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
wxYES | wxYES_DEFAULT | wxCENTRE);
int res = dlg.ShowModal();
if (wxID_YES == res) {
create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, get_filament_id, true);
create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, create_filament_id, true);
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but process has same preset, user cancel create the printer preset";
return;
@@ -2926,7 +2946,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
/****************************** clone filament preset ********************************/
std::vector<std::string> failures;
if (!selected_filament_presets.empty()) {
bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, get_filament_id, rewritten);
bool create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, create_filament_id, rewritten);
if (!create_preset_result) {
std::string message;
for (const std::string& failure : failures) {
@@ -2937,7 +2957,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
wxYES | wxYES_DEFAULT | wxCENTRE);
int res = dlg.ShowModal();
if (wxID_YES == res) {
create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, get_filament_id, true);
create_preset_result = preset_bundle->filaments.clone_presets_for_printer(selected_filament_presets, failures, printer_preset_name, create_filament_id, true);
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but filament has same preset, user cancel create the printer preset";
return;
@@ -2948,7 +2968,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
/****************************** clone process preset ********************************/
failures.clear();
if (!selected_process_presets.empty()) {
bool create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, get_filament_id, rewritten);
bool create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, create_filament_id, rewritten);
if (!create_preset_result) {
std::string message;
for (const std::string& failure : failures) {
@@ -2957,7 +2977,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
MessageDialog dlg(this, _L("Create process presets failed. As follows:\n") + from_u8(message) + _L("\nDo you want to rewrite it?"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
int res = dlg.ShowModal();
if (wxID_YES == res) {
create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, get_filament_id, true);
create_preset_result = preset_bundle->prints.clone_presets_for_printer(selected_process_presets, failures, printer_preset_name, create_filament_id, true);
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " printer preset no same preset but filament has same preset, user cancel create the printer preset";
return;
@@ -6086,6 +6086,57 @@ TEST_CASE("A vendor updated over the air resolves against the library installed
}
}
TEST_CASE("The vendor and filament scans read the bundled vendors that ship as their cache alone", "[Preset][Bundle][Regression]")
{
ScopedTemporaryDir temp_dir;
const fs::path profiles = temp_dir.path() / "resources" / PRESET_PROFILES_DIR;
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
fs::create_directories(profiles / lib / "filament");
std::ofstream((profiles / (lib + ".json")).string())
<< R"({"version":"1.0.0","name":")" << lib << R"(",)"
<< R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})";
std::ofstream((profiles / lib / "filament" / "generic_pla.json").string())
<< R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false","filament_id":"GFL99"})";
fs::create_directories(profiles / "Acme" / "machine");
fs::create_directories(profiles / "Acme" / "filament");
std::ofstream((profiles / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme",)"
<< R"("machine_model_list":[{"name":"Acme One","sub_path":"machine/model.json"}],)"
<< R"("machine_list":[{"name":"Acme Printer","sub_path":"machine/printer.json"}],)"
<< R"("filament_list":[{"name":"Acme PLA","sub_path":"filament/pla.json"}]})";
std::ofstream((profiles / "Acme" / "machine" / "model.json").string())
<< R"({"type":"machine_model","name":"Acme One","nozzle_diameter":"0.4"})";
std::ofstream((profiles / "Acme" / "machine" / "printer.json").string())
<< R"({"type":"machine","name":"Acme Printer","from":"system","instantiation":"true","printer_model":"Acme One","printer_variant":"0.4"})";
std::ofstream((profiles / "Acme" / "filament" / "pla.json").string())
<< R"({"type":"filament","name":"Acme PLA","from":"system","instantiation":"true","inherits":"Generic PLA","filament_id":"P0000001"})";
auto installed = [](const PresetCollection &presets) {
return std::count_if(presets.get_presets().begin(), presets.get_presets().end(), [](const Preset &preset) { return !preset.is_default; });
};
auto scan = [&installed] {
std::vector<std::string> found;
PresetBundle models;
models.load_system_models_from_json(ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(installed(models.printers) + installed(models.prints) + installed(models.filaments) == 0);
for (const auto &[vendor_id, vendor] : models.vendors)
for (const VendorProfile::PrinterModel &model : vendor.models)
found.push_back(vendor_id + " model " + model.id);
PresetBundle filaments;
filaments.load_system_filaments_json(ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(installed(filaments.printers) + installed(filaments.prints) == 0);
for (const Preset &preset : filaments.filaments.get_presets())
if (!preset.is_default)
found.push_back(preset.name + " " + preset.filament_id);
return found;
};
const std::vector<std::string> expected{"Acme model Acme One", "Acme PLA P0000001"};
REQUIRE(scan() == expected);
reduce_vendors_to_caches(profiles, {lib, "Acme"});
CHECK(scan() == expected);
}
namespace {
// A default preset config for type, built the way PresetBundle builds its default presets.