Hold every filament product to exactly one id, with no exceptions

filament_id is the plain mint of the product triple (filament_vendor,
filament_type, filament name), and nothing else feeds it. The tooling used to
accept any salt iteration of a preset's own triple, and its minting policy
stepped past ids that other products held in the tree or in the snapshot, so
which id a product got could depend on history. Every "salt split" in the tree
masked a redundant preset rather than a real need, and no shipped id is
salted, so salting goes entirely: no salt parameter, no id policy object, and
--generate no longer reads the snapshot.

--check now holds every declared and every inherited id to that one value and
lists each preset that misses it, variants under a wrong root included, instead
of folding them into the root's error. Two products whose triples mint one id
is reported as a collision naming both, and --generate refuses to write it;
the remedy is a rename so the triples differ. The Bambu catalog map generator
keys its rows by the same mint rather than by what the tree already ships.

No id changes: all 913 ids in the tree are already the mint of their triple,
so --generate is a no-op and the snapshot is untouched.
This commit is contained in:
SoftFever
2026-09-07 14:53:06 +08:00
parent b394ca891f
commit 9e1b000e7f
5 changed files with 277 additions and 419 deletions

View File

@@ -129,7 +129,7 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame
| Color | never a new id |
| Second diameter of the same product (1.75 + 2.85) | sibling filament, new id |
| "High-speed" tuned for a *different printer model* | same id (it is a printer variant) |
| "High-speed" selectable *alongside* the normal preset on one printer | new id (it is a product line) |
| "High-speed" selectable *alongside* the normal preset on one printer | new name, so a new id (it is a product line) |
## Structure rules
@@ -149,7 +149,9 @@ key needed). Tuning a generic material → **join the OrcaFilamentLibrary filame
are both the filament `MyBrand PLA`.
3. **Within one filament, variants' `compatible_printers` are pairwise disjoint** — per printer,
at most one compatible instantiated preset per id, or AMS matching turns ambiguous. The
C++ validator's `-f` check enforces this.
C++ validator's `-f` check enforces this, tree-wide in CI. Since one product carries one id
and cannot be split onto two, this rule is the *only* remedy for such an ambiguity: narrow
the `compatible_printers`, or retire the preset that duplicates another.
4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits
`Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library
preset on your printers, and it is what makes its triple — and so its id — the library's)
@@ -194,12 +196,17 @@ Snapmaker bundles alike; the OFL generic `Generic/PLA/Generic PLA` mints `OFDSrz
by 35 bundles — most by independent declarations converging on the same mint, the rest
purely through inheritance from the OFL preset.
On the rare collision with an existing id, the minter salts the input (`…/1`,
`…/2`, …) until free, and the result is frozen in the profile file. Salting is also used
deliberately: a *salt split* keeps two presets of one product on distinct ids where a single
id would be AMS-ambiguous on the same printer — the "selectable alongside" situation from the
table above, resolved without inventing a second filament name. The tooling recognizes salt
iterations of a triple as conformant and preserves such splits across re-mints.
Nothing but the triple feeds the mint — not the rest of the tree, not the snapshot, not what
another preset of the product happens to carry. Determined triple, determined id: one product
carries one id and there is no second acceptable value for it, so any other value on a preset
is a mismatch `--check` reports and `--generate` pulls back. Two *different* products whose
triples mint the same base62 value would be a collision (a roughly 36-bit id space against a
few thousand products); nothing salts past it: `--check` reports it naming both products,
`--generate` refuses to write it, and the remedy is a rename so their triples differ. Where
two presets of one product would be AMS-ambiguous on a printer, the fix is likewise in the
profiles — make their `compatible_printers` disjoint (structure rule 3), retire the redundant
preset, or, if they really are different products, give them different names so their triples
differ. Never a second id for one triple.
Workflow for a new filament:
@@ -217,10 +224,10 @@ python scripts/orca_extra_profile_check.py # 6. ...and everything else CI
`(filament_vendor, filament_type, filament name)` triple: it inserts one where an instantiated
filament resolves none, and re-derives one that does not match. A preset that *inherits* a
mismatching id is the one case left to the author — check 3b names it, and the fix is to inherit
a preset of the same filament or to give the preset its own key. It converges on an id the same
product already holds and salts only past ids *other* products hold, so an id already equal to a
salt iteration of its own triple is left alone and deliberate salt splits survive. The same run
assigns `generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process
a preset of the same filament or to give the preset its own key. A declaration is left alone
exactly when it already equals the one id its triple mints, and a collision (check 3d) is
reported and left unwritten. The same run assigns
`generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process
and machine preset of every vendor except BBL, which keeps its authoritative `G*` ids, strips
`setting_id` from base profiles, and fixes the misspelled `settings_id` key — dropped, or, for
BBL, whose ids have no formula to fall back on, restored under the correct name. It is idempotent and
@@ -231,9 +238,9 @@ check CI runs over both id kinds, of which `--check` is the `filament_id` half.
- `--filament-id` limits the run to `filament_id`.
- `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to
write both.
- `--vendor VENDOR` confines the run to that bundle; repeatable. The ids are still derived
tree-wide, so a narrowed run writes exactly what a full one would — and reports any duplicate
it was not allowed to clear, since only a run covering both bundles can.
- `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the
triple alone, so a narrowed run writes exactly what a full one would; `--check` reports
whatever it left outside.
- `--dry-run` reports what `--generate` would do and writes nothing; with no mode of its own it
implies `--generate`, so `--dry-run --vendor <Vendor>` previews just that bundle.
- `--profiles DIR` points the tooling at a different profile tree (default
@@ -309,10 +316,10 @@ one-to-one in both directions.
it from **BambuStudio's own shipped BBL bundle** — a sparse shallow clone of upstream `master`,
or `--bambustudio-dir <a BambuStudio resources/profiles checkout>`. Our BBL bundle is a fork of
Bambu's, tuned and extended independently, so it is not the source of truth for Bambu's ids.
A row's key is whatever id our tree already mints for that same
`(filament_vendor, filament_type, filament name)` triple; a product we do not ship gets a freshly
generated key and the row sits inert until some bundle claims that triple — `OFdyfQvU` /
`GFG03`, "Bambu PETG Matte", is such a row today.
A row's key is the id the product's `(filament_vendor, filament_type, filament name)` triple
mints — the same id any bundle of ours carries for it, since the id is a function of the triple
alone; the row of a product we do not ship sits inert until some bundle claims that triple —
`OFdyfQvU` / `GFG03`, "Bambu PETG Matte", is such a row today.
**Regenerate it in the same commit as every BBL profile sync**, and read the drift report it
prints. Two lines, both informational, neither blocking the write:
@@ -437,11 +444,12 @@ The checks, in brief:
its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change
surfaces as a snapshot diff.
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
mint (or a low salt iteration) of its declarer's own triple; the id an instantiated preset
*inherits* must equal the mint of *its* own triple, however it inherits it (a root, a real
filament, a library preset — structure rule 1); and every instantiated system filament must
resolve an effective id at all (recall: an id-less one is a hard load error in C++ that
discards the whole vendor bundle). The errors print the expected id.
one id its declarer's own triple mints, with no second acceptable value; the id an
instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits
it (a root, a real filament, a library preset — structure rule 1); and every instantiated
system filament must resolve an effective id at all (recall: an id-less one is a hard load
error in C++ that discards the whole vendor bundle); and no two products mint one id (a
base62 collision, resolved by renaming one of them). The errors print the expected id.
- **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor,
BBL and Qidi included.
- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and

View File

@@ -24,8 +24,11 @@ filament_id policy (see docs/HLSD/filament_id.md):
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
8 chars total, which satisfies the AMS length limit. Nobody invents ids by
hand; on the astronomically rare collision with another product's id the
input is salted ("/1", "/2", ...) until free and the result is frozen in file.
hand, and nothing but the triple feeds the mint — not the rest of the tree,
not the snapshot. Two products whose triples mint one id (a base62
collision; odds ~1e-5 over the whole tree) is an error --check reports and
--generate refuses to write; the remedy is a rename so the triples differ,
never a salted or hand-picked second id.
Identity changes (a filament rename, a filament_vendor/filament_type fix)
change the id BY DESIGN.
* Reserved id spaces that are never minted into or altered:
@@ -120,8 +123,6 @@ USER_CUSTOM_ID_RE = re.compile(r"^P[0-9A-Fa-f]{7}$", re.IGNORECASE)
# Filament name = preset base name: strip the first "@..." suffix. The space before
# "@" is optional because names like "Afinia PLA@HS" exist.
BASE_NAME_RE = re.compile(r"\s?@.*$")
# Salt iterations accepted by the identity check (check 3).
MAX_CHECK_SALT = 8
# A JSON string literal, for the byte-preserving key edits.
_JSON_STR = r'"(?:[^"\\]|\\.)*"'
@@ -190,43 +191,20 @@ def base_name(name):
return BASE_NAME_RE.sub("", name, count=1)
def generate_filament_id(filament_vendor, filament_type, filament_name, salt=0):
def generate_filament_id(filament_vendor, filament_type, filament_name):
"""Deterministic "OF" + 6-char base62 filament_id for a filament product.
input = "filament_product/<filament_vendor>/<filament_type>/<filament_name>"
(+ "/<salt>" when salted); u = uuid5(FILAMENT_ID_NAMESPACE, input); the id
tail is the low FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"),
most-significant first — the same derivation as generate_preset_setting_id.
The triple is the only input: no salt, no state, no second value.
input = "filament_product/<filament_vendor>/<filament_type>/<filament_name>";
u = uuid5(FILAMENT_ID_NAMESPACE, input); the id tail is the low
FILAMENT_ID_LENGTH base62 digits of int(u.bytes, "big"), most-significant
first — the same derivation as generate_preset_setting_id.
"""
key = f"filament_product/{filament_vendor}/{filament_type}/{filament_name}"
if salt:
key = f"{key}/{salt}"
u = uuid.uuid5(FILAMENT_ID_NAMESPACE, key)
return "OF" + _base62_tail(int.from_bytes(u.bytes, "big"), FILAMENT_ID_LENGTH)
def mint_filament_id(filament_vendor, filament_type, filament_name, taken):
"""Mint the product's id: the first salt iteration not in `taken`.
Bounded by MAX_CHECK_SALT, the last iteration the identity check accepts, so
the tool can never write an id its own --check would reject. Exhausting it
would take nine base62 collisions on one triple; it means something is wrong
with `taken`, not that a tenth salt is needed.
"""
for salt in range(MAX_CHECK_SALT + 1):
candidate = generate_filament_id(filament_vendor, filament_type, filament_name, salt)
if candidate not in taken:
return candidate
raise RuntimeError(
f"could not mint a free filament_id for {filament_vendor}/{filament_type}/"
f"{filament_name}: salts 0..{MAX_CHECK_SALT} are all taken")
def mint_iterations(triple):
"""The ids that count as the mint of `triple`: salt 0..MAX_CHECK_SALT."""
return {generate_filament_id(*triple, salt=s) for s in range(MAX_CHECK_SALT + 1)}
# ---------------------------------------------------------------------------
# Tree loading + loader-faithful effective-id resolution
# ---------------------------------------------------------------------------
@@ -439,12 +417,15 @@ def analyze_tree(profiles_dir):
triples = {} # fid -> set of triples of its declarers
declarer_triples = [] # (vendor, rec, fid, triple) per declarer
filament_triples = {} # (vendor, filament_name) -> {triple: [declarers]}
mints = {} # minted id -> triples minting it (declarers + instantiated)
for vendor, filaments in vendors.items():
occurring = vendor_ids.setdefault(vendor, set())
for rec in filaments.values():
triple = resolve_triple(rec["name"], filaments, ofl_filaments)
rec["triple"] = triple
if rec.get("filament_id") or rec["instantiation"]:
mints.setdefault(generate_filament_id(*triple), set()).add(triple)
if rec.get("filament_id"):
fid = rec["filament_id"]
occurring.add(fid)
@@ -466,16 +447,6 @@ def analyze_tree(profiles_dir):
if not rec.get("filament_id") and OF_ID_RE.match(eff):
inherited.append((vendor, rec, eff, triple))
# Identity (check 3b): an inherited id must be the mint of the preset's
# OWN triple. A declared id is held to the same rule as a declarer (3a),
# and an id whose declarer already fails 3a is reported there once, not
# again under every preset inheriting it.
unminted = {fid for _v, _r, fid, triple in declarer_triples
if OF_ID_RE.match(fid) and fid not in mint_iterations(triple)}
id_mismatches = [
(vendor, rec, eff, triple) for vendor, rec, eff, triple in inherited
if eff not in unminted and eff not in mint_iterations(triple)]
# Cross-bundle triple divergence (check 5, warning only): the same filament
# name declared in several bundles with different triples cannot converge
# on one id until the divergence is fixed.
@@ -495,12 +466,13 @@ def analyze_tree(profiles_dir):
"vendor_ids": vendor_ids,
"declared_ids": declared_ids,
"missing_effective": sorted(missing_effective),
"id_mismatches": id_mismatches,
"inherited": inherited,
"triples": {fid: sorted(list(t) for t in ts) for fid, ts in triples.items()},
"triple_sets": triples,
"declarer_triples": declarer_triples,
"filament_triples": filament_triples,
"cross_bundle_triples": cross_bundle_triples,
# id -> the products (triples) minting it, where there is more than one
"collisions": {fid: sorted(ts) for fid, ts in mints.items() if len(ts) > 1},
}
@@ -580,12 +552,14 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
2. Snapshot equality, both directions: every id in the tree, the filaments
claiming it and the triple its declarers resolve must equal the snapshot
entry exactly (the snapshot diff is the maintainer gate).
3. Identity: the id is a function of the triple alone. (a) A declared id
must equal the mint of the declarer's own triple or a salted iteration;
(b) the id an instantiated preset inherits must equal the mint of ITS
own triple — how it inherits it (a root, a real filament, an OFL
preset) is irrelevant; (c) every instantiated filament resolves an
effective id at all (an id-less one is a hard load error in C++).
3. Identity: the id is a function of the triple alone, and there is no
second acceptable value. (a) A declared id must equal the one id the
declarer's own triple mints; (b) the id an instantiated preset inherits
must equal the one ITS own triple mints — how it inherits it (a root, a
real filament, an OFL preset) is irrelevant; (c) every instantiated
filament resolves an effective id at all (an id-less one is a hard load
error in C++); (d) no two products mint one id (a base62 collision,
resolved by renaming one of them).
4. Reserved namespaces (GF*/QD_*/P-hex/"null", all ownerless) must not be
claimed by any vendor.
5. Triple integrity: (a) every declarer resolves non-empty filament_vendor
@@ -664,22 +638,36 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
errors += 1
# -- 3. identity: the id is a function of the triple alone ---------------
# One triple, one id: a declaration must carry exactly the mint of its
# triple, and there is no second acceptable value — not a salt, not a
# hand-picked one, not whatever another preset of the product carries. Two
# presets of one product that would be AMS-ambiguous on a printer are fixed
# in the profiles, by making their compatible_printers disjoint or by
# retiring the redundant one.
for vendor, rec, fid, triple in sorted(
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
if not OF_ID_RE.match(fid) or fid in mint_iterations(triple):
continue
want = generate_filament_id(*triple)
if not OF_ID_RE.match(fid) or fid == want:
continue # a non-OF id is check 1's error
print_error(
f'filament_id "{fid}" declared by "{rec["name"]}" ({rec["file"]}) does '
f'not match the mint of its triple "{"/".join(triple)}": expected '
f'"{generate_filament_id(*triple)}" (or a salted iteration); paste the '
f'expected id, or fix the triple and run "{GENERATE_CMD} --vendor {vendor}" '
f"(preview with --dry-run), then --update-snapshot")
f'"{want}"; paste the expected id, or fix the triple and run '
f'"{GENERATE_CMD} --vendor {vendor}" (preview with --dry-run), then '
f"--update-snapshot")
errors += 1
# (3b) An inherited id is held to the same single value, and every preset
# missing it is listed — a variant under a wrong root as much as a preset
# riding another product's root. Nothing is folded into the declarer's
# error: the report names each preset whose id is wrong.
for vendor, rec, eff, triple in sorted(
analysis["id_mismatches"], key=lambda x: (x[0], x[1]["file"])):
analysis["inherited"], key=lambda x: (x[0], x[1]["file"])):
want = generate_filament_id(*triple)
if eff == want:
continue
print_error(
f'preset "{rec["name"]}" ({rec["file"]}) inherits filament_id "{eff}" but '
f'its own triple "{"/".join(triple)}" mints "{generate_filament_id(*triple)}"; '
f'its own triple "{"/".join(triple)}" mints "{want}"; '
f"a preset carries the id of its own product: inherit a preset of the "
f"same filament, or declare its own key")
errors += 1
@@ -691,7 +679,15 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
f'instantiated filament "{name}" ({file}) resolves no filament_id anywhere '
f"in its inherits chain — this is a hard load error in the C++ loader; "
f'run "{GENERATE_CMD}" (expected id for filament '
f'"{vendor}/{base_name(name)}": "{expected}", salted if taken)')
f'"{vendor}/{base_name(name)}": "{expected}")')
errors += 1
# (3d) The mint is injective over the tree's products, or two of them are
# indistinguishable to every device that matches on the id.
for fid, ts in sorted(analysis["collisions"].items()):
print_error(
f'filament_id "{fid}" is the mint of {len(ts)} different products '
f'({"; ".join("/".join(t) for t in ts)}): a base62 collision; rename one '
f"of them so their triples differ")
errors += 1
# -- 4. reserved namespaces ----------------------------------------------
@@ -993,75 +989,36 @@ def rewrite_filament_id(path, old_id, new_id, dry_run=False):
# --generate
# ---------------------------------------------------------------------------
def make_want_id(analysis, snapshot=None):
"""Build the id policy: want_id(triple) -> the id that triple must carry.
A triple's id is its first mint iteration that no OTHER product holds. An id
is blocked when the tree or the snapshot records it under a triple set other
than exactly {triple}, or when this run already handed it to a different
triple. Same-triple reuse is therefore convergence — one product, one id in
every bundle — and salting only ever steps past another product's id.
Only a CONFORMANT record holds an id: a declaration whose value is not a mint
iteration of its own triple is transient — this run rewrites it — so it must
not block the product that legitimately mints the id it is squatting on. That
also makes the policy independent of which vendors a run writes, so a
--vendor-narrowed run picks the same ids as a full one.
"""
holders = {} # id -> the set of triples that legitimately hold it
for fid, ts in analysis["triple_sets"].items():
conformant = {t for t in ts if fid in mint_iterations(t)}
if conformant:
holders[fid] = conformant
for fid, entry in (snapshot or {}).get("ids", {}).items():
triple = tuple(snapshot_triple(entry))
if fid in mint_iterations(triple):
holders.setdefault(fid, set()).add(triple)
assigned = {} # triple -> the id chosen for it this run
run_taken = {} # id -> the triple this run gave it to
def want_id(triple):
if triple not in assigned:
blocked = {fid for fid, ts in holders.items() if ts != {triple}}
blocked |= {fid for fid, t in run_taken.items() if t != triple}
cand = mint_filament_id(*triple, taken=blocked)
assigned[triple] = cand
run_taken[cand] = triple
return assigned[triple]
return want_id
def _incomplete_triple(triple):
"""The name(s) of the empty mint-key fields, or "" when both are present."""
return " and ".join(k for k, v in (("filament_vendor", triple[0]),
("filament_type", triple[1])) if not v)
def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
vendors=None, dry_run=False, changed_paths=None):
def generate_filament_ids(profiles_dir=PROFILES_DIR, vendors=None, dry_run=False,
changed_paths=None):
"""Make every filament carry the id its own triple mints.
One rule, applied to declarations and to id-less filaments alike:
* a declared id that is not a mint iteration of the declarer's own triple —
a wrong OF id, or a foreign one such as a Bambu "GF*" arriving with an
upstream sync — is replaced in place;
* a declared id that is not the one its own triple mints — a wrong OF id,
or a foreign one such as a Bambu "GF*" arriving with an upstream sync —
is replaced in place;
* an instantiated filament that resolves no id at all gets one inserted
into its root(s): the id-less presets of the SAME filament its members
inherit, or the member itself (a parent of another filament cannot carry
this filament's id — check 3).
An id already equal to ANY salt iteration of its own triple is conformant
(check 3) and left alone, so deliberate salt splits — distinct presets of one
product kept apart for per-printer AMS matching — survive.
A declaration is left alone exactly when it already equals the one id its
triple mints (check 3). Two products minting one id (check 3d) are reported
and left unwritten: nothing salts past a collision, a rename resolves it.
`vendors` restricts what is WRITTEN; the analysis and the id policy always
span the whole tree, so a narrowed run mints exactly what a full one would.
`changed_paths`, when a set is passed, collects the files that changed. A
file whose layout offers no anchor for the edit is reported and counted as an
error, so one odd profile cannot abort the pass over all the others.
Never touches the snapshot — run --update-snapshot afterwards and review the
diff. Returns (files_changed, errors).
`vendors` restricts what is WRITTEN; the id is a function of the triple
alone, so a narrowed run writes exactly what a full one would, and --check
reports whatever it was not allowed to touch. `changed_paths`, when a set is
passed, collects the files that changed. A file whose layout offers no
anchor for the edit is reported and counted as an error, so one odd profile
cannot abort the pass over all the others. Never reads or touches the
snapshot — run --update-snapshot afterwards and review the diff. Returns
(files_changed, errors).
"""
_utf8_console()
analysis = analyze_tree(profiles_dir)
@@ -1080,24 +1037,26 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
print_error(f'unknown vendor "{v}" in {profiles_dir}')
return 0, errors + len(unknown)
want_id = make_want_id(analysis, load_snapshot(snapshot_path))
colliding = set() # triples no run may write an id for
for fid, ts in sorted(analysis["collisions"].items()):
print_error(
f'cannot write filament_id "{fid}": it is the mint of {len(ts)} different '
f'products ({"; ".join("/".join(t) for t in ts)}), a base62 collision; '
f"rename one of them so their triples differ")
errors += 1
colliding.update(ts)
verb = "would " if dry_run else ""
files_changed = 0
reminted = 0
inserted = 0
held_here = set() # ids the filaments this run may write legitimately hold
left_standing = {} # id -> files still declaring one non-conformantly
# 1. Declarations that are not the mint of their own triple.
for vendor, rec, fid, triple in sorted(
analysis["declarer_triples"], key=lambda x: (x[0], x[1]["file"])):
in_scope = wanted is None or vendor in wanted
if fid in mint_iterations(triple):
if in_scope:
held_here.add(fid)
continue
if not in_scope:
left_standing.setdefault(fid, []).append(rec["file"])
want = generate_filament_id(*triple)
if (fid == want or (wanted is not None and vendor not in wanted)
or triple in colliding):
continue
missing = _incomplete_triple(triple)
if missing:
@@ -1106,21 +1065,15 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
f'{missing}; the mint key needs both (generic materials use '
f'filament_vendor "Generic")')
errors += 1
left_standing.setdefault(fid, []).append(rec["file"])
continue
want = want_id(triple)
if fid == want:
continue
try:
rewrite_filament_id(rec["path"], fid, want, dry_run)
except (OSError, RuntimeError, ValueError) as e:
print_error(str(e))
errors += 1
left_standing.setdefault(fid, []).append(rec["file"])
continue
files_changed += 1
reminted += 1
held_here.add(want)
if changed_paths is not None:
# Index sub_paths are "/"-joined even on Windows, where the
# setting_id pass reaches the same file through os.walk: normalize
@@ -1166,6 +1119,8 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
errors += 1
continue
triple = (*next(iter(fields)), filament_name)
if triple in colliding:
continue # reported above
missing = _incomplete_triple(triple)
if missing:
print_error(
@@ -1174,7 +1129,7 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
f'filament_vendor "Generic")')
errors += 1
continue
new_id = want_id(triple)
new_id = generate_filament_id(*triple)
for name in sorted(roots):
root = roots[name]
try:
@@ -1185,24 +1140,11 @@ def generate_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH
continue
files_changed += 1
inserted += 1
held_here.add(new_id)
if changed_paths is not None:
changed_paths.add(os.path.normpath(root["path"]))
print_info(f'{verb}insert filament "{vendor}/{filament_name}": filament_id '
f'"{new_id}" -> {root["file"]}')
# A declaration that is not the mint of its own triple holds no id, so it
# never blocks the product the id belongs to — that is what makes a narrowed
# run mint exactly what a full one would. When the run is not allowed to
# rewrite that declaration, though, it stays behind on an id just handed to
# its rightful owner, and only a run that covers both can clear it.
for fid in sorted(held_here & set(left_standing)):
print_error(
f'filament_id "{fid}" belongs to a filament this run covers but is also '
f'declared by {", ".join(sorted(left_standing[fid]))}, which it did not '
f'rewrite; run "{GENERATE_CMD}" over both to clear the duplicate')
errors += 1
print_info(f"filament_ids inserted : {inserted}")
print_info(f"filament_ids re-minted : {reminted}")
return files_changed, errors
@@ -1376,10 +1318,10 @@ def build_parser():
narrow.add_argument("--setting-id", action="store_true",
help="write setting_id only, skipping filament_id")
narrow.add_argument("--vendor", metavar="VENDOR", action="append", default=[],
help="write only in this vendor bundle; repeatable. The ids are "
"still derived tree-wide, so a narrowed run writes exactly "
"what a full one would, and reports what it was not allowed "
"to fix")
help="write only in this vendor bundle; repeatable. The id is a "
"function of the triple alone, so a narrowed run writes "
"exactly what a full one would; --check reports whatever "
"it left outside")
parser.add_argument("--dry-run", "--dryrun", dest="dry_run", action="store_true",
help="report what would change and write nothing; with no mode of "
"its own it previews --generate")
@@ -1462,7 +1404,7 @@ def main(argv=None):
filament_files = errors = 0
if do_filament:
filament_files, e = generate_filament_ids(
args.profiles, snapshot_path, vendors, args.dry_run, changed)
args.profiles, vendors, args.dry_run, changed)
errors += e
if do_setting:
_n, e = generate_setting_ids(args.profiles, vendors, args.dry_run, changed)

