Load profile includes so synced Bambu Lab presets get their G-code back

This commit is contained in:
SoftFever
2026-09-24 13:22:37 +08:00
parent d51ad889fd
commit 2b602943a5
14 changed files with 768 additions and 350 deletions
+17 -13
View File
@@ -76,9 +76,10 @@ and the count of errors the original parse hit.
Each entry is one preset **in source form**: what its JSON sub-file states and nothing
that resolving it derives — the preset's own config diff, the name of the preset it
inherits, and the parse metadata (name, sub-path, description, instantiation, setting
and filament ids, renames). Non-instantiated base presets are stored too; the children
that inherit from them cannot resolve without them.
inherits, the names of the presets it includes, and the parse metadata (name, sub-path,
description, instantiation, setting and filament ids, renames). Non-instantiated base
presets are stored too; the children that inherit from or include them cannot resolve
without them.
**The payload names its own keys.** The dictionary holds the distinct `opt_key`s the
file uses, the `ConfigOptionType` each was written as, and the distinct enum *value
@@ -161,9 +162,9 @@ cache nothing can invalidate is worse than no cache.
Vendors load in a fixed order, because filament inheritance crosses exactly one
boundary: any vendor's filament may inherit from the shared Orca filament library,
and nothing else reaches across vendors. The library therefore goes first, alone;
every other vendor follows in parallel, resolving against it; and the results are
merged in a stable order:
and nothing else reaches across vendors — an `include` is always vendor-local. The
library therefore goes first, alone; every other vendor follows in parallel, resolving
against it; and the results are merged in a stable order:
```mermaid
flowchart LR
@@ -211,13 +212,16 @@ and its cache was never written back.
Serving from a cache is not a memory-image restore. The entries are deserialized and
then installed one by one — inheritance resolved against the presets installed before
them and the currently loaded filament library, configs flattened onto the collection
defaults, validated and registered — by the same function the JSON path calls straight
after parsing a sub-file. The two paths share everything below the parse, which is what
makes a cache-loaded bundle indistinguishable from a JSON-loaded one by construction
rather than by test coverage. Installation also rebuilds each preset's file path from
the local data directory, so a shipped cache never carries the generating machine's
paths.
them and the currently loaded filament library, includes layered in, configs flattened
onto the collection defaults, validated and registered — by the same function the JSON
path calls straight after parsing a sub-file. An `include` layers what the included
base states, between the parent and the preset's own keys: the base's diff against the
default, taken when the base itself was installed and before the per-variant padding
`inherits` sees, so only what a template sets reaches the presets including it. The two
paths share everything below the parse, which is what makes a cache-loaded bundle
indistinguishable from a JSON-loaded one by construction rather than by test coverage.
Installation also rebuilds each preset's file path from the local data directory, so a
shipped cache never carries the generating machine's paths.
App upgrades work because a cache normally survives one. Only a deliberate
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
+253 -253
View File
@@ -1,7 +1,7 @@
{
"name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.08.00.09",
"version": "02.08.00.10",
"force_update": "0",
"description": "the initial version of BBL configurations",
"machine_model_list": [
@@ -67,6 +67,22 @@
"name": "fdm_filament_common",
"sub_path": "filament/fdm_filament_common.json"
},
{
"name": "fdm_filament_template_direct_bowden",
"sub_path": "filament/fdm_filament_template_direct_bowden.json"
},
{
"name": "fdm_filament_template_direct_bowden_e3d",
"sub_path": "filament/fdm_filament_template_direct_bowden_e3d.json"
},
{
"name": "fdm_filament_template_direct_dual",
"sub_path": "filament/fdm_filament_template_direct_dual.json"
},
{
"name": "fdm_filament_template_direct_dual_e3d",
"sub_path": "filament/fdm_filament_template_direct_dual_e3d.json"
},
{
"name": "fdm_filament_abs",
"sub_path": "filament/fdm_filament_abs.json"
@@ -8246,261 +8262,9 @@
{
"name": "Generic TPU @BBL H2S",
"sub_path": "filament/Generic TPU @BBL H2S.json"
},
{
"name": "fdm_filament_template_direct_bowden",
"sub_path": "filament/fdm_filament_template_direct_bowden.json"
},
{
"name": "fdm_filament_template_direct_bowden_e3d",
"sub_path": "filament/fdm_filament_template_direct_bowden_e3d.json"
},
{
"name": "fdm_filament_template_direct_dual",
"sub_path": "filament/fdm_filament_template_direct_dual.json"
},
{
"name": "fdm_filament_template_direct_dual_e3d",
"sub_path": "filament/fdm_filament_template_direct_dual_e3d.json"
}
],
"machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_bbl_3dp_001_common",
"sub_path": "machine/fdm_bbl_3dp_001_common.json"
},
{
"name": "fdm_bbl_3dp_002_common",
"sub_path": "machine/fdm_bbl_3dp_002_common.json"
},
{
"name": "Bambu Lab A1 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.4 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.4 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.4 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 0.8 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.8 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.2 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.2 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.6 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.6 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.8 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.8 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.8 nozzle.json"
},
{
"name": "Bambu Lab A1 0.4 nozzle template change_filament_gcode",
"sub_path": "machine/Bambu Lab A1 0.4 nozzle template change_filament_gcode.json"
@@ -8856,6 +8620,242 @@
{
"name": "Bambu Lab X2D 0.4 nozzle template time_lapse_gcode",
"sub_path": "machine/Bambu Lab X2D 0.4 nozzle template time_lapse_gcode.json"
},
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_bbl_3dp_001_common",
"sub_path": "machine/fdm_bbl_3dp_001_common.json"
},
{
"name": "fdm_bbl_3dp_002_common",
"sub_path": "machine/fdm_bbl_3dp_002_common.json"
},
{
"name": "Bambu Lab A1 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.4 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.4 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.4 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.4 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.4 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.4 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.4 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.4 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.4 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.4 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.4 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.4 nozzle.json"
},
{
"name": "Bambu Lab A1 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 0.8 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.2 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.2 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.6 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.6 nozzle.json"
},
{
"name": "Bambu Lab A1 mini 0.8 nozzle",
"sub_path": "machine/Bambu Lab A1 mini 0.8 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.2 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.2 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.6 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.6 nozzle.json"
},
{
"name": "Bambu Lab A2L 0.8 nozzle",
"sub_path": "machine/Bambu Lab A2L 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1P 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1P 0.8 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P1S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P1S 0.8 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.2 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.2 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.6 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.6 nozzle.json"
},
{
"name": "Bambu Lab P2S 0.8 nozzle",
"sub_path": "machine/Bambu Lab P2S 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1 Carbon 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1 Carbon 0.8 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.2 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.2 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.6 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.6 nozzle.json"
},
{
"name": "Bambu Lab X1E 0.8 nozzle",
"sub_path": "machine/Bambu Lab X1E 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2C 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2C 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D 0.8 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.2 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.2 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.6 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.6 nozzle.json"
},
{
"name": "Bambu Lab H2D Pro 0.8 nozzle",
"sub_path": "machine/Bambu Lab H2D Pro 0.8 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.2 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.2 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.6 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.6 nozzle.json"
},
{
"name": "Bambu Lab X2D 0.8 nozzle",
"sub_path": "machine/Bambu Lab X2D 0.8 nozzle.json"
}
],
"process_list": [
+44 -36
View File
@@ -437,6 +437,7 @@ def resolve_filament_field(name, field, filaments, ofl_filaments, seen=None, in_
the same hop semantics as resolve_filament_id: own value, else walk
`inherits` in the vendor map with OFL base-bundle fallback. Values are list
options — the first element counts; "" when the chain never defines one.
Templates pulled in by `include` are not consulted: none states either field.
"""
if seen is None:
seen = set()
@@ -887,8 +888,8 @@ def _vendor_json_files(vendor_path):
def check_preset_name_uniqueness(profiles_dir, vendor):
"""No two profiles in a bundle may share a type and a name, indexed or not.
The loader resolves "inherits" through a per-type map of the bundle's profiles
(PresetBundle.cpp load_subfiles), and std::map::emplace keeps the first
The loader resolves "inherits" and "include" through per-type maps of the bundle's
profiles (PresetBundle.cpp load_subfiles), and std::map::emplace keeps the first
insertion: a second file claiming the name is silently dropped, and which one
wins is nothing but index order. An unindexed twin counts too - it is one
sub_path edit away from deciding that silently.
@@ -1287,11 +1288,11 @@ def check_normalized(profiles_dir, vendor):
Those two commands define a profile file's canonical shape - identifying keys
first, keys the slicer no longer reads gone, filament options that are vectors
written as vectors - and a <vendor>.json's canonical lists, ordered parents-first
so the loader resolves every "inherits" in one pass. Running them over a
contributed bundle has to be a no-op; where it would not be, the file that was
reviewed is not the file that ships, and the next maintainer to run normalize
carries an unrelated diff into their own change.
written as vectors - and a <vendor>.json's canonical lists, ordered
dependencies-first so the loader resolves every "inherits" and "include" in one
pass. Running them over a contributed bundle has to be a no-op; where it would
not be, the file that was reviewed is not the file that ships, and the next
maintainer to run normalize carries an unrelated diff into their own change.
It asks the normalize and update-index sections below rather than restating what
they do, because a second definition of normal is free to drift from the one that
@@ -2034,9 +2035,9 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
cli_config.json carry no "type" - all of them stay. A file that cannot be parsed
is reported and kept: never delete what could not be read.
An unindexed file that some surviving profile names in "inherits" is kept too,
and reported, UNLESS an indexed profile already carries that name: "inherits" is
resolved by preset name, so the indexed one is the parent every child actually
An unindexed file that some surviving profile names in "inherits" or "include" is
kept too, and reported, UNLESS an indexed profile already carries that name: both
are resolved by preset name, so the indexed one is the parent every child actually
gets, and the unindexed file is a stale copy the loader never reaches. Where no
indexed profile provides the name the inheriting preset really is broken, and
deleting the file would destroy the only record of the settings it was written
@@ -2069,7 +2070,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
listed.add(posixpath.normpath(sub_path.replace("\\", "/")))
candidates = {} # path -> profile, for every unindexed preset
inherited = set() # every name the files that stay claim as a parent
inherited = set() # every name the files that stay claim as a parent or include
provided = {} # name -> sub_path, for the profiles the loader can see
for sub in subs:
for path in _walk_json(os.path.join(vendor_dir, sub)):
@@ -2084,8 +2085,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
if not isinstance(profile, dict):
continue
if sub_path in listed or profile.get("type") not in PROFILE_TYPES:
if profile.get("inherits"):
inherited.add(profile["inherits"])
inherited.update(profile_dependencies(profile))
if sub_path in listed and profile.get("name"):
provided[profile["name"]] = sub_path
continue
@@ -2104,8 +2104,7 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
for path, profile in rescued.items():
del candidates[path]
kept[path] = profile
if profile.get("inherits"):
inherited.add(profile["inherits"])
inherited.update(profile_dependencies(profile))
for path in sorted(kept):
print_warning(f"{_rel(path, profiles_dir)}: not indexed by {vendor}.json but "
@@ -2142,40 +2141,48 @@ def trim_profiles(profiles_dir=PROFILES_DIR, vendors=None, profile_types=None,
# update-index
# ---------------------------------------------------------------------------
def topological_sort(profiles):
"""Order index entries parents-first, so the loader resolves inherits in one pass.
def profile_dependencies(profile):
"""The names a profile needs loaded before it: its parent and every include."""
include = profile.get("include") or []
if isinstance(include, str):
include = [include]
return [name for name in [profile.get("inherits"), *include] if name]
Entries whose parent is not in the same section keep their own (sorted) order at
the end; the loader finds those parents through the base bundle instead.
def topological_sort(profiles):
"""Order index entries dependencies-first, so the loader resolves every
"inherits" and "include" in one pass.
Entries that neither depend on nor are depended on by another in the same section
go at the end in name order; the loader finds their parents, if any, through the
base bundle instead. Every entry on a dependency cycle, which no order can satisfy,
goes there too.
"""
graph = defaultdict(list)
in_degree = defaultdict(int)
by_name = {p["name"]: p for p in profiles}
all_names = set(by_name)
placed = set()
for profile in profiles:
parent = profile.get("inherits")
child = profile["name"]
if parent in all_names:
graph[parent].append(child)
in_degree[child] += 1
in_degree.setdefault(parent, 0)
placed.add(child)
placed.add(parent)
for parent in profile_dependencies(profile):
if parent in all_names:
graph[parent].append(child)
in_degree[child] += 1
in_degree.setdefault(parent, 0)
queue = sorted(name for name, degree in in_degree.items() if degree == 0)
result = []
while queue:
current = queue.pop(0)
result.append(by_name[current])
placed.add(current)
for child in sorted(graph[current]):
in_degree[child] -= 1
if in_degree[child] == 0:
queue.append(child)
result.extend(by_name[name] for name in sorted(all_names - placed))
ordered = {p["name"] for p in result}
result.extend(by_name[name] for name in sorted(all_names - ordered))
return result
@@ -2225,15 +2232,15 @@ def build_index_sections(profiles_dir, vendor, profile_types=None):
"name": name,
"sub_path": os.path.relpath(path, vendor_dir).replace(os.sep, "/"),
}
if profile.get("inherits"):
entry["inherits"] = profile["inherits"]
for key in ("inherits", "include"):
if profile.get(key):
entry[key] = profile[key]
by_name[name].append(entry["sub_path"])
entries.append(entry)
sorted_entries = topological_sort(entries)
for entry in sorted_entries:
entry.pop("inherits", None) # ordering input only, not part of the index
sections[profile_type + "_list"] = sorted_entries
# inherits/include were ordering input only; the index holds name and sub_path
sections[profile_type + "_list"] = [{"name": e["name"], "sub_path": e["sub_path"]}
for e in topological_sort(entries)]
for rel, found in sorted(unplaceable.items()):
problems.append(f'{rel}: type {found!r} is not one of {list(PROFILE_TYPES)}, so it '
@@ -2418,7 +2425,8 @@ def build_parser():
add("update-index", [vendor_opt, type_opt, dry_run_opt, profiles_opt],
"regenerate the *_list sections of <vendor>.json",
"Rebuild the *_list sections of each <vendor>.json from the files on disk,\n"
"ordered parents-first so the loader resolves inherits in one pass.\n"
"ordered dependencies-first so the loader resolves inherits and include\n"
"in one pass.\n"
"\n"
"A profile is indexed under the section its own \"type\" names, so run\n"
"normalize first: it is what writes a missing type. Two files claiming one\n"
+20
View File
@@ -411,6 +411,26 @@ class TestUpdateIndex(TreeCase):
for entry in self.t.read_index("V")["filament_list"]:
self.assertEqual(sorted(entry), ["name", "sub_path"])
def test_include_targets_are_listed_before_their_users(self):
# The loader resolves include like inherits: in one pass over the list, so
# a template must be listed before every preset that includes it - even
# though a template has no parent of its own to order it by.
self.t.write("V", "machine/P.json", {"type": "machine", "name": "P",
"include": ["T start", "T end"]})
self.t.write("V", "machine/T start.json", {"type": "machine", "name": "T start"})
self.t.write("V", "machine/T end.json", {"type": "machine", "name": "T end"})
self.t.write("V", "filament/F.json", {"type": "filament", "name": "F",
"inherits": "B", "include": "S"})
self.t.write("V", "filament/B.json", {"type": "filament", "name": "B"})
self.t.write("V", "filament/S.json", {"type": "filament", "name": "S"})
rc, out = self.run_command("update-index")
self.assertEqual(rc, 0, out)
machines = [e["name"] for e in self.t.read_index("V")["machine_list"]]
self.assertEqual(machines, ["T end", "T start", "P"])
filaments = [e["name"] for e in self.t.read_index("V")["filament_list"]]
self.assertLess(filaments.index("B"), filaments.index("F"))
self.assertLess(filaments.index("S"), filaments.index("F"))
def test_a_profile_with_no_usable_type_is_reported_not_dropped(self):
self.t.write("V", "filament/A.json", {"type": "filament", "name": "A"})
self.t.write("V", "filament/B.json", {"name": "B"})
+6
View File
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
# profile_include_dump: prints what included templates contribute to a vendor's presets,
# to diff against the same tool built in BambuStudio. Built only on request.
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
target_link_libraries(profile_include_dump libslic3r boost_headeronly)
target_compile_definitions(profile_include_dump PRIVATE ${_DEV_DEFS})
endif()
# Function that adds source file encoding check to a target
+215
View File
@@ -0,0 +1,215 @@
// Prints what the profile `include` key contributes to one vendor's system
// presets, so that the dump from OrcaSlicer and the one from BambuStudio can be
// diffed: the two copies of this file differ only in how they load the vendor.
// For every preset whose values pass through an included template, from its
// own file or an ancestor's, it prints the templates those files name, in the
// order they apply, then the loaded value of each key the templates set. Every
// line starts with the preset, so a plain diff names the preset of each
// difference.
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include "nlohmann/json.hpp"
#include <algorithm>
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace Slic3r;
namespace fs = boost::filesystem;
using nlohmann::json;
namespace {
// What a sub-file states about where its values come from, and what it sets.
struct Entry
{
std::string inherits;
std::vector<std::string> includes;
std::vector<std::string> keys;
};
// Sub-files by name, per section of the vendor index: `inherits` and `include`
// both resolve within a section.
using Sections = std::map<std::string, std::map<std::string, Entry>>;
json read_json(const fs::path &path)
{
boost::nowide::ifstream in(path.string());
if (!in)
throw std::runtime_error("Cannot read " + path.string());
return json::parse(in);
}
Sections read_vendor(const fs::path &dir, const std::string &vendor)
{
// Keys that name and place a sub-file rather than set a value.
static const std::set<std::string> metadata = {"name", "type", "from", "instantiation", "inherits", "include"};
Sections sections;
const json index = read_json(dir / (vendor + ".json"));
for (const char *section : {"process_list", "filament_list", "machine_list"}) {
const auto list = index.find(section);
if (list == index.end())
continue;
for (const json &item : *list) {
const json file = read_json(dir / vendor / item.at("sub_path").get<std::string>());
// A file without a name goes by its name in the index.
Entry &entry = sections[section][file.value("name", item.at("name").get<std::string>())];
entry.inherits = file.value("inherits", "");
if (const auto include = file.find("include"); include != file.end()) {
if (include->is_string())
entry.includes.push_back(include->get<std::string>());
else
for (const json &name : *include)
entry.includes.push_back(name.get<std::string>());
}
for (auto it = file.begin(); it != file.end(); ++it)
if (metadata.count(it.key()) == 0)
entry.keys.push_back(it.key());
}
}
return sections;
}
const PresetCollection &presets_of(const PresetBundle &bundle, const std::string &section)
{
if (section == "process_list")
return bundle.prints;
if (section == "filament_list")
return bundle.filaments;
return bundle.printers;
}
size_t dump(const PresetBundle &bundle, const Sections &sections, std::ostream &out)
{
size_t dumped = 0;
for (const auto &[section, entries] : sections) {
std::vector<const Preset *> presets;
for (const Preset &preset : presets_of(bundle, section).get_presets())
if (preset.is_system)
presets.push_back(&preset);
std::sort(presets.begin(), presets.end(), [](const Preset *a, const Preset *b) { return a->name < b->name; });
for (const Preset *preset : presets) {
// The preset and its ancestors, root first: the order their includes apply in.
std::vector<const Entry *> lineage;
for (auto it = entries.find(preset->name); it != entries.end() && lineage.size() <= entries.size();
it = entries.find(it->second.inherits))
lineage.insert(lineage.begin(), &it->second);
std::string templates;
std::set<std::string> keys;
for (const Entry *entry : lineage)
for (const std::string &name : entry->includes) {
templates += (templates.empty() ? "" : "; ") + name;
if (const auto it = entries.find(name); it != entries.end())
keys.insert(it->second.keys.begin(), it->second.keys.end());
}
if (templates.empty())
continue;
const std::string prefix = section.substr(0, section.find('_')) + " | " + preset->name + " | ";
out << prefix << "include = " << templates << "\n";
for (const std::string &key : keys)
out << prefix << key << " = " << (preset->config.has(key) ? preset->config.opt_serialize(key) : "<absent>") << "\n";
++dumped;
}
}
return dumped;
}
// Orca: load the vendor as the app does, against the filament library when the
// directory has one: parsed from its JSON files or, with from_cache, from the
// preset cache the app reads on every launch after the first.
void load_vendor(PresetBundle &bundle, const std::string &dir, const std::string &vendor, bool from_cache)
{
const auto rule = ForwardCompatibilitySubstitutionRule::EnableSilent;
PresetBundle library;
const PresetBundle *base = nullptr;
if (fs::is_regular_file(fs::path(dir) / (std::string(PresetBundle::ORCA_FILAMENT_LIBRARY) + ".json"))) {
library.load_vendor_configs_from_json(dir, PresetBundle::ORCA_FILAMENT_LIBRARY, PresetBundle::LoadSystem, rule, nullptr, false);
base = &library;
}
if (!from_cache) {
bundle.load_vendor_configs_from_json(dir, vendor, PresetBundle::LoadSystem, rule, base, false);
return;
}
// Parse once to write <vendor>.opc beside the profile, then load that
// cache into a clean bundle. Any cache already there goes first, so that a
// failed write cannot leave it to be loaded; one that was not there before
// goes again afterwards.
const fs::path cache = fs::path(dir) / (vendor + ".opc");
const bool had_cache = fs::exists(cache);
fs::remove(cache);
PresetBundle parsed;
parsed.set_is_validation_mode(true); // parse the JSON: validation never serves a cache
parsed.set_generate_vendor_caches(true);
parsed.load_vendor_configs_from_json(dir, vendor, PresetBundle::LoadSystem, rule, base);
const bool loaded = bundle.load_vendor_cache(cache.string(), vendor, parsed.vendors.at(vendor).config_version, base);
if (!had_cache)
fs::remove(cache);
if (!loaded)
throw std::runtime_error("The preset cache " + cache.string() + " was not written or was rejected");
}
} // namespace
int main(int argc, char *argv[])
{
const char *usage = "Usage: profile_include_dump -p <profiles dir> -o <dump file> [-v <vendor>] [-l <log level>] [-c]\n"
" -v vendor to dump, BBL if omitted\n"
" -l log level, 0 (fatal) to 5 (trace); 1 if omitted\n"
" -c load the presets from the vendor's preset cache, generated first, not the JSON\n";
std::string dir, output, vendor = "BBL";
unsigned log_level = 1;
bool from_cache = false;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "-c") {
from_cache = true;
continue;
}
if (i + 1 == argc) {
std::cerr << usage;
return 1;
}
const std::string value = argv[++i];
if (arg == "-p")
dir = value;
else if (arg == "-o")
output = value;
else if (arg == "-v")
vendor = value;
else if (arg == "-l" && value.size() == 1 && value[0] >= '0' && value[0] <= '5')
log_level = unsigned(value[0] - '0');
else {
std::cerr << usage;
return 1;
}
}
if (dir.empty() || output.empty() || !fs::is_directory(dir)) {
std::cerr << usage;
return 1;
}
set_logging_level(log_level);
try {
PresetBundle bundle;
load_vendor(bundle, dir, vendor, from_cache);
boost::nowide::ofstream out(output);
if (!out)
throw std::runtime_error("Cannot write " + output);
const size_t dumped = dump(bundle, read_vendor(dir, vendor), out);
out.close();
if (!out)
throw std::runtime_error("Failed writing " + output);
std::cerr << "Dumped " << dumped << " " << vendor << " presets that include a template\n";
} catch (const std::exception &ex) {
std::cerr << ex.what() << "\n";
return 1;
}
return 0;
}
+3
View File
@@ -950,6 +950,9 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
}
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
}
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
} else {
+1
View File
@@ -62,6 +62,7 @@
#define ORCA_JSON_KEY_UPDATE_TIME "updated_time"
#define ORCA_JSON_KEY_CREATED_TIME "created_time"
#define BBL_JSON_KEY_INHERITS "inherits"
#define BBL_JSON_KEY_INCLUDES "include"
#define BBL_JSON_KEY_INSTANTIATION "instantiation"
#define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter"
#define BBL_JSON_KEY_PRINTER_TECH "machine_tech"
+65 -20
View File
@@ -6598,7 +6598,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// now, or deserialized from the vendor's cache; the code is shared so a
// cache-loaded bundle cannot come out different from a JSON-loaded one.
// Resolves `inherits` against the presets loaded before this one
// (config_maps) or against base_bundle's filament library, flattens, validates
// (config_maps) or against base_bundle's filament library, layers each
// `include` (include_maps) under the preset's own keys, flattens, validates
// and registers the preset. Returns the reason loading failed, empty on
// success.
std::string PresetBundle::load_vendor_preset(
@@ -6607,9 +6608,10 @@ std::string PresetBundle::load_vendor_preset(
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs)
const std::set<std::string>* retain_configs, const std::set<std::string>* retain_includes)
{
const VendorProfile* current_vendor_profile = &this->vendors.at(vendor_name);
const std::string subfile = path + "/" + vendor_name + "/" + entry.sub_path;
@@ -6652,14 +6654,30 @@ std::string PresetBundle::load_vendor_preset(
return reason;
}
}
else {
if (presets_collection->type() == Preset::TYPE_PRINTER)
default_config = &presets_collection->default_preset_for(entry.config_src).config;
else
default_config = &presets_collection->default_preset().config;
}
else
default_config = &presets_collection->default_preset_for(entry.config_src).config;
config = *default_config;
// Layer each included preset's own keys over the parent, in the order listed;
// this preset's own keys go on top.
for (const std::string& name : entry.includes) {
auto it = include_maps.find(name);
if (it == include_maps.end()) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not find include " << name << " for " << preset_name;
continue;
}
config.apply(it->second);
}
config.apply(entry.config_src);
// Record what a base states, its diff against the default, for the presets
// that include it. It is taken before extend_default_config_length pads every
// per-variant key to the base's variant count: the padded defaults would
// otherwise override the values each includer inherits.
if (entry.instantiation == "false" && (retain_includes == nullptr || retain_includes->count(preset_name) != 0)) {
DynamicPrintConfig included;
included.apply_only(config, config.diff(presets_collection->default_preset_for(config).config));
include_maps.emplace(preset_name, std::move(included));
}
extend_default_config_length(config, true, *default_config);
if (entry.instantiation == "false" && "Template" != vendor_name) {
// Report configuration fields, which are misplaced into a wrong group.
@@ -7118,6 +7136,15 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
}
entry.name = key_values[BBL_JSON_KEY_NAME];
entry.description = key_values[BBL_JSON_KEY_DESCRIPTION];
// A file that states no instantiation and is named as G-code, or has no
// name, is a template that is only there to be included. A nameless one
// goes by its name in the vendor index.
if (auto it = key_values.find(BBL_JSON_KEY_INSTANTIATION);
(it == key_values.end() || it->second.empty()) && (entry.name.empty() || entry.name.find("gcode") != std::string::npos)) {
key_values[BBL_JSON_KEY_INSTANTIATION] = "false";
if (entry.name.empty())
entry.name = subfile_iter.first;
}
if(key_values.find(BBL_JSON_KEY_INSTANTIATION) == key_values.end())
{
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Missing instantiation attribute for " << entry.name;
@@ -7146,6 +7173,20 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
return reason;
}
}
if (auto it = key_values.find(BBL_JSON_KEY_INCLUDES); it != key_values.end()) {
// An array of names, or one bare name; load_from_json kept the JSON text.
nlohmann::json includes = nlohmann::json::parse(it->second);
if (!includes.is_array())
includes = nlohmann::json::array({std::move(includes)});
for (const auto& name : includes) {
if (name.is_string())
entry.includes.push_back(name.get<std::string>());
else {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": invalid include " << name.dump() << " for " << entry.name;
}
}
}
if (key_values.find(ORCA_JSON_KEY_RENAMED_FROM) != key_values.end()) {
if (!unescape_strings_cstyle(key_values[ORCA_JSON_KEY_RENAMED_FROM], entry.renamed_from)) {
BOOST_LOG_TRIVIAL(error) << "Error in a Config \"" << dir << "\": The preset \"" << entry.name
@@ -7162,7 +7203,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
return reason;
};
std::map<std::string, DynamicPrintConfig> configs;
std::map<std::string, DynamicPrintConfig> configs, include_maps;
std::map<std::string, std::string> filament_id_maps;
// Orca: whether to (re)write the vendor's cache after this parse, leaving it
// in step with the profile so the next run reads it instead. It is written
@@ -7179,6 +7220,7 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
auto load_subfiles = [&](std::vector<std::pair<std::string, std::string>>& subfiles,
std::vector<CachedPreset>& entries, const char* kind, bool is_from_lib = false) {
configs.clear();
include_maps.clear();
filament_id_maps.clear();
for (auto& subfile : subfiles) {
CachedPreset entry;
@@ -7186,8 +7228,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
if (reason.empty()) {
const int errors_before_install = m_errors;
reason = load_vendor_preset(entry, dir, vendor_name, base_bundle, flags,
substitution_context, substitutions, configs, filament_id_maps, presets,
presets_loaded, is_from_lib);
substitution_context, substitutions, configs, include_maps, filament_id_maps,
presets, presets_loaded, is_from_lib);
install_errors += m_errors - errors_before_install;
}
if (!reason.empty()) {
@@ -7982,26 +8024,29 @@ bool PresetBundle::load_vendor_cache(const std::string& cache_path, const std::s
// parsed), so no substitutions are reported, as before.
ConfigSubstitutionContext substitution_context { ForwardCompatibilitySubstitutionRule::EnableSilent };
PresetsConfigSubstitutions substitutions;
std::map<std::string, DynamicPrintConfig> configs;
std::map<std::string, DynamicPrintConfig> configs, include_maps;
std::map<std::string, std::string> filament_id_maps;
const std::string path = boost::filesystem::path(cache_path).parent_path().string();
size_t count = 0;
auto install_entries = [&](const std::vector<CachedPreset>& entries, PresetCollection* presets, bool is_from_lib) {
configs.clear();
include_maps.clear();
filament_id_maps.clear();
// Only configs of presets that other entries inherit are ever looked
// up again; registering just those skips one full config copy for
// every leaf preset. The library's filaments are all retained — they
// become the m_config_maps other vendors resolve against.
std::set<std::string> inherited;
for (const CachedPreset& entry : entries)
// Only configs of presets that other entries inherit or include are
// ever looked up again; registering just those skips one full config
// copy for every leaf preset. The library's filaments are all retained
// — they become the m_config_maps other vendors resolve against.
std::set<std::string> inherited, included;
for (const CachedPreset& entry : entries) {
if (! entry.inherits.empty())
inherited.insert(entry.inherits);
included.insert(entry.includes.begin(), entry.includes.end());
}
const std::set<std::string>* retain_configs = is_from_lib ? nullptr : &inherited;
for (const CachedPreset& entry : entries) {
const std::string reason = load_vendor_preset(entry, path, vendor_name,
base_bundle, LoadConfigBundleAttribute::LoadSystem, substitution_context, substitutions,
configs, filament_id_maps, presets, count, is_from_lib, retain_configs);
configs, include_maps, filament_id_maps, presets, count, is_from_lib, retain_configs, &included);
if (! reason.empty())
throw std::runtime_error("entry " + entry.name + " failed to install: " + reason);
}
+12 -10
View File
@@ -634,22 +634,24 @@ private:
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Load one source-form preset entry into this bundle: resolve `inherits`,
// flatten, validate and register the preset. Returns the reason loading
// failed, empty on success. See the definition for the sharing contract
// between the JSON parse and the cache load.
// retain_configs, when non-null, names the only presets registered into
// config_maps (a full config copy each). The cache load passes the names its
// entries inherit — the only ones ever looked up again; the JSON parse
// retains all, not knowing what later subfiles inherit.
// Load one source-form preset entry into this bundle: resolve `inherits`
// and `include`, flatten, validate and register the preset. Returns the
// reason loading failed, empty on success. See the definition for the
// sharing contract between the JSON parse and the cache load.
// retain_configs / retain_includes, when non-null, name the only presets
// registered into config_maps / include_maps (a config copy each). The
// cache load passes the names its entries inherit / include — the only
// ones ever looked up again; the JSON parse retains all, not knowing what
// later subfiles name.
std::string load_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr);
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
+2 -2
View File
@@ -254,7 +254,7 @@ constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
// save_entries below), or a change to the cache's own layout or the
// meaning of its stamps. Option-schema drift is NOT such a change — the
// dictionary handles it, which is why this no longer moves every release.
constexpr uint32_t CACHE_VERSION = 1;
constexpr uint32_t CACHE_VERSION = 2;
// A stamp-string read that refuses an absurd length before allocating anything.
// The stamps are read from files named from the outside (peek_version is
@@ -325,7 +325,7 @@ void visit_entry(Archive& ar, Entry& e, ConfigFn&& config)
{
ar(e.name, e.sub_path);
config();
ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
ar(e.inherits, e.includes, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
}
// The count comes from a file that has already passed magic and CRC, but a
+8 -6
View File
@@ -107,12 +107,13 @@ void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, con
// comes after.
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
// One preset as its JSON subfile states it: the config diff, the name of the
// preset it inherits, and the parse metadata — everything the parse phase of
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
// is resolved when the entry is installed, against whatever filament library
// is loaded then, so a cache carries no other vendor's values and no other
// vendor's update can make it stale.
// One preset as its JSON subfile states it: the config diff, the names of the
// preset it inherits and the presets it includes, and the parse metadata —
// everything the parse phase of load_vendor_configs_from_json extracts and
// nothing it derives. Inheritance and includes are resolved when the entry is
// installed, against whatever filament library is loaded then, so a cache
// carries no other vendor's values and no other vendor's update can make it
// stale.
// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every
// field below in this order — once, for the save, the load and the name peek alike.
struct CachedPreset
@@ -121,6 +122,7 @@ struct CachedPreset
std::string sub_path; // path under the vendor's directory
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
std::string inherits;
std::vector<std::string> includes; // layered under config_src, in this order
std::string description;
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
std::string setting_id;
+18
View File
@@ -490,6 +490,24 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
}
TEST_CASE("load_from_json hands a preset's include list to the caller instead of the config", "[Config]") {
ScopedTemporaryFile tmp(".json");
{
boost::nowide::ofstream ofs(tmp.string());
ofs << R"({"type":"machine","name":"P","instantiation":"true","include":["T start","T end"],"machine_end_gcode":"M84"})";
}
DynamicPrintConfig config;
ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable);
std::map<std::string, std::string> key_values;
std::string reason;
REQUIRE(config.load_from_json(tmp.string(), substitutions, false, key_values, reason) == 0);
CHECK(reason.empty());
CHECK(key_values["include"] == R"(["T start","T end"])");
CHECK_FALSE(config.has("include"));
CHECK(substitutions.unrecogized_keys.empty());
CHECK(config.opt_string("machine_end_gcode") == "M84");
}
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
DynamicPrintConfig config;
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
+104 -10
View File
@@ -53,41 +53,64 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std
}
// A small but complete vendor: one machine model, one process, a non-instantiated
// base filament with an instantiated child inheriting it, a second standalone
// filament carrying explicit metadata, and one machine preset with a rename — so
// the equivalence test below sees every CachedPreset field populated.
void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version)
// base filament with an instantiated child that inherits it and includes a
// dual-extruder template, a second standalone filament carrying explicit
// metadata, and one machine preset with a rename that includes a G-code template
// — so the equivalence test below sees every CachedPreset field populated. The
// templates are listed before the presets that include them, as update-index
// orders them, or after them when `templates_last` asks for the broken order.
void write_full_vendor_tree(const fs::path& dir, const std::string& vendor, const std::string& version,
bool templates_last = false)
{
fs::create_directories(dir / vendor / "process");
fs::create_directories(dir / vendor / "filament");
fs::create_directories(dir / vendor / "machine");
const std::string filament_template = R"({"name":")" + vendor + R"( dual template","sub_path":"filament/template.json"})";
const std::string filament_presets = R"({"name":")" + vendor + R"( Base PLA","sub_path":"filament/base.json"},)"
R"({"name":")" + vendor + R"( PLA @0.4","sub_path":"filament/pla.json"},)"
R"({"name":")" + vendor + R"( Silk PLA @0.4","sub_path":"filament/silk.json"})";
const std::string machine_template = R"({"name":")" + vendor + R"( 0.4 template machine_start_gcode","sub_path":"machine/start.json"})";
const std::string machine_presets = R"({"name":")" + vendor + R"( 0.4 nozzle","sub_path":"machine/printer.json"})";
std::ofstream((dir / (vendor + ".json")).string())
<< R"({"version":")" << version << R"(","name":")" << vendor << R"(",)"
<< R"("machine_model_list":[{"name":"Test Model","sub_path":"machine/model.json"}],)"
<< R"("process_list":[{"name":"0.20mm Standard @)" << vendor << R"(","sub_path":"process/standard.json"}],)"
<< R"("filament_list":[)"
<< R"({"name":")" << vendor << R"( Base PLA","sub_path":"filament/base.json"},)"
<< R"({"name":")" << vendor << R"( PLA @0.4","sub_path":"filament/pla.json"},)"
<< R"({"name":")" << vendor << R"( Silk PLA @0.4","sub_path":"filament/silk.json"}],)"
<< R"("machine_list":[{"name":")" << vendor << R"( 0.4 nozzle","sub_path":"machine/printer.json"}]})";
<< (templates_last ? filament_presets + "," + filament_template : filament_template + "," + filament_presets)
<< R"(],"machine_list":[)"
<< (templates_last ? machine_presets + "," + machine_template : machine_template + "," + machine_presets)
<< "]}";
std::ofstream((dir / vendor / "machine" / "model.json").string())
<< R"({"type":"machine_model","name":"Test Model","nozzle_diameter":"0.4"})";
std::ofstream((dir / vendor / "process" / "standard.json").string())
<< R"({"type":"process","name":"0.20mm Standard @)" << vendor
<< R"(","from":"system","instantiation":"true","layer_height":"0.2"})";
// The base sets two per-variant keys; the template restates one of them for
// two variants and adds a third; the child restates the third.
std::ofstream((dir / vendor / "filament" / "base.json").string())
<< R"({"type":"filament","name":")" << vendor
<< R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42"})";
<< R"( Base PLA","from":"system","instantiation":"false","filament_id":"GFA_base","filament_cost":"42",)"
<< R"("activate_air_filtration":["1"],"filament_max_volumetric_speed":["12"]})";
std::ofstream((dir / vendor / "filament" / "template.json").string())
<< R"({"type":"filament","name":")" << vendor << R"( dual template","from":"system","instantiation":"false",)"
<< R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)"
<< R"("filament_max_volumetric_speed":["20","22"],"filament_flush_temp":["0","0"]})";
std::ofstream((dir / vendor / "filament" / "pla.json").string())
<< R"({"type":"filament","name":")" << vendor
<< R"( PLA @0.4","from":"system","instantiation":"true","filament_id":"GFA00","filament_cost":"20",)"
<< R"("setting_id":"GFSA04","description":"Test PLA description"})";
std::ofstream((dir / vendor / "filament" / "silk.json").string())
<< R"({"type":"filament","name":")" << vendor
<< R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA"})";
<< R"( Silk PLA @0.4","from":"system","instantiation":"true","inherits":")" << vendor << R"( Base PLA",)"
<< R"("include":[")" << vendor << R"( dual template"],"filament_flush_temp":["5","5"]})";
// The template states two G-codes; the printer restates one of them.
std::ofstream((dir / vendor / "machine" / "start.json").string())
<< R"({"type":"machine","name":")" << vendor << R"( 0.4 template machine_start_gcode","from":"system",)"
<< R"("instantiation":"false","machine_start_gcode":"G28 ; template","machine_end_gcode":"M84 ; template"})";
std::ofstream((dir / vendor / "machine" / "printer.json").string())
<< R"({"type":"machine","name":")" << vendor
<< R"( 0.4 nozzle","from":"system","instantiation":"true","printer_model":"Test Model","printer_variant":"0.4",)"
<< R"("include":[")" << vendor << R"( 0.4 template machine_start_gcode"],"machine_end_gcode":"M84 ; own",)"
<< R"("renamed_from":")" << vendor << R"( old 0.4 nozzle"})";
}
@@ -598,6 +621,7 @@ TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "
const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->renamed_from == std::vector<std::string>{"Acme old 0.4 nozzle"});
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include
}
TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]")
@@ -1618,3 +1642,73 @@ TEST_CASE("a stamp string with an absurd length is rejected, not allocated", "[V
CHECK(VendorCacheFile::peek_version(cache, "Evil").empty());
}
TEST_CASE("an included template's keys land on the preset, between the parent's and its own", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0");
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(bundle.error_count() == 0);
const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // from the include
CHECK(pr->config.opt_string("machine_end_gcode") == "M84 ; own"); // the preset's own key wins
CHECK(presets_for(bundle.printers, "Acme").size() == 1); // the template is no preset
const Preset* silk = bundle.filaments.find_preset("Acme Silk PLA @0.4", false);
REQUIRE(silk != nullptr);
const auto* speed = silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed");
REQUIRE(speed != nullptr);
CHECK(speed->values == std::vector<double>{20., 22.}); // the include wins over the parent
const auto* flush = silk->config.option<ConfigOptionInts>("filament_flush_temp");
REQUIRE(flush != nullptr);
CHECK(flush->values == std::vector<int>{5, 5}); // the preset's own key wins
// A per-variant key the template never mentions keeps the parent's value.
// The loader pads every base to its variant count; an include taken from the
// padded copy would carry the padded default [0,0] over the parent's 1.
const auto* air = silk->config.option<ConfigOptionBools>("activate_air_filtration");
REQUIRE(air != nullptr);
CHECK(air->values == std::vector<unsigned char>{1, 1});
CHECK(presets_for(bundle.filaments, "Acme").size() == 2); // the template is no preset
}
TEST_CASE("an include listed after the preset that names it is an error, and the preset loads without it", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true);
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
// One error per unresolved include: the printer's and the filament's.
CHECK(bundle.error_count() == 2);
const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template");
const Preset* silk = bundle.filaments.find_preset("Acme Silk PLA @0.4", false);
REQUIRE(silk != nullptr);
CHECK(silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>{12.});
}
TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]")
{
InstallDirs dirs;
write_full_vendor_tree(dirs.system, "Acme", "1.0.0");
const fs::path start = dirs.system / "Acme" / "machine" / "start.json";
SECTION("named as G-code") {
std::ofstream(start.string()) << R"({"type":"machine","name":"Acme 0.4 template machine_start_gcode","from":"system",)"
<< R"("machine_start_gcode":"G28 ; template"})";
}
SECTION("not named, so included by its name in the vendor index") {
std::ofstream(start.string()) << R"({"type":"machine","from":"system","machine_start_gcode":"G28 ; template"})";
}
PresetBundle bundle;
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
CHECK(bundle.error_count() == 0);
const Preset* pr = bundle.printers.find_preset("Acme 0.4 nozzle", false);
REQUIRE(pr != nullptr);
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
CHECK(presets_for(bundle.printers, "Acme").size() == 1);
}