mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-10 22:12:49 +00:00
Add codegen pipeline
This commit is contained in:
605
tools/config_codegen.py
Normal file
605
tools/config_codegen.py
Normal file
@@ -0,0 +1,605 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OrcaSlicer Config Code Generator
|
||||
|
||||
Reads compiled protobuf descriptor set and generates C++ source files
|
||||
that replace hand-written config registration, preset lists, and invalidation chains.
|
||||
|
||||
Usage:
|
||||
# Step 1: Compile .proto files to a descriptor set
|
||||
protoc --proto_path=src/PrintConfigs --descriptor_set_out=config.desc \
|
||||
--include_imports src/PrintConfigs/*.proto
|
||||
|
||||
# Step 2: Generate Python bindings (one-time, or when config_metadata.proto changes)
|
||||
protoc --proto_path=src/PrintConfigs --python_out=tools/ config_metadata.proto
|
||||
|
||||
# Step 3: Run codegen
|
||||
python tools/config_codegen.py config.desc codegen/generated/
|
||||
|
||||
Outputs:
|
||||
- PrintConfigDef_generated.cpp (init_fff_params body)
|
||||
- Preset_options_generated.cpp (s_Preset_*_options arrays)
|
||||
- Invalidation_generated.cpp (opt_key -> steps map)
|
||||
- OptionKeys_generated.cpp (extruder/filament key lists)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Add tools/ to path so we can import generated config_metadata_pb2
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
try:
|
||||
from google.protobuf import descriptor_pb2
|
||||
# Import the generated bindings - this registers extensions globally
|
||||
import config_metadata_pb2 as meta_pb2
|
||||
except ImportError as e:
|
||||
print(f"ERROR: {e}")
|
||||
print("Ensure google-protobuf is installed: pip install protobuf")
|
||||
print("And that config_metadata_pb2.py exists in tools/")
|
||||
print("Generate it with: protoc --proto_path=src/PrintConfigs --python_out=tools/ config_metadata.proto")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Proto FieldDescriptorProto.Type enum values
|
||||
TYPE_DOUBLE = 1
|
||||
TYPE_FLOAT = 2
|
||||
TYPE_INT64 = 3
|
||||
TYPE_UINT64 = 4
|
||||
TYPE_INT32 = 5
|
||||
TYPE_FIXED64 = 6
|
||||
TYPE_FIXED32 = 7
|
||||
TYPE_BOOL = 8
|
||||
TYPE_STRING = 9
|
||||
TYPE_MESSAGE = 11
|
||||
TYPE_UINT32 = 13
|
||||
TYPE_ENUM = 14
|
||||
TYPE_SINT32 = 17
|
||||
TYPE_SINT64 = 18
|
||||
|
||||
# Proto label
|
||||
LABEL_OPTIONAL = 1
|
||||
LABEL_REQUIRED = 2
|
||||
LABEL_REPEATED = 3
|
||||
|
||||
|
||||
def mode_to_cpp(mode_val):
|
||||
"""Convert mode enum value to C++ constant."""
|
||||
return {
|
||||
meta_pb2.MODE_SIMPLE: "comSimple",
|
||||
meta_pb2.MODE_ADVANCED: "comAdvanced",
|
||||
meta_pb2.MODE_DEVELOP: "comDevelop",
|
||||
}.get(mode_val, "comAdvanced")
|
||||
|
||||
|
||||
_PRINT_STEPS = None # set after meta_pb2 import resolves enum values
|
||||
_OBJECT_STEPS = None
|
||||
|
||||
|
||||
def _init_step_sets():
|
||||
global _PRINT_STEPS, _OBJECT_STEPS
|
||||
if _PRINT_STEPS is None:
|
||||
_PRINT_STEPS = {meta_pb2.STEP_GCODE_EXPORT, meta_pb2.STEP_SKIRT_BRIM, meta_pb2.STEP_WIPE_TOWER}
|
||||
_OBJECT_STEPS = {meta_pb2.STEP_SLICE, meta_pb2.STEP_PERIMETERS, meta_pb2.STEP_INFILL, meta_pb2.STEP_SUPPORT}
|
||||
|
||||
|
||||
def step_to_cpp(step_val):
|
||||
"""Convert invalidation step to C++ constant."""
|
||||
return {
|
||||
meta_pb2.STEP_GCODE_EXPORT: "psGCodeExport",
|
||||
meta_pb2.STEP_SKIRT_BRIM: "psSkirtBrim",
|
||||
meta_pb2.STEP_WIPE_TOWER: "psWipeTower",
|
||||
meta_pb2.STEP_SLICE: "posSlice",
|
||||
meta_pb2.STEP_PERIMETERS: "posPerimeters",
|
||||
meta_pb2.STEP_INFILL: "posInfill",
|
||||
meta_pb2.STEP_SUPPORT: "posSupportMaterial",
|
||||
meta_pb2.STEP_NONE: "",
|
||||
}.get(step_val, "")
|
||||
|
||||
|
||||
def proto_type_to_co_type(field_desc, is_nullable=False):
|
||||
"""
|
||||
Map a protobuf field descriptor to OrcaSlicer's coXXX type constant
|
||||
and ConfigOptionXXX class name.
|
||||
|
||||
Returns: (co_type_str, config_option_class, is_vector)
|
||||
"""
|
||||
ftype = field_desc.type
|
||||
is_repeated = (field_desc.label == LABEL_REPEATED)
|
||||
type_name = field_desc.type_name # For message types
|
||||
|
||||
# Handle message types (FloatOrPercent, Point2D)
|
||||
if ftype == TYPE_MESSAGE:
|
||||
if "FloatOrPercent" in type_name:
|
||||
if is_repeated:
|
||||
return ("coFloatsOrPercents", "ConfigOptionFloatsOrPercents", True)
|
||||
return ("coFloatOrPercent", "ConfigOptionFloatOrPercent", False)
|
||||
elif "Point2D" in type_name:
|
||||
if is_repeated:
|
||||
return ("coPoints", "ConfigOptionPoints", True)
|
||||
return ("coPoint", "ConfigOptionPoint", False)
|
||||
|
||||
# Handle enum types
|
||||
if ftype == TYPE_ENUM:
|
||||
if is_repeated:
|
||||
return ("coEnums", "ConfigOptionEnumsGeneric", True)
|
||||
return ("coEnum", "ConfigOptionEnum", False)
|
||||
|
||||
# Scalar/vector types
|
||||
if ftype in (TYPE_FLOAT, TYPE_DOUBLE):
|
||||
if is_repeated:
|
||||
if is_nullable:
|
||||
return ("coFloats", "ConfigOptionFloatsNullable", True)
|
||||
return ("coFloats", "ConfigOptionFloats", True)
|
||||
return ("coFloat", "ConfigOptionFloat", False)
|
||||
|
||||
if ftype in (TYPE_INT32, TYPE_INT64, TYPE_SINT32, TYPE_SINT64,
|
||||
TYPE_UINT32, TYPE_UINT64, TYPE_FIXED32, TYPE_FIXED64):
|
||||
if is_repeated:
|
||||
if is_nullable:
|
||||
return ("coInts", "ConfigOptionIntsNullable", True)
|
||||
return ("coInts", "ConfigOptionInts", True)
|
||||
return ("coInt", "ConfigOptionInt", False)
|
||||
|
||||
if ftype == TYPE_BOOL:
|
||||
if is_repeated:
|
||||
if is_nullable:
|
||||
return ("coBools", "ConfigOptionBoolsNullable", True)
|
||||
return ("coBools", "ConfigOptionBools", True)
|
||||
return ("coBool", "ConfigOptionBool", False)
|
||||
|
||||
if ftype == TYPE_STRING:
|
||||
if is_repeated:
|
||||
return ("coStrings", "ConfigOptionStrings", True)
|
||||
return ("coString", "ConfigOptionString", False)
|
||||
|
||||
return ("coNone", "ConfigOption", False)
|
||||
|
||||
|
||||
def parse_field_options(field_desc_proto):
|
||||
"""
|
||||
Re-parse FieldOptions from a FieldDescriptorProto with extensions registered.
|
||||
This is needed because the FileDescriptorSet parser doesn't know about our
|
||||
custom extensions, so they end up as unknown fields. Re-parsing with the
|
||||
extensions registered (via config_metadata_pb2 import) resolves them.
|
||||
"""
|
||||
from google.protobuf import descriptor_pb2
|
||||
opts = field_desc_proto.options
|
||||
if not opts.ByteSize():
|
||||
return descriptor_pb2.FieldOptions()
|
||||
|
||||
# Re-parse the serialized options with extensions registered
|
||||
reparsed = descriptor_pb2.FieldOptions()
|
||||
reparsed.ParseFromString(opts.SerializeToString())
|
||||
return reparsed
|
||||
|
||||
|
||||
class FieldInfo:
|
||||
"""Parsed information about a single config field from proto descriptor."""
|
||||
|
||||
def __init__(self, field_desc):
|
||||
self.name = field_desc.name
|
||||
self.field_desc = field_desc
|
||||
|
||||
# Re-parse options with extensions registered
|
||||
opts = parse_field_options(field_desc)
|
||||
|
||||
# Read extensions using the proper protobuf API
|
||||
self.label = opts.Extensions[meta_pb2.label] or None
|
||||
self.full_label = opts.Extensions[meta_pb2.full_label] or None
|
||||
self.tooltip = opts.Extensions[meta_pb2.tooltip] or None
|
||||
self.category = opts.Extensions[meta_pb2.category] or None
|
||||
self.sidetext = opts.Extensions[meta_pb2.sidetext] or None
|
||||
self.min_value = opts.Extensions[meta_pb2.min_value] if opts.HasExtension(meta_pb2.min_value) else None
|
||||
self.max_value = opts.Extensions[meta_pb2.max_value] if opts.HasExtension(meta_pb2.max_value) else None
|
||||
self.max_literal = opts.Extensions[meta_pb2.max_literal] if opts.HasExtension(meta_pb2.max_literal) else None
|
||||
self.mode = opts.Extensions[meta_pb2.mode] # 0 = MODE_SIMPLE (default)
|
||||
self.has_mode = opts.HasExtension(meta_pb2.mode)
|
||||
self.ratio_over = opts.Extensions[meta_pb2.ratio_over] or None
|
||||
self.multiline = opts.Extensions[meta_pb2.multiline]
|
||||
self.full_width = opts.Extensions[meta_pb2.full_width]
|
||||
self.height = opts.Extensions[meta_pb2.height] or None
|
||||
self.is_nullable = opts.Extensions[meta_pb2.is_nullable]
|
||||
self.gui_type = opts.Extensions[meta_pb2.gui_type] or None
|
||||
self.gui_flags = opts.Extensions[meta_pb2.gui_flags] or None
|
||||
self.enum_keys_map = opts.Extensions[meta_pb2.enum_keys_map_ref] or None
|
||||
self.no_cli = opts.Extensions[meta_pb2.no_cli]
|
||||
self.readonly = opts.Extensions[meta_pb2.readonly]
|
||||
self.preset = opts.Extensions[meta_pb2.preset] # 0 = PRESET_PRINT
|
||||
self.invalidates = list(opts.Extensions[meta_pb2.invalidates])
|
||||
self.list_membership = list(opts.Extensions[meta_pb2.list_membership])
|
||||
self.legacy_name = opts.Extensions[meta_pb2.legacy_name] or None
|
||||
|
||||
# Default value and enum metadata
|
||||
self.has_default = opts.Extensions[meta_pb2.has_default]
|
||||
self.default_value = opts.Extensions[meta_pb2.default_value] if self.has_default else None
|
||||
self.enum_value_entries = list(opts.Extensions[meta_pb2.enum_value_entries])
|
||||
self.enum_label_entries = list(opts.Extensions[meta_pb2.enum_label_entries])
|
||||
self.co_type_hint = opts.Extensions[meta_pb2.co_type_hint] or None
|
||||
|
||||
# Resolve C++ type info - co_type_hint overrides auto-detection
|
||||
co_type, option_class, is_vec = proto_type_to_co_type(
|
||||
field_desc, self.is_nullable)
|
||||
if self.co_type_hint:
|
||||
co_type = self.co_type_hint
|
||||
# Fix up option_class for hint-overridden types
|
||||
hint_class_map = {
|
||||
"coPercent": "ConfigOptionPercent",
|
||||
"coPercents": "ConfigOptionPercents",
|
||||
"coEnum": "ConfigOptionEnum",
|
||||
"coEnums": "ConfigOptionEnumsGeneric",
|
||||
}
|
||||
if self.co_type_hint in hint_class_map:
|
||||
option_class = hint_class_map[self.co_type_hint]
|
||||
self.co_type = co_type
|
||||
self.option_class = option_class
|
||||
self.is_vector = is_vec
|
||||
|
||||
|
||||
class CodeGenerator:
|
||||
"""Generates C++ source files from parsed proto descriptors."""
|
||||
|
||||
def __init__(self, descriptor_set):
|
||||
self.descriptor_set = descriptor_set
|
||||
self.fields = [] # All FieldInfo objects
|
||||
self.virtual_keys_by_preset = { # virtual_preset_keys per preset type
|
||||
meta_pb2.PRESET_PRINT: [],
|
||||
meta_pb2.PRESET_FILAMENT: [],
|
||||
meta_pb2.PRESET_PRINTER: [],
|
||||
}
|
||||
self._parse_all_fields()
|
||||
|
||||
@staticmethod
|
||||
def _preset_type_from_filename(name: str) -> int:
|
||||
"""Infer preset type from proto filename (printer/filament/print)."""
|
||||
n = name.lower()
|
||||
if "printer" in n:
|
||||
return meta_pb2.PRESET_PRINTER
|
||||
if "filament" in n:
|
||||
return meta_pb2.PRESET_FILAMENT
|
||||
return meta_pb2.PRESET_PRINT
|
||||
|
||||
def _parse_all_fields(self):
|
||||
"""Parse all message fields from all proto files in the descriptor set."""
|
||||
for file_desc in self.descriptor_set.file:
|
||||
# Skip google/protobuf imports
|
||||
if file_desc.name.startswith("google/"):
|
||||
continue
|
||||
# Skip config_metadata.proto (it's just extensions, no settings)
|
||||
if "config_metadata" in file_desc.name:
|
||||
continue
|
||||
|
||||
preset_type = self._preset_type_from_filename(file_desc.name)
|
||||
|
||||
for msg_desc in file_desc.message_type:
|
||||
# Skip wrapper messages (FloatOrPercent, Point2D)
|
||||
if msg_desc.name in ("FloatOrPercent", "Point2D"):
|
||||
continue
|
||||
|
||||
# Collect message-level virtual_preset_keys
|
||||
vkeys = list(msg_desc.options.Extensions[meta_pb2.virtual_preset_keys])
|
||||
self.virtual_keys_by_preset[preset_type].extend(vkeys)
|
||||
|
||||
for field_desc in msg_desc.field:
|
||||
self.fields.append(FieldInfo(field_desc))
|
||||
|
||||
def generate_init_fff_params(self) -> str:
|
||||
"""
|
||||
Generate the body of PrintConfigDef::init_fff_params().
|
||||
Output: C++ code that's a drop-in replacement for the hand-written registrations.
|
||||
"""
|
||||
lines = []
|
||||
lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====")
|
||||
lines.append("// DO NOT EDIT MANUALLY. Edit .proto files and re-run codegen.")
|
||||
lines.append("")
|
||||
|
||||
for field in self.fields:
|
||||
lines.append(f' def = this->add("{field.name}", {field.co_type});')
|
||||
|
||||
if field.label:
|
||||
lines.append(f' def->label = L("{self._escape_cpp(field.label)}");')
|
||||
|
||||
if field.full_label:
|
||||
lines.append(f' def->full_label = L("{self._escape_cpp(field.full_label)}");')
|
||||
|
||||
if field.category:
|
||||
lines.append(f' def->category = L("{self._escape_cpp(field.category)}");')
|
||||
|
||||
if field.tooltip:
|
||||
tooltip_escaped = self._escape_cpp(field.tooltip)
|
||||
# Split long tooltips across lines
|
||||
if len(tooltip_escaped) > 80:
|
||||
lines.append(f' def->tooltip = L("{tooltip_escaped}");')
|
||||
else:
|
||||
lines.append(f' def->tooltip = L("{tooltip_escaped}");')
|
||||
|
||||
if field.sidetext:
|
||||
lines.append(f' def->sidetext = L("{self._escape_cpp(field.sidetext)}");')
|
||||
|
||||
if field.min_value is not None:
|
||||
lines.append(f' def->min = {self._format_number(field.min_value)};')
|
||||
|
||||
if field.max_value is not None:
|
||||
lines.append(f' def->max = {self._format_number(field.max_value)};')
|
||||
|
||||
if field.max_literal is not None:
|
||||
lines.append(f' def->max_literal = {self._format_number(field.max_literal)};')
|
||||
|
||||
if field.ratio_over:
|
||||
lines.append(f' def->ratio_over = "{field.ratio_over}";')
|
||||
|
||||
if field.has_mode:
|
||||
lines.append(f' def->mode = {mode_to_cpp(field.mode)};')
|
||||
|
||||
if field.is_nullable:
|
||||
lines.append(f' def->nullable = true;')
|
||||
|
||||
if field.readonly:
|
||||
lines.append(f' def->readonly = true;')
|
||||
|
||||
if field.multiline:
|
||||
lines.append(f' def->multiline = true;')
|
||||
|
||||
if field.full_width:
|
||||
lines.append(f' def->full_width = true;')
|
||||
|
||||
if field.height:
|
||||
lines.append(f' def->height = {field.height};')
|
||||
|
||||
if field.gui_type:
|
||||
lines.append(f' def->gui_type = ConfigOptionDef::GUIType::{field.gui_type};')
|
||||
|
||||
if field.gui_flags:
|
||||
lines.append(f' def->gui_flags = "{field.gui_flags}";')
|
||||
|
||||
if field.no_cli:
|
||||
lines.append(f' def->cli = ConfigOptionDef::nocli;')
|
||||
|
||||
if field.enum_keys_map:
|
||||
lines.append(f' def->enum_keys_map = &{field.enum_keys_map};')
|
||||
|
||||
# Enum values/labels
|
||||
for ev in field.enum_value_entries:
|
||||
lines.append(f' def->enum_values.push_back("{self._escape_cpp(ev)}");')
|
||||
for el in field.enum_label_entries:
|
||||
lines.append(f' def->enum_labels.push_back(L("{self._escape_cpp(el)}"));')
|
||||
|
||||
# Default value - reconstruct full C++ from co_type + default_value
|
||||
if field.has_default:
|
||||
cpp_expr = self._reconstruct_default_cpp(
|
||||
field.default_value or "", field.co_type, field.enum_keys_map)
|
||||
lines.append(f' def->set_default_value({cpp_expr});')
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_preset_options(self) -> str:
|
||||
"""Generate s_Preset_print_options, s_Preset_filament_options, etc."""
|
||||
lines = []
|
||||
lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====")
|
||||
lines.append("")
|
||||
|
||||
for var_name, preset_type in [
|
||||
("s_Preset_print_options", meta_pb2.PRESET_PRINT),
|
||||
("s_Preset_filament_options", meta_pb2.PRESET_FILAMENT),
|
||||
("s_Preset_printer_options", meta_pb2.PRESET_PRINTER),
|
||||
]:
|
||||
# Field-derived keys + message-level virtual keys, deduplicated and sorted
|
||||
field_names = [f.name for f in self.fields if f.preset == preset_type]
|
||||
virtual_names = self.virtual_keys_by_preset[preset_type]
|
||||
all_names = sorted(set(field_names) | set(virtual_names))
|
||||
|
||||
lines.append(f"static const std::vector<std::string> {var_name} = {{")
|
||||
for name in all_names:
|
||||
lines.append(f' "{name}",')
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_invalidation_map(self) -> str:
|
||||
"""Generate opt_key -> invalidation steps mapping, split by PrintStep vs PrintObjectStep."""
|
||||
_init_step_sets()
|
||||
lines = []
|
||||
lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====")
|
||||
lines.append("")
|
||||
|
||||
lines.append("static const std::unordered_map<std::string, std::vector<PrintStep>> "
|
||||
"s_print_steps_map = {")
|
||||
for field in sorted(self.fields, key=lambda x: x.name):
|
||||
if field.invalidates:
|
||||
steps = [step_to_cpp(s) for s in field.invalidates
|
||||
if s in _PRINT_STEPS and step_to_cpp(s)]
|
||||
if steps:
|
||||
lines.append(f' {{"{field.name}", {{{", ".join(steps)}}}}},')
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
lines.append("static const std::unordered_map<std::string, std::vector<PrintObjectStep>> "
|
||||
"s_object_steps_map = {")
|
||||
for field in sorted(self.fields, key=lambda x: x.name):
|
||||
if field.invalidates:
|
||||
steps = [step_to_cpp(s) for s in field.invalidates
|
||||
if s in _OBJECT_STEPS and step_to_cpp(s)]
|
||||
if steps:
|
||||
lines.append(f' {{"{field.name}", {{{", ".join(steps)}}}}},')
|
||||
lines.append("};")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_option_key_lists(self) -> str:
|
||||
"""Generate extruder_option_keys, filament_option_keys, etc."""
|
||||
lines = []
|
||||
lines.append("// ===== AUTO-GENERATED by tools/config_codegen.py =====")
|
||||
lines.append("")
|
||||
|
||||
extruder_keys = [f for f in self.fields
|
||||
if meta_pb2.LIST_EXTRUDER_OPTION_KEYS in f.list_membership]
|
||||
filament_keys = [f for f in self.fields
|
||||
if meta_pb2.LIST_FILAMENT_OPTION_KEYS in f.list_membership]
|
||||
|
||||
for var_name, keys in [
|
||||
("s_extruder_option_keys", extruder_keys),
|
||||
("s_filament_option_keys", filament_keys),
|
||||
]:
|
||||
lines.append(f"static const std::vector<std::string> {var_name} = {{")
|
||||
for f in sorted(keys, key=lambda x: x.name):
|
||||
lines.append(f' "{f.name}",')
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _reconstruct_default_cpp(default_value, co_type, enum_keys_map=None):
|
||||
"""Reconstruct full C++ default expression from co_type + extracted value args.
|
||||
|
||||
Maps (co_type, args) -> 'new ConfigOptionXxx(args)' or 'new ConfigOptionXxx{args}'.
|
||||
"""
|
||||
import re as _re
|
||||
|
||||
# Type -> C++ class mappings
|
||||
SCALAR_CLASS = {
|
||||
"coFloat": "ConfigOptionFloat",
|
||||
"coBool": "ConfigOptionBool",
|
||||
"coInt": "ConfigOptionInt",
|
||||
"coString": "ConfigOptionString",
|
||||
"coPercent": "ConfigOptionPercent",
|
||||
"coFloatOrPercent": "ConfigOptionFloatOrPercent",
|
||||
"coPoint": "ConfigOptionPoint",
|
||||
"coPoint3": "ConfigOptionPoint3",
|
||||
}
|
||||
LIST_CLASS = {
|
||||
"coFloats": "ConfigOptionFloats",
|
||||
"coInts": "ConfigOptionInts",
|
||||
"coBools": "ConfigOptionBools",
|
||||
"coStrings": "ConfigOptionStrings",
|
||||
"coPercents": "ConfigOptionPercents",
|
||||
"coFloatsOrPercents": "ConfigOptionFloatsOrPercents",
|
||||
"coPoints": "ConfigOptionPoints",
|
||||
}
|
||||
|
||||
# Do NOT unescape \n → newline here; C++ string literals use \n as escape sequence.
|
||||
# Only unescape escaped quotes so they appear correctly in the reconstructed expression.
|
||||
args = default_value.replace('\\"', '"')
|
||||
|
||||
# Empty args -> default constructor for any type
|
||||
if not args:
|
||||
if co_type == "coEnum":
|
||||
enum_type = "int"
|
||||
if enum_keys_map:
|
||||
m = _re.match(r'ConfigOptionEnum<(\w+)>::', enum_keys_map)
|
||||
if m:
|
||||
enum_type = m.group(1)
|
||||
return f"new ConfigOptionEnum<{enum_type}>()"
|
||||
if co_type == "coEnums":
|
||||
return "new ConfigOptionEnumsGeneric{}"
|
||||
all_classes = {**SCALAR_CLASS, **LIST_CLASS}
|
||||
cls = all_classes.get(co_type, "ConfigOption")
|
||||
return f"new {cls}()"
|
||||
|
||||
if co_type in SCALAR_CLASS:
|
||||
return f"new {SCALAR_CLASS[co_type]}({args})"
|
||||
|
||||
if co_type in LIST_CLASS:
|
||||
return f"new {LIST_CLASS[co_type]}{{{args}}}"
|
||||
|
||||
if co_type == "coEnum":
|
||||
# Extract enum type from enum_keys_map, e.g.
|
||||
# "ConfigOptionEnum<BedType>::get_enum_values()" -> "BedType"
|
||||
enum_type = "int"
|
||||
if enum_keys_map:
|
||||
m = _re.match(r'ConfigOptionEnum<(\w+)>::', enum_keys_map)
|
||||
if m:
|
||||
enum_type = m.group(1)
|
||||
return f"new ConfigOptionEnum<{enum_type}>({args})"
|
||||
|
||||
if co_type == "coEnums":
|
||||
# List-of-enum: use ConfigOptionEnumsGenericNullable if nullable,
|
||||
# otherwise ConfigOptionEnumsGeneric.
|
||||
# We don't have is_nullable here, so always emit Generic; caller
|
||||
# can override when is_nullable is set.
|
||||
return f"new ConfigOptionEnumsGeneric{{ {args} }}"
|
||||
|
||||
# Fallback: try generic
|
||||
return f"new ConfigOption({args})"
|
||||
|
||||
@staticmethod
|
||||
def _escape_cpp(s):
|
||||
"""Escape a string for C++ string literal.
|
||||
|
||||
Proto strings already contain C++ escape sequences (\\n, \\", etc.)
|
||||
as literal backslash + char. We pass those through and only escape
|
||||
unescaped quotes and actual newlines.
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
# Replace actual newline characters (rare) with \n escape
|
||||
s = s.replace('\n', '\\n')
|
||||
# Don't double-escape backslashes that are already part of escape sequences.
|
||||
# The proto strings store them as literal \n, \", \t etc.
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def _format_number(val):
|
||||
"""Format a number for C++ (int vs float)."""
|
||||
if val is None:
|
||||
return "0"
|
||||
if isinstance(val, float) and val == int(val):
|
||||
return str(int(val))
|
||||
return str(val)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate C++ config code from protobuf descriptors")
|
||||
parser.add_argument("descriptor_set",
|
||||
help="Path to compiled .desc file (protoc --descriptor_set_out)")
|
||||
parser.add_argument("output_dir",
|
||||
help="Directory to write generated C++ files")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read descriptor set
|
||||
desc_path = Path(args.descriptor_set)
|
||||
if not desc_path.exists():
|
||||
print(f"ERROR: Descriptor file not found: {desc_path}")
|
||||
sys.exit(1)
|
||||
|
||||
with open(desc_path, 'rb') as f:
|
||||
raw = f.read()
|
||||
|
||||
file_descriptor_set = descriptor_pb2.FileDescriptorSet()
|
||||
file_descriptor_set.ParseFromString(raw)
|
||||
|
||||
print(f"Loaded {len(file_descriptor_set.file)} proto files")
|
||||
for fd in file_descriptor_set.file:
|
||||
if not fd.name.startswith("google/"):
|
||||
print(f" - {fd.name}: {len(fd.message_type)} messages")
|
||||
|
||||
# Generate code
|
||||
gen = CodeGenerator(file_descriptor_set)
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
outputs = {
|
||||
"PrintConfigDef_generated.cpp": gen.generate_init_fff_params(),
|
||||
"Preset_options_generated.cpp": gen.generate_preset_options(),
|
||||
"Invalidation_generated.cpp": gen.generate_invalidation_map(),
|
||||
"OptionKeys_generated.cpp": gen.generate_option_key_lists(),
|
||||
}
|
||||
|
||||
for filename, content in outputs.items():
|
||||
out_path = output_dir / filename
|
||||
with open(out_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"Generated: {out_path}")
|
||||
|
||||
print(f"\nDone. {len(gen.fields)} settings processed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
47
tools/config_metadata_pb2.py
Normal file
47
tools/config_metadata_pb2.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# NO CHECKED-IN PROTOBUF GENCODE
|
||||
# source: config_metadata.proto
|
||||
# Protobuf Python Version: 6.32.1
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import runtime_version as _runtime_version
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
_runtime_version.ValidateProtobufRuntimeVersion(
|
||||
_runtime_version.Domain.PUBLIC,
|
||||
6,
|
||||
32,
|
||||
1,
|
||||
'',
|
||||
'config_metadata.proto'
|
||||
)
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x63onfig_metadata.proto\x12\x04orca\x1a google/protobuf/descriptor.proto\"0\n\x0e\x46loatOrPercent\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x0f\n\x07percent\x18\x02 \x01(\x08\"\x1f\n\x07Point2D\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01*B\n\nConfigMode\x12\x0f\n\x0bMODE_SIMPLE\x10\x00\x12\x11\n\rMODE_ADVANCED\x10\x01\x12\x10\n\x0cMODE_DEVELOP\x10\x02*G\n\nPresetType\x12\x10\n\x0cPRESET_PRINT\x10\x00\x12\x13\n\x0fPRESET_FILAMENT\x10\x01\x12\x12\n\x0ePRESET_PRINTER\x10\x02*\xaa\x01\n\x10InvalidationStep\x12\x15\n\x11STEP_GCODE_EXPORT\x10\x00\x12\x13\n\x0fSTEP_SKIRT_BRIM\x10\x01\x12\x13\n\x0fSTEP_WIPE_TOWER\x10\x02\x12\x0e\n\nSTEP_SLICE\x10\x03\x12\x13\n\x0fSTEP_PERIMETERS\x10\x04\x12\x0f\n\x0bSTEP_INFILL\x10\x05\x12\x10\n\x0cSTEP_SUPPORT\x10\x06\x12\r\n\tSTEP_NONE\x10\x07*\x81\x01\n\x14OptionListMembership\x12\r\n\tLIST_NONE\x10\x00\x12\x1d\n\x19LIST_EXTRUDER_OPTION_KEYS\x10\x01\x12\x1d\n\x19LIST_FILAMENT_OPTION_KEYS\x10\x02\x12\x1c\n\x18LIST_VARIANT_OPTION_KEYS\x10\x03:.\n\x05label\x12\x1d.google.protobuf.FieldOptions\x18\xd1\x86\x03 \x01(\t:3\n\nfull_label\x12\x1d.google.protobuf.FieldOptions\x18\xd2\x86\x03 \x01(\t:0\n\x07tooltip\x12\x1d.google.protobuf.FieldOptions\x18\xd3\x86\x03 \x01(\t:1\n\x08\x63\x61tegory\x12\x1d.google.protobuf.FieldOptions\x18\xd4\x86\x03 \x01(\t:1\n\x08sidetext\x12\x1d.google.protobuf.FieldOptions\x18\xd5\x86\x03 \x01(\t:2\n\tmin_value\x12\x1d.google.protobuf.FieldOptions\x18\xd6\x86\x03 \x01(\x01:2\n\tmax_value\x12\x1d.google.protobuf.FieldOptions\x18\xd7\x86\x03 \x01(\x01:4\n\x0bmax_literal\x12\x1d.google.protobuf.FieldOptions\x18\xd8\x86\x03 \x01(\x01:?\n\x04mode\x12\x1d.google.protobuf.FieldOptions\x18\xd9\x86\x03 \x01(\x0e\x32\x10.orca.ConfigMode:3\n\nratio_over\x12\x1d.google.protobuf.FieldOptions\x18\xda\x86\x03 \x01(\t:2\n\tmultiline\x12\x1d.google.protobuf.FieldOptions\x18\xdd\x86\x03 \x01(\x08:3\n\nfull_width\x12\x1d.google.protobuf.FieldOptions\x18\xde\x86\x03 \x01(\x08:/\n\x06height\x12\x1d.google.protobuf.FieldOptions\x18\xdf\x86\x03 \x01(\x05:A\n\x06preset\x12\x1d.google.protobuf.FieldOptions\x18\xdb\x86\x03 \x01(\x0e\x32\x10.orca.PresetType:L\n\x0binvalidates\x12\x1d.google.protobuf.FieldOptions\x18\xdc\x86\x03 \x03(\x0e\x32\x16.orca.InvalidationStep:T\n\x0flist_membership\x12\x1d.google.protobuf.FieldOptions\x18\xe2\x86\x03 \x03(\x0e\x32\x1a.orca.OptionListMembership:4\n\x0blegacy_name\x12\x1d.google.protobuf.FieldOptions\x18\xe0\x86\x03 \x01(\t:4\n\x0bis_nullable\x12\x1d.google.protobuf.FieldOptions\x18\xe1\x86\x03 \x01(\x08:1\n\x08gui_type\x12\x1d.google.protobuf.FieldOptions\x18\xe3\x86\x03 \x01(\t:2\n\tgui_flags\x12\x1d.google.protobuf.FieldOptions\x18\xe4\x86\x03 \x01(\t::\n\x11\x65num_keys_map_ref\x12\x1d.google.protobuf.FieldOptions\x18\xe5\x86\x03 \x01(\t:/\n\x06no_cli\x12\x1d.google.protobuf.FieldOptions\x18\xe6\x86\x03 \x01(\x08:1\n\x08readonly\x12\x1d.google.protobuf.FieldOptions\x18\xe7\x86\x03 \x01(\x08:5\n\x0c\x63o_type_hint\x12\x1d.google.protobuf.FieldOptions\x18\xe8\x86\x03 \x01(\t:6\n\rdefault_value\x12\x1d.google.protobuf.FieldOptions\x18\xe9\x86\x03 \x01(\t:4\n\x0bhas_default\x12\x1d.google.protobuf.FieldOptions\x18\xec\x86\x03 \x01(\x08:;\n\x12\x65num_value_entries\x12\x1d.google.protobuf.FieldOptions\x18\xea\x86\x03 \x03(\t:;\n\x12\x65num_label_entries\x12\x1d.google.protobuf.FieldOptions\x18\xeb\x86\x03 \x03(\t:1\n\x08tab_type\x12\x1d.google.protobuf.FieldOptions\x18\xed\x86\x03 \x01(\t:1\n\x08tab_page\x12\x1d.google.protobuf.FieldOptions\x18\xee\x86\x03 \x01(\t:5\n\x0ctab_optgroup\x12\x1d.google.protobuf.FieldOptions\x18\xef\x86\x03 \x01(\t:>\n\x13virtual_preset_keys\x12\x1f.google.protobuf.MessageOptions\x18\xe1\xd4\x03 \x03(\tb\x06proto3')
|
||||
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'config_metadata_pb2', _globals)
|
||||
if not _descriptor._USE_C_DESCRIPTORS:
|
||||
DESCRIPTOR._loaded_options = None
|
||||
_globals['_CONFIGMODE']._serialized_start=148
|
||||
_globals['_CONFIGMODE']._serialized_end=214
|
||||
_globals['_PRESETTYPE']._serialized_start=216
|
||||
_globals['_PRESETTYPE']._serialized_end=287
|
||||
_globals['_INVALIDATIONSTEP']._serialized_start=290
|
||||
_globals['_INVALIDATIONSTEP']._serialized_end=460
|
||||
_globals['_OPTIONLISTMEMBERSHIP']._serialized_start=463
|
||||
_globals['_OPTIONLISTMEMBERSHIP']._serialized_end=592
|
||||
_globals['_FLOATORPERCENT']._serialized_start=65
|
||||
_globals['_FLOATORPERCENT']._serialized_end=113
|
||||
_globals['_POINT2D']._serialized_start=115
|
||||
_globals['_POINT2D']._serialized_end=146
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
80
tools/run_codegen.py
Normal file
80
tools/run_codegen.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convenience script: runs the codegen pipeline.
|
||||
|
||||
1. Compile .proto -> binary descriptor set (protoc)
|
||||
2. Generate C++ from descriptors (config_codegen.py)
|
||||
3. Validate output against original
|
||||
|
||||
Usage:
|
||||
python tools/run_codegen.py # full pipeline
|
||||
python tools/run_codegen.py --validate-only # just validate
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
PROTO_DIR = ROOT / "src" / "PrintConfigs"
|
||||
PROTO_GEN_DIR = PROTO_DIR / "generated"
|
||||
CODEGEN_OUT = ROOT / "codegen" / "generated"
|
||||
DESC_FILE = ROOT / "config.desc"
|
||||
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
print(f" $ {' '.join(str(c) for c in cmd)}")
|
||||
result = subprocess.run(cmd, **kwargs)
|
||||
if result.returncode != 0:
|
||||
print(f" FAILED (exit code {result.returncode})")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def step_compile():
|
||||
print("\n=== Step 1: Compile .proto -> descriptor set ===")
|
||||
proto_files = [f for f in PROTO_GEN_DIR.glob("*.proto") if not f.name.endswith("_gen.proto")]
|
||||
if not proto_files:
|
||||
print(" ERROR: No .proto files found")
|
||||
return False
|
||||
|
||||
return run([
|
||||
"protoc",
|
||||
f"--proto_path={PROTO_DIR}",
|
||||
f"--proto_path={PROTO_GEN_DIR}",
|
||||
f"--descriptor_set_out={DESC_FILE}",
|
||||
"--include_imports",
|
||||
] + [str(f) for f in proto_files])
|
||||
|
||||
|
||||
def step_generate():
|
||||
print("\n=== Step 2: Generate C++ from descriptors ===")
|
||||
return run([sys.executable, str(ROOT / "tools" / "config_codegen.py"),
|
||||
str(DESC_FILE), str(CODEGEN_OUT)])
|
||||
|
||||
|
||||
def step_validate():
|
||||
print("\n=== Step 3: Validate ===")
|
||||
return run([sys.executable, str(ROOT / "tools" / "validate_codegen.py")])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Run OrcaSlicer config codegen pipeline")
|
||||
parser.add_argument("--validate-only", action="store_true",
|
||||
help="Only run validation")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.validate_only:
|
||||
sys.exit(0 if step_validate() else 1)
|
||||
|
||||
for name, fn in [("Compile", step_compile), ("Generate", step_generate), ("Validate", step_validate)]:
|
||||
if not fn():
|
||||
print(f"\n*** Pipeline FAILED at: {name} ***")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n=== Pipeline completed successfully ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
213
tools/validate_codegen.py
Normal file
213
tools/validate_codegen.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validates generated PrintConfigDef code against the original PrintConfig.cpp.
|
||||
|
||||
Compares setting keys, types, defaults, enum values/labels, and metadata
|
||||
to ensure the codegen output is a faithful reproduction.
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
def parse_original_settings(cpp_path):
|
||||
"""Parse the original init_fff_params() into a dict of key -> properties."""
|
||||
with open(cpp_path, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
m = re.search(r'void PrintConfigDef::init_fff_params\(\)(.*?)^\}', text, re.DOTALL | re.MULTILINE)
|
||||
if not m:
|
||||
print("ERROR: Could not find init_fff_params()")
|
||||
sys.exit(1)
|
||||
body = m.group(1)
|
||||
|
||||
settings = OrderedDict()
|
||||
current_key = None
|
||||
|
||||
for line in body.split('\n'):
|
||||
stripped = line.strip()
|
||||
|
||||
# Detect this->add("key", coType)
|
||||
add_match = re.search(r'this->add(?:_nullable)?\s*\(\s*"([^"]+)"\s*,\s*(co\w+)\s*\)', stripped)
|
||||
if add_match:
|
||||
current_key = add_match.group(1)
|
||||
co_type = add_match.group(2)
|
||||
# Last definition wins (handles duplicates)
|
||||
settings[current_key] = {
|
||||
'co_type': co_type,
|
||||
'has_default': False,
|
||||
'enum_values': 0,
|
||||
'enum_labels': 0,
|
||||
'has_enum_map': False,
|
||||
}
|
||||
continue
|
||||
|
||||
if current_key and current_key in settings:
|
||||
s = settings[current_key]
|
||||
if 'set_default_value' in stripped:
|
||||
s['has_default'] = True
|
||||
if re.search(r'enum_values\.(?:push_back|emplace_back)', stripped):
|
||||
s['enum_values'] += 1
|
||||
if re.search(r'enum_labels\.(?:push_back|emplace_back)', stripped):
|
||||
s['enum_labels'] += 1
|
||||
if 'enum_keys_map' in stripped and '=' in stripped:
|
||||
s['has_enum_map'] = True
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
def parse_generated_settings(gen_path):
|
||||
"""Parse the generated PrintConfigDef code into a dict of key -> properties."""
|
||||
with open(gen_path, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
settings = OrderedDict()
|
||||
current_key = None
|
||||
|
||||
for line in text.split('\n'):
|
||||
stripped = line.strip()
|
||||
|
||||
add_match = re.search(r'this->add(?:_nullable)?\s*\(\s*"([^"]+)"\s*,\s*(co\w+)\s*\)', stripped)
|
||||
if add_match:
|
||||
current_key = add_match.group(1)
|
||||
co_type = add_match.group(2)
|
||||
settings[current_key] = {
|
||||
'co_type': co_type,
|
||||
'has_default': False,
|
||||
'enum_values': 0,
|
||||
'enum_labels': 0,
|
||||
'has_enum_map': False,
|
||||
}
|
||||
continue
|
||||
|
||||
if current_key and current_key in settings:
|
||||
s = settings[current_key]
|
||||
if 'set_default_value' in stripped:
|
||||
s['has_default'] = True
|
||||
if 'enum_values.push_back' in stripped:
|
||||
s['enum_values'] += 1
|
||||
if 'enum_labels.push_back' in stripped:
|
||||
s['enum_labels'] += 1
|
||||
if 'enum_keys_map' in stripped and '=' in stripped:
|
||||
s['has_enum_map'] = True
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
def main():
|
||||
orig_path = Path("src/libslic3r/PrintConfig.cpp")
|
||||
gen_path = Path("codegen/generated/PrintConfigDef_generated.cpp")
|
||||
|
||||
if not orig_path.exists() or not gen_path.exists():
|
||||
print("ERROR: Required files not found")
|
||||
sys.exit(1)
|
||||
|
||||
print("Parsing original...")
|
||||
orig = parse_original_settings(orig_path)
|
||||
print(f" {len(orig)} settings")
|
||||
|
||||
print("Parsing generated...")
|
||||
gen = parse_generated_settings(gen_path)
|
||||
print(f" {len(gen)} settings")
|
||||
|
||||
# Known exceptions: settings that exist in original but are commented out
|
||||
# or have runtime-generated enums
|
||||
known_exceptions = {
|
||||
'adaptive_layer_height', # Commented out in original
|
||||
'spaghetti_detector', # Commented out in original
|
||||
}
|
||||
# Settings with runtime-generated enum values (loop over MaterialType::all())
|
||||
runtime_enum_keys = {
|
||||
'filament_type', # enum_values from runtime loop
|
||||
}
|
||||
|
||||
# Compare
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Missing keys
|
||||
orig_keys = set(orig.keys())
|
||||
gen_keys = set(gen.keys())
|
||||
|
||||
missing = orig_keys - gen_keys
|
||||
extra = gen_keys - orig_keys
|
||||
|
||||
if missing:
|
||||
real_missing = missing - known_exceptions
|
||||
if real_missing:
|
||||
errors.append(f"MISSING from generated ({len(real_missing)}): {sorted(real_missing)}")
|
||||
noted = missing & known_exceptions
|
||||
if noted:
|
||||
warnings.append(f"Known exceptions (commented out in original): {sorted(noted)}")
|
||||
if extra:
|
||||
warnings.append(f"EXTRA in generated ({len(extra)}): {sorted(extra)}")
|
||||
|
||||
# Compare shared keys
|
||||
shared = orig_keys & gen_keys
|
||||
type_mismatches = []
|
||||
default_mismatches = []
|
||||
enum_val_mismatches = []
|
||||
enum_lbl_mismatches = []
|
||||
enum_map_mismatches = []
|
||||
|
||||
for key in sorted(shared):
|
||||
o = orig[key]
|
||||
g = gen[key]
|
||||
|
||||
if o['co_type'] != g['co_type']:
|
||||
type_mismatches.append(f" {key}: orig={o['co_type']} gen={g['co_type']}")
|
||||
|
||||
if o['has_default'] != g['has_default'] and key not in known_exceptions:
|
||||
default_mismatches.append(f" {key}: orig={o['has_default']} gen={g['has_default']}")
|
||||
|
||||
if o['enum_values'] != g['enum_values'] and key not in runtime_enum_keys:
|
||||
enum_val_mismatches.append(f" {key}: orig={o['enum_values']} gen={g['enum_values']}")
|
||||
|
||||
if o['enum_labels'] != g['enum_labels']:
|
||||
enum_lbl_mismatches.append(f" {key}: orig={o['enum_labels']} gen={g['enum_labels']}")
|
||||
|
||||
if o['has_enum_map'] != g['has_enum_map']:
|
||||
enum_map_mismatches.append(f" {key}: orig={o['has_enum_map']} gen={g['has_enum_map']}")
|
||||
|
||||
# Report
|
||||
print("\n=== VALIDATION RESULTS ===\n")
|
||||
|
||||
if type_mismatches:
|
||||
errors.append(f"TYPE MISMATCHES ({len(type_mismatches)}):\n" + "\n".join(type_mismatches))
|
||||
|
||||
if default_mismatches:
|
||||
errors.append(f"DEFAULT MISMATCHES ({len(default_mismatches)}):\n" + "\n".join(default_mismatches))
|
||||
|
||||
if enum_val_mismatches:
|
||||
warnings.append(f"ENUM VALUE COUNT MISMATCHES ({len(enum_val_mismatches)}):\n" + "\n".join(enum_val_mismatches))
|
||||
|
||||
if enum_lbl_mismatches:
|
||||
warnings.append(f"ENUM LABEL COUNT MISMATCHES ({len(enum_lbl_mismatches)}):\n" + "\n".join(enum_lbl_mismatches))
|
||||
|
||||
if enum_map_mismatches:
|
||||
warnings.append(f"ENUM MAP MISMATCHES ({len(enum_map_mismatches)}):\n" + "\n".join(enum_map_mismatches))
|
||||
|
||||
if warnings:
|
||||
print("WARNINGS:")
|
||||
for w in warnings:
|
||||
print(f" {w}")
|
||||
print()
|
||||
|
||||
if errors:
|
||||
print("ERRORS:")
|
||||
for e in errors:
|
||||
print(f" {e}")
|
||||
print(f"\nValidation FAILED with {len(errors)} error(s)")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"All {len(shared)} shared settings validated successfully")
|
||||
if extra:
|
||||
print(f" ({len(extra)} extra settings from axis expansion)")
|
||||
print("\nValidation PASSED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user