View File

@@ -155,15 +155,13 @@ class SyntheticTree:
def assign(self, vendors=None, dry_run=False):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
changed, errors = afi.generate_filament_ids(
self.profiles, self.snapshot, vendors, dry_run)
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
return changed, errors, buf.getvalue()
def remint(self, vendors, dry_run=False):
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
changed, errors = afi.generate_filament_ids(
self.profiles, self.snapshot, vendors, dry_run)
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
return changed, errors, buf.getvalue()
def cli(self, *flags):
@@ -240,8 +238,6 @@ class TestMint(unittest.TestCase):
# layout ("filament_product/<vendor>/<type>/<name>") and base62 tail.
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA"),
"OF5CgdDq")
self.assertEqual(afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA",
salt=1), "OFD9mV8H")
self.assertEqual(afi.generate_filament_id("Generic", "PLA", "Generic PLA"),
"OFDSrzZ8")
@@ -262,95 +258,6 @@ class TestMint(unittest.TestCase):
self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PETG", "PolyLite PLA"))
self.assertNotEqual(base, afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA Pro"))
def test_salt_changes_id(self):
base = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA")
salted = afi.generate_filament_id("Polymaker", "PLA", "PolyLite PLA", salt=1)
self.assertNotEqual(base, salted)
self.assertRegex(salted, r"^OF[0-9A-Za-z]{6}$")
def test_mint_salt_iteration(self):
triple = ("Polymaker", "PLA", "PolyLite PLA")
taken = {afi.generate_filament_id(*triple, salt=s) for s in range(2)}
self.assertEqual(afi.mint_filament_id(*triple, taken),
afi.generate_filament_id(*triple, salt=2))
self.assertEqual(afi.mint_filament_id(*triple, set()),
afi.generate_filament_id(*triple))
def test_mint_never_returns_a_salt_the_identity_check_rejects(self):
# mint_filament_id stops at MAX_CHECK_SALT, the last iteration check 3
# accepts, so the tool can never write an id its own --check rejects.
triple = ("Polymaker", "PLA", "PolyLite PLA")
all_taken = afi.mint_iterations(triple)
self.assertEqual(len(all_taken), afi.MAX_CHECK_SALT + 1)
last = afi.mint_filament_id(*triple, all_taken - {afi.generate_filament_id(
*triple, salt=afi.MAX_CHECK_SALT)})
self.assertIn(last, afi.mint_iterations(triple))
with self.assertRaises(RuntimeError):
afi.mint_filament_id(*triple, all_taken)
def test_want_id_policy(self):
# The whole id policy: a triple's id is its first mint iteration that no
# OTHER product holds, in the tree or in the snapshot.
triple = ("Polymaker", "PLA", "PolyLite PLA")
other = ("Elegoo", "PETG", "Rapid PETG")
salt0 = afi.generate_filament_id(*triple)
salt1 = afi.generate_filament_id(*triple, salt=1)
def snapshot(**entries):
return {"ids": {fid: {"filaments": [], "name": t[2],
"filament_type": t[1], "filament_vendor": t[0]}
for fid, t in entries.items()}}
# nobody holds it: salt 0
self.assertEqual(afi.make_want_id({"triple_sets": {}})(triple), salt0)
# the tree holds it under this very triple: reuse is convergence
self.assertEqual(
afi.make_want_id({"triple_sets": {salt0: {triple}}})(triple), salt0)
# Another triple "holds" it, but salt0 is not a mint iteration of THAT
# triple, so the holder is a wrong declaration this very run rewrites.
# A transient squatter must not push a product off its own id.
self.assertEqual(
afi.make_want_id({"triple_sets": {salt0: {other}}})(triple), salt0)
self.assertEqual(
afi.make_want_id({"triple_sets": {salt0: {triple, other}}})(triple), salt0)
# The snapshot is read the same way: only a conformant record holds.
self.assertEqual(
afi.make_want_id({"triple_sets": {}}, snapshot(**{salt0: other}))(triple),
salt0)
# ... and the snapshot's own triple reuses it
self.assertEqual(
afi.make_want_id({"triple_sets": {}}, snapshot(**{salt0: triple}))(triple),
salt0)
# Unused here, but salting must still be reachable: see
# test_want_id_salts_past_a_genuine_collision.
self.assertNotEqual(salt0, salt1)
# memoized: one triple keeps one id for the whole run
want_id = afi.make_want_id({"triple_sets": {}})
self.assertEqual(want_id(triple), want_id(triple))
# ... and a second triple never gets the id this run already handed out
self.assertNotEqual(want_id(triple), want_id(other))
def test_want_id_salts_past_a_genuine_collision(self):
# Salting exists for one case only: two products whose triples mint the
# same id. That needs a base62 collision, so force one — salt 0 of every
# triple collapses to a single value while the salted iterations stay
# distinct. `other` then CONFORMANTLY holds the shared salt-0 id, and
# `triple` must step past it instead of stealing it.
triple = ("V", "PLA", "X")
other = ("W", "ABS", "Y")
real = afi.generate_filament_id
def colliding(vendor, ftype, name, salt=0):
return "OFcolid" if salt == 0 else real(vendor, ftype, name, salt)
afi.generate_filament_id = colliding
try:
self.assertIn("OFcolid", afi.mint_iterations(other)) # holder conforms
got = afi.make_want_id({"triple_sets": {"OFcolid": {other}}})(triple)
finally:
afi.generate_filament_id = real
self.assertEqual(got, real(*triple, salt=1))
class TestBaseName(unittest.TestCase):
def test_filament_name_derivation(self):
@@ -589,12 +496,13 @@ class TestChecks(OfCleanTreeCase):
self.assertIn('which records "AVendor/PLA/APLA"', out)
# Sanctioning the new triple is not enough: the old id is no longer its
# mint (check 3, nothing grandfathered) — the identity fix is a re-mint,
# reported once on the root, not again under the variant inheriting it.
# reported on the root and again under the variant inheriting it.
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertEqual(errors, 2, out)
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"APLA @P1" (VendorA/filament/APLA @P1.json) inherits filament_id', out)
_changed, errors, out = self.t.remint(["VendorA"])
self.assertEqual(errors, 0, out)
rc, _out = self.t.update_snapshot()
@@ -613,16 +521,6 @@ class TestChecks(OfCleanTreeCase):
self.assertIn("does not match the mint of its triple", out)
self.assertIn(afi.generate_filament_id("BV", "PLA", "BNEW"), out)
def test_check3_salted_mint_is_accepted(self):
salted = afi.generate_filament_id("BV", "PLA", "BNEW", salt=3)
self.t.write_preset("VendorA", preset("BNEW @base", filament_id=salted,
instantiation=False,
filament_vendor="BV", filament_type="PLA"))
self.t.write_preset("VendorA", preset("BNEW @P1", inherits="BNEW @base",
compatible_printers=["P1"]))
_errors, out = self.t.check() # check 2 still wants a snapshot update
self.assertNotIn("does not match the mint", out)
def test_check3_no_grandfathering_of_a_wrong_declaration(self):
# Sanctioning the tree does not excuse a declaration from its mint.
self.t.write_preset("VendorA", preset("CNEW @base", filament_id="OFZZZZZZ",
@@ -633,8 +531,9 @@ class TestChecks(OfCleanTreeCase):
rc, _out = self.t.update_snapshot()
self.assertEqual(rc, 0)
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
self.assertEqual(errors, 2, out) # the declaration, and the variant inheriting it
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"CNEW @P1" (VendorA/filament/CNEW @P1.json) inherits filament_id', out)
def test_check3_inherited_id_must_be_the_mint_of_own_triple(self):
# A preset of another filament inheriting APLA's root takes APLA's id,
@@ -654,6 +553,78 @@ class TestChecks(OfCleanTreeCase):
errors, out = self.t.check()
self.assertEqual(errors, 1, out)
def test_check3_lists_every_preset_inheriting_a_wrong_id(self):
# A wrong declaration is reported under every preset inheriting it, its
# own product's variant and another product alike: each one's effective
# id is not the mint of its own triple, and each is listed. Nothing is
# folded into the declarer's error.
self.t.write_preset("VendorA", preset("DNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="DV", filament_type="PLA"))
self.t.write_preset("VendorA", preset("DNEW @P1", inherits="DNEW @base",
compatible_printers=["P1"]))
self.t.write_preset("VendorA", preset("Other DNEW @P1", inherits="DNEW @base",
compatible_printers=["P1 0.4 nozzle"]))
errors, out = self.t.check()
self.assertIn("does not match the mint of its triple", out)
self.assertIn('"DNEW @P1" (VendorA/filament/DNEW @P1.json) inherits filament_id', out)
self.assertIn('"Other DNEW @P1" (VendorA/filament/Other DNEW @P1.json) inherits '
'filament_id', out)
# The unsanctioned id (check 2), the declaration (3a), and both presets
# inheriting it (3b).
self.assertEqual(errors, 4, out)
def test_check3_reports_an_inherited_mismatch_even_when_its_own_product_misdeclares_the_id(self):
# "Tuned PLA @P1" inherits APLA's root, so it carries APLA's id: wrong
# for its own product however the declarations around it are fixed.
# That "Tuned PLA @base" — its own product — misdeclares that same id
# is a second error, not a reason to leave the first unreported.
apla_id = afi.generate_filament_id("AVendor", "PLA", "APLA")
self.t.write_preset("VendorA", preset("Tuned PLA @base", filament_id=apla_id,
instantiation=False,
filament_vendor="AVendor",
filament_type="PLA"))
self.t.write_preset("VendorA", preset("Tuned PLA @P1", inherits="APLA @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertIn('"Tuned PLA @base" (VendorA/filament/Tuned PLA @base.json) does '
'not match the mint of its triple', out)
self.assertIn('"Tuned PLA @P1" (VendorA/filament/Tuned PLA @P1.json) inherits '
'filament_id', out)
# The unsanctioned claim and triple (check 2), the declaration (3a) and
# the inherited id (3b): four distinct errors, nothing folded away.
self.assertEqual(errors, 4, out)
def test_check3_reports_a_collision_between_two_products(self):
# Two products whose triples mint one id is a base62 collision. There
# is no salted or hand-picked second id to fall back on: the check
# names both products, and the remedy is a rename so the triples differ.
collide = {("V", "PLA", "X"), ("W", "ABS", "Y")}
real = afi.generate_filament_id
def colliding(vendor, ftype, name):
return "OFcolid0" if (vendor, ftype, name) in collide else real(vendor, ftype, name)
afi.generate_filament_id = colliding
self.addCleanup(setattr, afi, "generate_filament_id", real)
for vendor, ftype, name in sorted(collide):
self.t.write_preset("VendorA", preset(f"{name} @base", filament_id="OFcolid0",
instantiation=False,
filament_vendor=vendor,
filament_type=ftype))
self.t.write_preset("VendorA", preset(f"{name} @P1", inherits=f"{name} @base",
compatible_printers=["P1"]))
errors, out = self.t.check()
self.assertIn("collision", out)
self.assertIn("V/PLA/X", out)
self.assertIn("W/ABS/Y", out)
# Each declaration is the mint of its own triple, so the collision is
# the only identity error — no product is pushed off its id — and the
# unsanctioned id (check 2) is the only other one.
self.assertNotIn("does not match the mint", out)
self.assertNotIn("inherits filament_id", out)
self.assertEqual(errors, 2, out)
def test_check3_renamed_tuned_generic_is_an_identity_error(self):
# Riding the OFL generic under another base name: same rule, same error.
self.t.write_preset("VendorA", preset("Tuned PLA @P1",
@@ -1144,9 +1115,9 @@ class TestAssign(OfCleanTreeCase):
self.assertEqual(b2["filament_id"], want)
def test_converges_on_an_existing_tree_id_for_the_same_triple(self):
# BEHAVIOUR CHANGE: an id-less filament whose product is already shipped
# (with its conforming id) in another bundle converges on that id
# instead of salting past it. One product, one id, in every bundle.
# An id-less filament whose product is already shipped (with its
# conforming id) in another bundle converges on that id. One product,
# one id, in every bundle.
want = afi.generate_filament_id("CV", "PLA", "CPLA")
self.t.add_vendor("VendorB", [
preset("CPLA @base", filament_id=want, instantiation=False,
@@ -1166,9 +1137,8 @@ class TestAssign(OfCleanTreeCase):
def test_a_squatted_id_is_still_minted_for_its_own_product(self):
# VendorB's "Other" declares the id that belongs to VendorA's "DPLA" —
# a copy-paste, not a real claim, since it is not the mint of Other's
# own triple. That declaration is transient (--generate rewrites it), so
# it must NOT push DPLA onto a salted id: DPLA gets its canonical one.
# a copy-paste, not a claim. The id is the mint of DPLA's triple and
# nothing else, so DPLA gets it whatever VendorB carries.
want0 = afi.generate_filament_id("DV", "PLA", "DPLA")
self.t.add_vendor("VendorB", [
preset("Other @base", filament_id=want0, instantiation=False,
@@ -1180,27 +1150,51 @@ class TestAssign(OfCleanTreeCase):
self.t.write_preset("VendorA", preset("DPLA @P1", inherits="DPLA @base",
compatible_printers=["P1"]))
changed, errors, out = self.t.assign(["VendorA"])
# VendorB is outside the write set, so the id DPLA just took is still
# declared there: the run says so rather than leaving a silent duplicate.
self.assertEqual((changed, errors), (1, 1), out)
self.assertIn("declared by VendorB/filament/Other @base.json", out)
# ...and keeps saying so until a run that covers VendorB clears it.
self.assertEqual(self.t.assign(["VendorA"])[:2], (0, 1))
self.assertEqual((changed, errors), (1, 0), out)
root = load_json_file(self.t.preset_path("VendorA", "DPLA @base"))
self.assertEqual(root["filament_id"], want0)
# A full run re-mints Other to its own triple and the duplicate is gone.
other = load_json_file(self.t.preset_path("VendorB", "Other @base"))
self.assertEqual(other["filament_id"], want0)
changed, errors, out = self.t.assign()
# VendorB's declaration is VendorB's own mismatch: a run not allowed to
# touch it leaves it to --check, and a run over VendorB re-mints it.
_errors, out = self.t.check()
self.assertIn('"Other @base" (VendorB/filament/Other @base.json) does not match '
'the mint of its triple', out)
changed, errors, out = self.t.assign(["VendorB"])
self.assertEqual((changed, errors), (1, 0), out)
other = load_json_file(self.t.preset_path("VendorB", "Other @base"))
self.assertEqual(other["filament_id"],
afi.generate_filament_id("OV", "ABS", "Other"))
def test_generate_refuses_to_write_into_a_collision(self):
# "X" ships on the id its triple mints; a new product "Y" whose triple
# mints the very same id is a base62 collision. The run does not salt
# past it: it reports both products, writes nothing for "Y", and leaves
# the remedy — a rename, so the triples differ — to the author.
collide = {("V", "PLA", "X"), ("W", "ABS", "Y")}
real = afi.generate_filament_id
def colliding(vendor, ftype, name):
return "OFcolid0" if (vendor, ftype, name) in collide else real(vendor, ftype, name)
afi.generate_filament_id = colliding
self.addCleanup(setattr, afi, "generate_filament_id", real)
self.t.write_preset("VendorA", preset("X @base", filament_id="OFcolid0",
instantiation=False,
filament_vendor="V", filament_type="PLA"))
self.t.write_preset("VendorA", preset("X @P1", inherits="X @base",
compatible_printers=["P1"]))
self.t.write_preset("VendorA", preset("Y @base", instantiation=False,
filament_vendor="W", filament_type="ABS"))
self.t.write_preset("VendorA", preset("Y @P1", inherits="Y @base",
compatible_printers=["P1"]))
before = self.t.bytes_map()
changed, errors, out = self.t.assign()
self.assertEqual((changed, errors), (0, 1), out)
self.assertIn("V/PLA/X", out)
self.assertIn("W/ABS/Y", out)
self.assertEqual(self.t.bytes_map(), before)
def test_mismatching_of_declaration_is_re_derived(self):
# BEHAVIOUR CHANGE: the default run used to leave an OF-format id alone
# and needed a separate mode to fix it. One rule now: an id that is not
# a mint iteration of its own triple is rewritten.
# One rule: an id that is not the mint of its own triple is rewritten.
self.t.write_preset("VendorA", preset("KNEW @base", filament_id="OFZZZZZZ",
instantiation=False,
filament_vendor="KV",
@@ -1445,20 +1439,19 @@ class TestRemint(SyntheticTreeCase):
changed, errors, _out = self.t.remint(["VendorA"])
self.assertEqual((changed, errors), (0, 0))
def test_salted_declaration_survives_remint(self):
# A deliberate salt split (a second preset of one product kept on its
# own id for per-printer AMS disambiguation) is mint-conformant and
# must not be converged back onto salt 0.
salt1 = afi.generate_filament_id(*self.TRIPLE, salt=1)
self.t.write_preset("VendorA", preset("APLA @legacy",
filament_id=salt1,
def test_second_id_of_one_product_is_converged_by_remint(self):
# A second preset of one product kept on an id of its own is not a
# sanctioned split: the triple determines the id, so --generate pulls it
# back. An AMS ambiguity this exposes is fixed in the profiles instead.
want = afi.generate_filament_id(*self.TRIPLE)
self.t.write_preset("VendorA", preset("APLA @legacy", filament_id="OFlegac1",
inherits="APLA @base",
compatible_printers=["P2 0.4 nozzle"]))
changed, errors, out = self.t.remint(["VendorA"])
self.assertEqual(errors, 0, out)
self.assertEqual(changed, 1) # only the non-conformant AX01 root
self.assertEqual(changed, 2) # the non-conformant AX01 root and @legacy
legacy = load_json_file(self.t.preset_path("VendorA", "APLA @legacy"))
self.assertEqual(legacy["filament_id"], salt1)
self.assertEqual(legacy["filament_id"], want)
def test_same_triple_converges_within_run(self):
self.t.add_vendor("VendorB", [
@@ -1490,22 +1483,24 @@ class TestRemint(SyntheticTreeCase):
self.assertEqual(a["filament_id"], want)
def test_not_blocked_by_a_non_conformant_occurrence(self):
# VendorB carries APLA's id under a PETG triple of its own, so that
# declaration is wrong and this run's job is to rewrite it. It must not
# cost VendorA's APLA the id its triple actually mints.
# VendorB carries APLA's id under a PETG triple of its own: that
# declaration is wrong, and it costs VendorA's APLA nothing — the id is
# the mint of APLA's triple, whoever else is squatting on it.
want0 = afi.generate_filament_id(*self.TRIPLE)
self.t.add_vendor("VendorB", [
preset("BPLA @base", filament_id=want0, instantiation=False,
filament_vendor="BV", filament_type="PETG"),
])
changed, errors, out = self.t.remint(["VendorA"])
self.assertEqual((changed, errors), (1, 1), out)
self.assertIn("declared by VendorB/filament/BPLA @base.json", out)
self.assertEqual((changed, errors), (1, 0), out)
root = load_json_file(self.t.preset_path("VendorA", "APLA @base"))
self.assertEqual(root["filament_id"], want0)
# Widening the run to both bundles clears it.
changed, errors, out = self.t.remint(["VendorA", "VendorB"])
# A run over VendorB re-mints the squatter to its own triple.
changed, errors, out = self.t.remint(["VendorB"])
self.assertEqual((changed, errors), (1, 0), out)
self.assertEqual(
load_json_file(self.t.preset_path("VendorB", "BPLA @base"))["filament_id"],
afi.generate_filament_id("BV", "PETG", "BPLA"))
def test_bbl_is_reminted_like_any_vendor(self):
self.t.add_vendor("BBL", [
@@ -1578,8 +1573,7 @@ class TestCli(unittest.TestCase):
# Named explicitly, both modes run against that tree.
rc, out = self.t.cli("--update-snapshot")
self.assertEqual(rc, 0, out)
# --generate only reads the snapshot as a list of ids not to reuse, so
# it keeps working without one.
# --generate never reads the snapshot, so it keeps working without one.
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
rc = afi.main(["--dry-run", "--profiles", self.t.profiles])
@@ -1762,8 +1756,7 @@ class TestReviewFixes(OfCleanTreeCase):
touched = set()
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
afi.generate_filament_ids(self.t.profiles, self.t.snapshot,
None, False, touched)
afi.generate_filament_ids(self.t.profiles, None, False, touched)
afi.generate_setting_ids(self.t.profiles, None, False, touched)
for name in ("SoloPLA @P1", "WrongPLA @P1"):
self.assertEqual([t for t in touched if t.endswith(name + ".json")],
@@ -1841,58 +1834,21 @@ class TestBambuMap(unittest.TestCase):
return filaments
def test_one_row_per_filament(self):
rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]), {})
rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]))
self.assertEqual(rows, {afi.generate_filament_id("Bambu Lab", "ABS", "Bambu ABS"):
{"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}})
def test_shared_bambu_id_is_an_error(self):
with self.assertRaises(SystemExit):
ubfi.derive_rows(self._bs_tree([("A", "GFX00", "V", "PLA"), ("B", "GFX00", "V", "PLA")]), {})
def test_reuses_the_id_we_ship_for_that_triple(self):
rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]),
{("Bambu Lab", "ABS", "Bambu ABS"): "OFsalted1"})
self.assertIn("OFsalted1", rows)
ubfi.derive_rows(self._bs_tree([("A", "GFX00", "V", "PLA"), ("B", "GFX00", "V", "PLA")]))
@unittest.skipUnless(os.path.isdir("/Users/lijiang/codes/BambuStudio/resources/profiles"), "no local clone")
def test_local_clone_yields_the_catalog(self):
rows = ubfi.derive_rows(afi.load_vendor_filaments("/Users/lijiang/codes/BambuStudio/resources/profiles", "BBL")[0], {})
rows = ubfi.derive_rows(afi.load_vendor_filaments("/Users/lijiang/codes/BambuStudio/resources/profiles", "BBL")[0])
self.assertEqual(len(rows), 100)
self.assertEqual(len({r["bambu_id"] for r in rows.values()}), 100)
class TestOrcaTriplesFromAnalysis(unittest.TestCase):
def _tree(self):
t = SyntheticTree()
self.addCleanup(t.cleanup)
t.add_vendor("VendorA", [
# Same triple declared under two different ids on purpose: mirrors
# a real, sanctioned case (Cubicon's xCeler line keeps its own id
# per printer instead of inheriting the filament's @base id).
# Ambiguous, but must stay harmless unless something needs it.
preset("Ambig PLA @base", filament_id="OF111111", instantiation=False,
filament_vendor="V", filament_type="PLA"),
preset("Ambig PLA @P1", filament_id="OF222222", inherits="Ambig PLA @base",
compatible_printers=["P1"]),
# An unambiguous filament elsewhere in the same tree.
preset("Needed PLA @base", filament_id="OF333333", instantiation=False,
filament_vendor="V2", filament_type="PLA"),
preset("Needed PLA @P1", inherits="Needed PLA @base",
compatible_printers=["P1"]),
])
return afi.analyze_tree(t.profiles)
def test_ambiguity_outside_needed_triples_is_ignored(self):
analysis = self._tree()
result = ubfi.orca_triples_from_analysis(analysis, {("V2", "PLA", "Needed PLA")})
self.assertEqual(result, {("V2", "PLA", "Needed PLA"): "OF333333"})
def test_ambiguity_inside_needed_triples_is_an_error(self):
analysis = self._tree()
with self.assertRaises(SystemExit):
ubfi.orca_triples_from_analysis(analysis, {("V", "PLA", "Ambig PLA")})
class TestWriteMap(unittest.TestCase):
def test_format(self):
d = tempfile.mkdtemp(prefix="bambu_map_test_")

View File

@@ -141,13 +141,10 @@ class SettingTree:
return changed, errors, buf.getvalue()
def run_filament_ids(self, vendors=None, dry_run=False):
"""The OTHER half of --generate, against this tree's own (absent)
snapshot, so the id policy never consults the repo's real one."""
"""The OTHER half of --generate."""
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
changed, errors = afi.generate_filament_ids(
self.profiles, os.path.join(self.dir, "filament_id_snapshot.json"),
vendors, dry_run)
changed, errors = afi.generate_filament_ids(self.profiles, vendors, dry_run)
return changed, errors, buf.getvalue()

View File

@@ -15,9 +15,9 @@ has to be hand-maintained. See docs/HLSD/filament_id.md.
One row per BambuStudio filament PRODUCT: one named spool product = one
"@base"-declared filament_id, shared by every per-printer/per-nozzle
instantiation of it (BambuStudio follows the same one-product-one-id shape
Orca's own filament_id policy does). A row's key reuses whatever OF id Orca
already ships for that same (filament_vendor, filament_type, filament) triple;
a triple Orca does not ship anywhere yet gets a freshly generated one.
Orca's own filament_id policy does). A row's key is the OF id that product's
(filament_vendor, filament_type, filament) triple mints — the id Orca carries
for it wherever it ships it, since the id is a function of the triple alone.
Map format:
{
@@ -57,7 +57,6 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from orca_id_tool import ( # noqa: E402
BAMBU_MAP_PATH,
OFL,
OF_ID_RE,
PROFILES_DIR,
analyze_tree,
base_name,
@@ -77,8 +76,8 @@ BAMBUSTUDIO_REPO = "https://github.com/bambulab/BambuStudio"
# Row derivation
# ---------------------------------------------------------------------------
def derive_rows(bs_filaments, orca_triples):
"""orca_triples: {(vendor, type, name): orca_id} from analyze_tree()["triples"]."""
def derive_rows(bs_filaments):
"""One row per BambuStudio product, keyed by the OF id its triple mints."""
by_filament = {} # filament_name -> (bambu_id, triple)
for rec in bs_filaments.values():
if not rec["instantiation"]:
@@ -98,50 +97,11 @@ def derive_rows(bs_filaments, orca_triples):
raise SystemExit(f"Bambu id {bambu_id} is shared by "
f"{seen[bambu_id]!r} and {filament_name!r}")
seen[bambu_id] = filament_name
orca_id = orca_triples.get(triple) or generate_filament_id(*triple) # reuse a salted id if we ship one
rows[orca_id] = {"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]}
rows[generate_filament_id(*triple)] = {
"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]}
return rows
def orca_triples_from_analysis(orca_analysis, needed_triples):
"""{(vendor, type, name): orca_id}, inverted from analyze_tree()["triples"]
(id -> [[vendor, type, name], ...]) and restricted to `needed_triples`
(the triples BambuStudio's own bundle ships, i.e. the only ones derive_rows
will ever look up).
Kept to ids matching the OF format: our own BBL bundle still declares
Bambu's GF ids today (analyze_tree already excludes those as BBL-island
declarations), but filtering here too means a future triple shipped under
a non-OF scheme can never key a map row, which must always be keyed by
the OF id the BBL bundle receives once it is re-minted onto the OF space.
A needed triple owned by more than one OF id would make derive_rows's
reuse step a guess, so that is a hard error rather than a silent pick.
Restricting the scan to needed_triples matters for that check: elsewhere
in the tree one triple legitimately resolves two different ids on purpose
(e.g. Cubicon's xCeler line declares its own id per printer instead of
inheriting its root's, sanctioned in scripts/filament_id_snapshot.json)
and that pre-existing, Bambu-unrelated divergence must not block a map
that never reads it.
"""
needed_triples = set(needed_triples)
result = {}
for fid, triple_list in orca_analysis["triples"].items():
if not OF_ID_RE.match(fid):
continue
for triple in triple_list:
triple = tuple(triple)
if triple not in needed_triples:
continue
owner = result.get(triple)
if owner and owner != fid:
raise SystemExit(
f"Orca triple {triple} resolves to more than one filament_id: "
f"{owner!r} and {fid!r}")
result[triple] = fid
return result
# ---------------------------------------------------------------------------
# Drift report (informational only)
# ---------------------------------------------------------------------------
@@ -269,16 +229,11 @@ def main(argv=None):
print_error(e)
raise SystemExit("unreadable BambuStudio filament profile(s)")
orca_analysis = analyze_tree(PROFILES_DIR)
needed_triples = {resolve_triple(rec["name"], bs_filaments, {})
for rec in bs_filaments.values() if rec["instantiation"]}
orca_triples = orca_triples_from_analysis(orca_analysis, needed_triples)
rows = derive_rows(bs_filaments, orca_triples)
rows = derive_rows(bs_filaments)
write_map(args.output, rows, commit, datetime.date.today().isoformat())
print_success(f"wrote {len(rows)} row(s) to {args.output} (BambuStudio @ {commit})")
for line in drift_report(rows, orca_analysis):
for line in drift_report(rows, analyze_tree(PROFILES_DIR)):
print_info(line)
finally:
if workdir: