fix(codegen): drop grpcio-tools, stop committing generated code

The codegen only ever needed a protoc binary, but every entry point installed
grpcio-tools to get one. That drags in the grpcio C extension, which has no
Windows/ARM64 wheel and falls back to building from source there, so the ARM64
job died with "Failed building wheel for grpcio" -> "protoc not found".

tools/codegen_toolchain.py resolves the toolchain instead: protoc from $PROTOC,
PATH, a cache, grpc_tools when already installed, or a pinned checksum-verified
protoc release unpacked into .codegen-tools/; protobuf and pyyaml from the
calling interpreter or a cached virtualenv it re-execs into (distro Pythons
refuse `pip install` under PEP 668). All four build scripts and all three CI
jobs are now just `python tools/run_codegen.py`, with no pip lines around it.

tools/config_metadata_pb2.py was the one generated file checked into git. The
orca.* option extensions are now read out of the descriptor set, which already
carries config_metadata.proto via --include_imports, so nothing is generated
into the tree -- and the protobuf>=6.33.5,<7 CI pin goes away with it, since it
only existed to satisfy gencode's hard ValidateProtobufRuntimeVersion check.
Generated C++ verified byte-identical under upb and the pure-Python protobuf
runtime (what win/arm64 installs), and under both grpc_tools' and standalone
protoc.

Also fixed:
- build_release_macos.sh still passed -DPython3_EXECUTABLE=<codegen venv>,
  pointing the bundled *embed* interpreter at the codegen environment -- the
  same confusion fae4b124 fixed on the CMake side.
- Tab.cpp #includes TabLayout_generated.cpp but had no dependency on
  codegen_config, so an incremental build after a .proto edit could compile it
  while the file was being rewritten. libslic3r already had this guard.
- ConfigCodegen.cmake now probes with `codegen_toolchain.py --check` (which
  never downloads or installs), prefers a host interpreter over the embed one,
  and lets a fresh clone generate at configure time instead of erroring out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ExPikaPaka
2026-07-28 12:53:05 +02:00
parent fae4b1241b
commit 2f67e46047
16 changed files with 446 additions and 188 deletions

260
tools/codegen_toolchain.py Normal file
View File

@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""
Toolchain resolution for the config codegen.
The codegen needs exactly three things: a protoc binary, the protobuf Python
runtime and pyyaml. Every entry point used to install `grpcio-tools` to get
protoc, which drags in the grpcio C extension: it has no Windows/ARM64 wheel and
falls back to building from source there, which is what broke the ARM64 build
("Failed building wheel for grpcio" -> "protoc not found"). Nothing in the
codegen uses gRPC.
Resolution order, so callers only ever run `python tools/run_codegen.py`:
protoc $PROTOC -> PATH -> cached download -> grpc_tools (if installed) ->
pinned, checksum-verified protoc release downloaded into
.codegen-tools/ (gitignored)
runtime the current interpreter, else a cached virtualenv under
.codegen-tools/venv that the entry point re-execs into
Set PROTOC=/path/to/protoc (or put protoc on PATH) to build offline.
"""
import hashlib
import importlib.util
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import urllib.request
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CACHE_DIR = ROOT / ".codegen-tools"
# Pinned so every machine generates with the same compiler, and checksummed
# because we execute what we download. To bump: change the version and refresh
# every hash from https://github.com/protocolbuffers/protobuf/releases/tag/v<ver>
PROTOC_VERSION = "28.3"
PROTOC_ARCHIVE_SHA256 = {
"win64": "ce64f49bdeddef49ce4bd313a8f59bcf92fcf67b5831efbf66170386d2e66948",
"linux-x86_64": "0ad949f04a6a174da83cdcbdb36dee0a4925272a5b6d83f79a6bf9852076d53f",
"linux-aarch_64": "1de522032a8b194002fe35cab86d747848238b5e4de4f99648372079f5b46f9a",
"osx-universal_binary": "52df502b263da20f3311b23b5c6553d10cc25c6ebb85df381d80a2806b6a698b",
}
# pip name -> import name
PYTHON_PACKAGES = {"protobuf": "google.protobuf", "pyyaml": "yaml"}
# Guards against an endless re-exec loop if the virtualenv still can't import.
_BOOTSTRAP_ENV = "ORCA_CODEGEN_BOOTSTRAPPED"
_PROTOC_EXE = "protoc.exe" if os.name == "nt" else "protoc"
def _protoc_dir():
return CACHE_DIR / f"protoc-{PROTOC_VERSION}"
def _archive_key():
"""Release asset for this host, or None if protobuf ships no build for it."""
if sys.platform == "win32":
# There is no win/arm64 release; the x64 build runs under Windows'
# emulation, which is how the ARM64 CI job gets a protoc.
return "win64"
if sys.platform == "darwin":
return "osx-universal_binary"
if sys.platform.startswith("linux"):
machine = platform.machine().lower()
if machine in ("x86_64", "amd64"):
return "linux-x86_64"
if machine in ("aarch64", "arm64"):
return "linux-aarch_64"
return None
def _download_protoc():
"""Fetch and unpack the pinned protoc. Returns the binary path, or None."""
key = _archive_key()
if key is None:
print(f" ERROR: no pinned protoc release for {sys.platform}/{platform.machine()}.")
print(" Install protoc from your package manager and re-run, or set PROTOC=<path>.")
return None
url = (f"https://github.com/protocolbuffers/protobuf/releases/download/"
f"v{PROTOC_VERSION}/protoc-{PROTOC_VERSION}-{key}.zip")
print(f" Downloading protoc {PROTOC_VERSION} ({key})...")
CACHE_DIR.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(dir=CACHE_DIR) as tmp:
archive = Path(tmp) / "protoc.zip"
try:
with urllib.request.urlopen(url, timeout=120) as response:
archive.write_bytes(response.read())
except OSError as exc:
print(f" ERROR: download failed: {exc}")
print(" Install protoc manually and re-run, or set PROTOC=<path>.")
return None
actual = hashlib.sha256(archive.read_bytes()).hexdigest()
expected = PROTOC_ARCHIVE_SHA256[key]
if actual != expected:
print(f" ERROR: protoc archive checksum mismatch for {key} {PROTOC_VERSION}:")
print(f" expected {expected}")
print(f" actual {actual}")
return None
# Unpack the whole archive, not just bin/: protoc resolves the well-known
# imports (google/protobuf/descriptor.proto) from ../include next to it.
staging = Path(tmp) / "unpacked"
with zipfile.ZipFile(archive) as zf:
zf.extractall(staging)
target = _protoc_dir()
if target.exists():
shutil.rmtree(target)
# Move into place only once complete, so an interrupted run never leaves
# a half-extracted toolchain that later runs would happily use.
shutil.move(str(staging), str(target))
binary = target / "bin" / _PROTOC_EXE
if not binary.exists():
print(f" ERROR: protoc archive did not contain bin/{_PROTOC_EXE}")
return None
binary.chmod(binary.stat().st_mode | 0o755)
return binary
def find_protoc(allow_download=True):
"""Return the protoc command as a list, or None if it can't be resolved."""
override = os.environ.get("PROTOC")
if override:
return [override]
on_path = shutil.which("protoc")
if on_path:
return [on_path]
cached = _protoc_dir() / "bin" / _PROTOC_EXE
if cached.exists():
return [str(cached)]
# Honour a pre-existing grpcio-tools install rather than downloading.
if importlib.util.find_spec("grpc_tools") is not None:
return [sys.executable, "-m", "grpc_tools.protoc"]
if allow_download:
binary = _download_protoc()
if binary is not None:
return [str(binary)]
return None
def missing_packages():
"""pip names of the required Python packages this interpreter can't import."""
missing = []
for pip_name, module in PYTHON_PACKAGES.items():
try:
found = importlib.util.find_spec(module) is not None
except (ImportError, ValueError):
found = False
if not found:
missing.append(pip_name)
return missing
def _venv_python():
venv_dir = CACHE_DIR / "venv"
if os.name == "nt":
return venv_dir / "Scripts" / "python.exe"
return venv_dir / "bin" / "python"
def bootstrap_python():
"""The cached virtualenv interpreter, if it exists and has the packages."""
python = _venv_python()
if not python.exists():
return None
probe = subprocess.run([str(python), "-c", "import google.protobuf, yaml"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return python if probe.returncode == 0 else None
def _ensure_venv(missing):
"""Create/reuse .codegen-tools/venv with the required packages installed."""
python = _venv_python()
if not python.exists():
print(f" Creating codegen virtualenv in {python.parent.parent}...")
if subprocess.run([sys.executable, "-m", "venv", str(python.parent.parent)]).returncode != 0 \
or not python.exists():
return None
print(f" Installing {', '.join(missing)} into the codegen virtualenv...")
result = subprocess.run([str(python), "-m", "pip", "install", "--quiet",
"--disable-pip-version-check", *missing])
return python if result.returncode == 0 else None
def ensure_python_runtime():
"""
Guarantee protobuf + pyyaml are importable.
If they aren't, re-run the calling script in a cached virtualenv that has
them and exit with its status. Keeping the packages out of the caller's
interpreter is what lets the build scripts work on distros that refuse
`pip install` into a system Python (PEP 668).
"""
missing = missing_packages()
if not missing:
return
if os.environ.get(_BOOTSTRAP_ENV):
# We are already inside the bootstrapped environment: installing again
# would just fail the same way.
print(f" ERROR: {', '.join(missing)} still missing after bootstrap.")
sys.exit(1)
# Reuse a complete virtualenv as-is: builds call this every time, and pip
# would otherwise hit the network on each one.
python = bootstrap_python() or _ensure_venv(missing)
if python is None:
print(f" ERROR: could not provide {', '.join(missing)}.")
print(f" Install them for this interpreter: {sys.executable} -m pip install "
f"{' '.join(missing)}")
sys.exit(1)
env = dict(os.environ, **{_BOOTSTRAP_ENV: "1"})
sys.exit(subprocess.run([str(python), *sys.argv], env=env).returncode)
def toolchain_ready():
"""
True if the codegen can run without downloading or installing anything --
either from this interpreter or by re-execing into an existing bootstrap
virtualenv. This is what decides whether a build regenerates on proto edits.
"""
if missing_packages() and bootstrap_python() is None:
return False
return find_protoc(allow_download=False) is not None
def main():
# --check is what ConfigCodegen.cmake probes with: it answers whether this
# interpreter can regenerate during a build, and must not download, install
# or create anything while doing so.
if "--check" in sys.argv[1:]:
return 0 if toolchain_ready() else 1
missing = missing_packages()
print(f"python: {sys.executable}")
print(f"packages: {'all present' if not missing else 'missing ' + ', '.join(missing)}")
protoc = find_protoc()
print(f"protoc: {' '.join(protoc) if protoc else 'NOT FOUND'}")
return 0 if protoc else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -10,11 +10,10 @@ Usage:
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 2: Run codegen
python tools/config_codegen.py config.desc src/slic3r/GUI/generated/
# Step 3: Run codegen
python tools/config_codegen.py config.desc codegen/generated/
(tools/run_codegen.py does both, and resolves protoc for you)
Outputs:
- PrintConfigDef_generated.cpp (init_fff_params body)
@@ -29,20 +28,22 @@ import re
import argparse
from pathlib import Path
# Add tools/ to path so we can import generated config_metadata_pb2
# Add tools/ to path so we can import the sibling helper modules
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
from config_metadata import load_descriptor_set
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")
print("Ensure protobuf is installed: pip install protobuf")
print("Or just run the pipeline, which bootstraps it: python tools/run_codegen.py")
sys.exit(1)
# The orca.* option extensions and enum constants, bound by main() from the
# descriptor set (see config_metadata.py for why they aren't imported).
meta_pb2 = None
# Proto FieldDescriptorProto.Type enum values
TYPE_DOUBLE = 1
@@ -179,9 +180,8 @@ 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.
extensions registered (see config_metadata.load_descriptor_set) resolves them.
"""
from google.protobuf import descriptor_pb2
opts = field_desc_proto.options
if not opts.ByteSize():
return descriptor_pb2.FieldOptions()
@@ -981,11 +981,8 @@ def main():
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)
global meta_pb2
file_descriptor_set, meta_pb2 = load_descriptor_set(desc_path)
print(f"Loaded {len(file_descriptor_set.file)} proto files")
for fd in file_descriptor_set.file:

67
tools/config_metadata.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Access to the orca.* option extensions declared in config_metadata.proto.
The extensions are read straight out of the compiled descriptor set: protoc runs
with --include_imports, so config_metadata.proto travels inside the .desc file
that the codegen already consumes. Registering it in the default descriptor pool
before the descriptor set is parsed is what makes the custom options resolve
instead of landing in unknown fields.
Doing it this way keeps generated code out of git. The alternative -- a checked-in
config_metadata_pb2.py -- also pinned the protobuf runtime to whichever protoc
produced it, because gencode embeds a hard ValidateProtobufRuntimeVersion() check.
"""
from google.protobuf import descriptor_pb2, descriptor_pool
METADATA_PROTO = "config_metadata.proto"
class Metadata:
"""
Stand-in for the generated config_metadata_pb2 module.
Exposes each orca extension as an attribute holding its FieldDescriptor
(usable as `options.Extensions[meta.label]`) and each enum value as an int
constant (`meta.MODE_SIMPLE`, `meta.STEP_SLICE`, ...), matching how the
generated module was used.
"""
def __init__(self, file_descriptor):
for name, extension in file_descriptor.extensions_by_name.items():
setattr(self, name, extension)
for enum in file_descriptor.enum_types_by_name.values():
for value in enum.values:
setattr(self, value.name, value.number)
def load_descriptor_set(path):
"""
Read a protoc descriptor set and return (FileDescriptorSet, Metadata).
The file is parsed twice on purpose: the first pass only locates the embedded
config_metadata.proto so its extensions can be registered, the second one
parses with those extensions known.
"""
with open(path, 'rb') as f:
raw = f.read()
probe = descriptor_pb2.FileDescriptorSet()
probe.ParseFromString(raw)
metadata_file = next((f for f in probe.file if f.name == METADATA_PROTO), None)
if metadata_file is None:
raise RuntimeError(
f"{path} does not contain {METADATA_PROTO} -- protoc must be run with "
"--include_imports")
pool = descriptor_pool.Default()
try:
file_descriptor = pool.FindFileByName(METADATA_PROTO)
except KeyError:
pool.Add(metadata_file)
file_descriptor = pool.FindFileByName(METADATA_PROTO)
descriptor_set = descriptor_pb2.FileDescriptorSet()
descriptor_set.ParseFromString(raw)
return descriptor_set, Metadata(file_descriptor)

View File

@@ -1,51 +0,0 @@
# -*- 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.33.5
"""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,
33,
5,
'',
'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*S\n\nConfigMode\x12\x0f\n\x0bMODE_SIMPLE\x10\x00\x12\x11\n\rMODE_ADVANCED\x10\x01\x12\x10\n\x0cMODE_DEVELOP\x10\x02\x12\x0f\n\x0bMODE_EXPERT\x10\x03*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\nCoTypeHint\x12\x16\n\x12\x43O_TYPE_HINT_UNSET\x10\x00\x12\r\n\tcoPercent\x10\x01\x12\x0e\n\ncoPercents\x10\x02\x12\n\n\x06\x63oEnum\x10\x03\x12\x0b\n\x07\x63oEnums\x10\x04*\x96\x01\n\x07GuiType\x12\x12\n\x0eGUI_TYPE_UNSET\x10\x00\x12\x0f\n\x0bi_enum_open\x10\x01\x12\x0f\n\x0b\x66_enum_open\x10\x02\x12\t\n\x05\x63olor\x10\x03\x12\x0f\n\x0bselect_open\x10\x04\x12\n\n\x06slider\x10\x05\x12\n\n\x06legend\x10\x06\x12\x0e\n\none_string\x10\x07\x12\x11\n\rplugin_picker\x10\x08:.\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:@\n\x08gui_type\x12\x1d.google.protobuf.FieldOptions\x18\xe3\x86\x03 \x01(\x0e\x32\r.orca.GuiType: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:G\n\x0c\x63o_type_hint\x12\x1d.google.protobuf.FieldOptions\x18\xe8\x86\x03 \x01(\x0e\x32\x10.orca.CoTypeHint: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=231
_globals['_PRESETTYPE']._serialized_start=233
_globals['_PRESETTYPE']._serialized_end=304
_globals['_INVALIDATIONSTEP']._serialized_start=307
_globals['_INVALIDATIONSTEP']._serialized_end=477
_globals['_OPTIONLISTMEMBERSHIP']._serialized_start=480
_globals['_OPTIONLISTMEMBERSHIP']._serialized_end=609
_globals['_COTYPEHINT']._serialized_start=611
_globals['_COTYPEHINT']._serialized_end=703
_globals['_GUITYPE']._serialized_start=706
_globals['_GUITYPE']._serialized_end=856
_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)

View File

@@ -6,17 +6,23 @@ Convenience script: runs the codegen pipeline.
2. Generate C++ from descriptors (config_codegen.py)
3. Validate output against original
The toolchain (protoc, protobuf, pyyaml) is resolved by codegen_toolchain.py, so
this script is the single entry point every build script and CI job calls -- no
`pip install` lines needed around it.
Usage:
python tools/run_codegen.py # full pipeline
python tools/run_codegen.py --validate-only # just validate
"""
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import codegen_toolchain # noqa: E402
ROOT = Path(__file__).resolve().parent.parent
PROTO_DIR = ROOT / "src" / "PrintConfigs"
CODEGEN_OUT = ROOT / "src" / "slic3r" / "GUI" / "generated"
@@ -24,22 +30,6 @@ DESC_FILE = ROOT / "config.desc"
LAYOUT_YAML = PROTO_DIR / "layout.yaml"
def _ensure_pyyaml():
"""Install pyyaml if not present — needed for tab layout generation."""
try:
import yaml # noqa: F401
return True
except ImportError:
print(" Installing pyyaml (required for tab layout generation)...")
result = subprocess.run(
[sys.executable, "-m", "pip", "install", "pyyaml", "-q"],
capture_output=True)
if result.returncode != 0:
print(" ERROR: failed to install pyyaml")
return False
return True
def run(cmd, **kwargs):
print(f" $ {' '.join(str(c) for c in cmd)}")
result = subprocess.run(cmd, **kwargs)
@@ -49,19 +39,6 @@ def run(cmd, **kwargs):
return True
def _protoc_cmd():
"""Return the protoc command list. Prefers standalone protoc, falls back to grpc_tools."""
if shutil.which("protoc"):
return ["protoc"]
try:
import grpc_tools.protoc # noqa: F401
return [sys.executable, "-m", "grpc_tools.protoc"]
except ImportError:
pass
print(" ERROR: protoc not found. Install protoc or run: pip install grpcio-tools")
return None
def step_compile():
print("\n=== Step 1: Compile .proto -> descriptor set ===")
proto_files = [f for f in PROTO_DIR.glob("*.proto") if not f.name.endswith("_gen.proto") and f.name != "config_metadata.proto"]
@@ -69,7 +46,7 @@ def step_compile():
print(" ERROR: No .proto files found")
return False
protoc = _protoc_cmd()
protoc = codegen_toolchain.find_protoc()
if protoc is None:
return False
@@ -82,7 +59,6 @@ def step_compile():
def step_generate():
print("\n=== Step 2: Generate C++ from descriptors + layout.yaml ===")
_ensure_pyyaml() # tab layout generation requires pyyaml
return run([sys.executable, str(ROOT / "tools" / "config_codegen.py"),
str(DESC_FILE), str(CODEGEN_OUT)])
@@ -106,8 +82,11 @@ def main():
help="Skip validation step (used by cmake build)")
args = parser.parse_args()
# Re-execs into a virtualenv with protobuf/pyyaml if this interpreter lacks them.
codegen_toolchain.ensure_python_runtime()
if args.validate_only:
# Compile + lint the protos, then check the committed generated files are current.
# Compile + lint the protos, then check the generated files against PrintConfig.cpp.
sys.exit(0 if (step_compile() and step_lint() and step_validate()) else 1)
for name, fn in [("Compile", step_compile), ("Generate", step_generate)]: